wip: titlebar
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 2m0s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m36s
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 2m0s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m36s
This commit is contained in:
16
Cargo.lock
generated
16
Cargo.lock
generated
@@ -1317,6 +1317,7 @@ dependencies = [
|
||||
"smol",
|
||||
"state",
|
||||
"theme",
|
||||
"titlebar",
|
||||
"tracing-subscriber",
|
||||
"ui",
|
||||
]
|
||||
@@ -6570,6 +6571,21 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "titlebar"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"common",
|
||||
"gpui",
|
||||
"linicon",
|
||||
"log",
|
||||
"smallvec",
|
||||
"theme",
|
||||
"ui",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.49.0"
|
||||
|
||||
@@ -29,6 +29,7 @@ icons = [
|
||||
[dependencies]
|
||||
assets = { path = "../assets" }
|
||||
ui = { path = "../ui" }
|
||||
titlebar = { path = "../titlebar" }
|
||||
dock = { path = "../dock" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
|
||||
@@ -13,6 +13,7 @@ use gpui::{
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::{ActiveTheme, Theme, ThemeRegistry};
|
||||
use titlebar::TitleBar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::{h_flex, v_flex, Root, Sizable, WindowExtension};
|
||||
@@ -28,6 +29,9 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Workspace {
|
||||
/// App's Title Bar
|
||||
titlebar: Entity<TitleBar>,
|
||||
|
||||
/// App's Dock Area
|
||||
dock: Entity<DockArea>,
|
||||
|
||||
@@ -38,6 +42,7 @@ pub struct Workspace {
|
||||
impl Workspace {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let titlebar = cx.new(|_| TitleBar::new());
|
||||
let dock =
|
||||
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
|
||||
|
||||
@@ -100,6 +105,7 @@ impl Workspace {
|
||||
});
|
||||
|
||||
Self {
|
||||
titlebar,
|
||||
dock,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
@@ -278,8 +284,14 @@ impl Render for Workspace {
|
||||
.on_action(cx.listener(Self::on_keyring))
|
||||
.relative()
|
||||
.size_full()
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
// Title Bar
|
||||
.child(self.titlebar.clone())
|
||||
// Dock
|
||||
.child(self.dock.clone())
|
||||
.child(self.dock.clone()),
|
||||
)
|
||||
// Notifications
|
||||
.children(notification_layer)
|
||||
// Modals
|
||||
|
||||
@@ -89,6 +89,10 @@ pub struct ThemeColors {
|
||||
pub drop_target_background: Hsla,
|
||||
pub cursor: Hsla,
|
||||
pub selection: Hsla,
|
||||
|
||||
// System
|
||||
pub titlebar: Hsla,
|
||||
pub titlebar_inactive: Hsla,
|
||||
}
|
||||
|
||||
/// The default colors for the theme.
|
||||
@@ -171,6 +175,9 @@ impl ThemeColors {
|
||||
drop_target_background: brand().dark_alpha().step_2(),
|
||||
cursor: hsl(200., 100., 50.),
|
||||
selection: hsl(200., 100., 50.).alpha(0.25),
|
||||
|
||||
titlebar: neutral().dark().step_2(),
|
||||
titlebar_inactive: neutral().dark().step_3(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
crates/titlebar/Cargo.toml
Normal file
21
crates/titlebar/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "titlebar"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
theme = { path = "../theme" }
|
||||
ui = { path = "../ui" }
|
||||
|
||||
gpui.workspace = true
|
||||
smallvec.workspace = true
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.61", features = ["Wdk_System_SystemServices"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
linicon = "2.3.0"
|
||||
181
crates/titlebar/src/lib.rs
Normal file
181
crates/titlebar/src/lib.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use std::mem;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
#[cfg(target_os = "linux")]
|
||||
use gpui::MouseButton;
|
||||
use gpui::{
|
||||
div, px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
|
||||
ParentElement, Pixels, Render, StatefulInteractiveElement as _, Styled, Window,
|
||||
WindowControlArea,
|
||||
};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING};
|
||||
use ui::h_flex;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::platforms::linux::LinuxWindowControls;
|
||||
use crate::platforms::windows::WindowsWindowControls;
|
||||
|
||||
mod platforms;
|
||||
|
||||
pub struct TitleBar {
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
should_move: bool,
|
||||
}
|
||||
|
||||
impl Default for TitleBar {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TitleBar {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
children: smallvec![],
|
||||
should_move: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn height(window: &mut Window) -> Pixels {
|
||||
(1.75 * window.rem_size()).max(px(34.))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn height(_window: &mut Window) -> Pixels {
|
||||
px(32.)
|
||||
}
|
||||
|
||||
pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
|
||||
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
if window.is_window_active() && !self.should_move {
|
||||
cx.theme().titlebar
|
||||
} else {
|
||||
cx.theme().titlebar_inactive
|
||||
}
|
||||
} else {
|
||||
cx.theme().titlebar
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_children<T>(&mut self, children: T)
|
||||
where
|
||||
T: IntoIterator<Item = AnyElement>,
|
||||
{
|
||||
self.children = children.into_iter().collect();
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for TitleBar {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TitleBar {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
#[cfg(target_os = "linux")]
|
||||
let supported_controls = window.window_controls();
|
||||
let decorations = window.window_decorations();
|
||||
let height = Self::height(window);
|
||||
let color = self.title_bar_color(window, cx);
|
||||
let children = mem::take(&mut self.children);
|
||||
|
||||
h_flex()
|
||||
.window_control_area(WindowControlArea::Drag)
|
||||
.h(height)
|
||||
.w_full()
|
||||
.map(|this| {
|
||||
if window.is_fullscreen() {
|
||||
this.px_2()
|
||||
} else if cx.theme().platform.is_mac() {
|
||||
this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
|
||||
.pr_2()
|
||||
.when(children.len() <= 1, |this| {
|
||||
this.pr(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
|
||||
})
|
||||
} else {
|
||||
this.px_2()
|
||||
}
|
||||
})
|
||||
.map(|this| match decorations {
|
||||
Decorations::Server => this,
|
||||
Decorations::Client { tiling } => this
|
||||
.when(!(tiling.top || tiling.right), |el| {
|
||||
el.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |el| {
|
||||
el.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
}),
|
||||
})
|
||||
.bg(color)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.content_stretch()
|
||||
.child(
|
||||
div()
|
||||
.id("title-bar")
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.when(cx.theme().platform.is_mac(), |this| {
|
||||
this.on_click(|event, window, _| {
|
||||
if event.click_count() == 2 {
|
||||
window.titlebar_double_click();
|
||||
}
|
||||
})
|
||||
})
|
||||
.when(cx.theme().platform.is_linux(), |this| {
|
||||
this.on_click(|event, window, _| {
|
||||
if event.click_count() == 2 {
|
||||
window.zoom_window();
|
||||
}
|
||||
})
|
||||
})
|
||||
.children(children),
|
||||
)
|
||||
.when(!window.is_fullscreen(), |this| match cx.theme().platform {
|
||||
PlatformKind::Linux => {
|
||||
#[cfg(target_os = "linux")]
|
||||
if matches!(decorations, Decorations::Client { .. }) {
|
||||
this.child(LinuxWindowControls::new(None))
|
||||
.when(supported_controls.window_menu, |this| {
|
||||
this.on_mouse_down(MouseButton::Right, move |ev, window, _| {
|
||||
window.show_window_menu(ev.position)
|
||||
})
|
||||
})
|
||||
.on_mouse_move(cx.listener(move |this, _ev, window, _| {
|
||||
if this.should_move {
|
||||
this.should_move = false;
|
||||
window.start_window_move();
|
||||
}
|
||||
}))
|
||||
.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
|
||||
this.should_move = false;
|
||||
}))
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |this, _ev, _window, _cx| {
|
||||
this.should_move = false;
|
||||
}),
|
||||
)
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |this, _ev, _window, _cx| {
|
||||
this.should_move = true;
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
this
|
||||
}
|
||||
PlatformKind::Windows => this.child(WindowsWindowControls::new(height)),
|
||||
PlatformKind::Mac => this,
|
||||
})
|
||||
}
|
||||
}
|
||||
221
crates/titlebar/src/platforms/linux.rs
Normal file
221
crates/titlebar/src/platforms/linux.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
svg, Action, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use linicon::{lookup_icon, IconType};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_flex, Icon, IconName, Sizable};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct LinuxWindowControls {
|
||||
close_window_action: Option<Box<dyn Action>>,
|
||||
}
|
||||
|
||||
impl LinuxWindowControls {
|
||||
pub fn new(close_window_action: Option<Box<dyn Action>>) -> Self {
|
||||
Self {
|
||||
close_window_action,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for LinuxWindowControls {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id("linux-window-controls")
|
||||
.gap_2()
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child(WindowControl::new(
|
||||
LinuxControl::Minimize,
|
||||
IconName::WindowMinimize,
|
||||
))
|
||||
.child({
|
||||
if window.is_maximized() {
|
||||
WindowControl::new(LinuxControl::Restore, IconName::WindowRestore)
|
||||
} else {
|
||||
WindowControl::new(LinuxControl::Maximize, IconName::WindowMaximize)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
WindowControl::new(LinuxControl::Close, IconName::WindowClose)
|
||||
.when_some(self.close_window_action, |this, close_action| {
|
||||
this.close_action(close_action)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct WindowControl {
|
||||
kind: LinuxControl,
|
||||
fallback: IconName,
|
||||
close_action: Option<Box<dyn Action>>,
|
||||
}
|
||||
|
||||
impl WindowControl {
|
||||
pub fn new(kind: LinuxControl, fallback: IconName) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
fallback,
|
||||
close_action: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_action(mut self, action: Box<dyn Action>) -> Self {
|
||||
self.close_action = Some(action);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_gnome(&self) -> bool {
|
||||
matches!(detect_desktop_environment(), DesktopEnvironment::Gnome)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for WindowControl {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let is_gnome = self.is_gnome();
|
||||
|
||||
h_flex()
|
||||
.id(self.kind.as_icon_name())
|
||||
.group("")
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.rounded_full()
|
||||
.size_6()
|
||||
.when(is_gnome, |this| {
|
||||
this.bg(cx.theme().ghost_element_background_alt)
|
||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||
.active(|this| this.bg(cx.theme().ghost_element_active))
|
||||
})
|
||||
.map(|this| {
|
||||
if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() {
|
||||
this.child(
|
||||
svg()
|
||||
.external_path(SharedString::from(path))
|
||||
.size_4()
|
||||
.text_color(cx.theme().text),
|
||||
)
|
||||
} else {
|
||||
this.child(Icon::new(self.fallback).small().text_color(cx.theme().text))
|
||||
}
|
||||
})
|
||||
.on_mouse_move(|_, _window, cx| cx.stop_propagation())
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
match self.kind {
|
||||
LinuxControl::Minimize => window.minimize_window(),
|
||||
LinuxControl::Restore => window.zoom_window(),
|
||||
LinuxControl::Maximize => window.zoom_window(),
|
||||
LinuxControl::Close => cx.quit(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static DE: OnceLock<DesktopEnvironment> = OnceLock::new();
|
||||
static LINUX_CONTROLS: OnceLock<HashMap<LinuxControl, Option<String>>> = OnceLock::new();
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum DesktopEnvironment {
|
||||
Gnome,
|
||||
Kde,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Detect the current desktop environment
|
||||
pub fn detect_desktop_environment() -> &'static DesktopEnvironment {
|
||||
DE.get_or_init(|| {
|
||||
// Try to use environment variables first
|
||||
if let Ok(output) = std::env::var("XDG_CURRENT_DESKTOP") {
|
||||
let desktop = output.to_lowercase();
|
||||
if desktop.contains("gnome") {
|
||||
return DesktopEnvironment::Gnome;
|
||||
} else if desktop.contains("kde") {
|
||||
return DesktopEnvironment::Kde;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback detection methods
|
||||
if let Ok(output) = std::env::var("DESKTOP_SESSION") {
|
||||
let session = output.to_lowercase();
|
||||
if session.contains("gnome") {
|
||||
return DesktopEnvironment::Gnome;
|
||||
} else if session.contains("kde") || session.contains("plasma") {
|
||||
return DesktopEnvironment::Kde;
|
||||
}
|
||||
}
|
||||
|
||||
DesktopEnvironment::Unknown
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum LinuxControl {
|
||||
Minimize,
|
||||
Restore,
|
||||
Maximize,
|
||||
Close,
|
||||
}
|
||||
|
||||
impl LinuxControl {
|
||||
pub fn as_icon_name(&self) -> &'static str {
|
||||
match self {
|
||||
LinuxControl::Close => "window-close",
|
||||
LinuxControl::Minimize => "window-minimize",
|
||||
LinuxControl::Maximize => "window-maximize",
|
||||
LinuxControl::Restore => "window-restore",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn linux_controls() -> &'static HashMap<LinuxControl, Option<String>> {
|
||||
LINUX_CONTROLS.get_or_init(|| {
|
||||
let mut icons = HashMap::new();
|
||||
icons.insert(LinuxControl::Close, None);
|
||||
icons.insert(LinuxControl::Minimize, None);
|
||||
icons.insert(LinuxControl::Maximize, None);
|
||||
icons.insert(LinuxControl::Restore, None);
|
||||
|
||||
let icon_names = [
|
||||
(LinuxControl::Close, vec!["window-close", "dialog-close"]),
|
||||
(
|
||||
LinuxControl::Minimize,
|
||||
vec!["window-minimize", "window-lower"],
|
||||
),
|
||||
(
|
||||
LinuxControl::Maximize,
|
||||
vec!["window-maximize", "window-expand"],
|
||||
),
|
||||
(
|
||||
LinuxControl::Restore,
|
||||
vec!["window-restore", "window-return"],
|
||||
),
|
||||
];
|
||||
|
||||
for (control, icon_names) in icon_names {
|
||||
for icon_name in icon_names {
|
||||
// Try GNOME-style naming first
|
||||
let mut control_icon = lookup_icon(format!("{icon_name}-symbolic"))
|
||||
.find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG));
|
||||
|
||||
// If not found, try KDE-style naming
|
||||
if control_icon.is_none() {
|
||||
control_icon = lookup_icon(icon_name)
|
||||
.find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG));
|
||||
}
|
||||
|
||||
if let Some(Ok(icon)) = control_icon {
|
||||
icons
|
||||
.entry(control)
|
||||
.and_modify(|v| *v = Some(icon.path.to_string_lossy().to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
icons
|
||||
})
|
||||
}
|
||||
6
crates/titlebar/src/platforms/mac.rs
Normal file
6
crates/titlebar/src/platforms/mac.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
/// Use pixels here instead of a rem-based size because the macOS traffic
|
||||
/// lights are a static size, and don't scale with the rest of the UI.
|
||||
///
|
||||
/// Magic number: There is one extra pixel of padding on the left side due to
|
||||
/// the 1px border around the window on macOS apps.
|
||||
pub const TRAFFIC_LIGHT_PADDING: f32 = 80.;
|
||||
4
crates/titlebar/src/platforms/mod.rs
Normal file
4
crates/titlebar/src/platforms/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
pub mod mac;
|
||||
pub mod windows;
|
||||
147
crates/titlebar/src/platforms/windows.rs
Normal file
147
crates/titlebar/src/platforms/windows.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels,
|
||||
RenderOnce, Rgba, StatefulInteractiveElement, Styled, Window, WindowControlArea,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::h_flex;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct WindowsWindowControls {
|
||||
button_height: Pixels,
|
||||
}
|
||||
|
||||
impl WindowsWindowControls {
|
||||
pub fn new(button_height: Pixels) -> Self {
|
||||
Self { button_height }
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn get_font() -> &'static str {
|
||||
"Segoe Fluent Icons"
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_font() -> &'static str {
|
||||
use windows::Wdk::System::SystemServices::RtlGetVersion;
|
||||
|
||||
let mut version = unsafe { std::mem::zeroed() };
|
||||
let status = unsafe { RtlGetVersion(&mut version) };
|
||||
|
||||
if status.is_ok() && version.dwBuildNumber >= 22000 {
|
||||
"Segoe Fluent Icons"
|
||||
} else {
|
||||
"Segoe MDL2 Assets"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for WindowsWindowControls {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let close_button_hover_color = Rgba {
|
||||
r: 232.0 / 255.0,
|
||||
g: 17.0 / 255.0,
|
||||
b: 32.0 / 255.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
let button_hover_color = cx.theme().ghost_element_hover;
|
||||
let button_active_color = cx.theme().ghost_element_active;
|
||||
|
||||
div()
|
||||
.id("windows-window-controls")
|
||||
.font_family(Self::get_font())
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_center()
|
||||
.content_stretch()
|
||||
.max_h(self.button_height)
|
||||
.min_h(self.button_height)
|
||||
.child(WindowsCaptionButton::new(
|
||||
"minimize",
|
||||
WindowsCaptionButtonIcon::Minimize,
|
||||
button_hover_color,
|
||||
button_active_color,
|
||||
))
|
||||
.child(WindowsCaptionButton::new(
|
||||
"maximize-or-restore",
|
||||
if window.is_maximized() {
|
||||
WindowsCaptionButtonIcon::Restore
|
||||
} else {
|
||||
WindowsCaptionButtonIcon::Maximize
|
||||
},
|
||||
button_hover_color,
|
||||
button_active_color,
|
||||
))
|
||||
.child(WindowsCaptionButton::new(
|
||||
"close",
|
||||
WindowsCaptionButtonIcon::Close,
|
||||
close_button_hover_color,
|
||||
button_active_color,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum WindowsCaptionButtonIcon {
|
||||
Minimize,
|
||||
Restore,
|
||||
Maximize,
|
||||
Close,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct WindowsCaptionButton {
|
||||
id: ElementId,
|
||||
icon: WindowsCaptionButtonIcon,
|
||||
hover_background_color: Hsla,
|
||||
active_background_color: Hsla,
|
||||
}
|
||||
|
||||
impl WindowsCaptionButton {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
icon: WindowsCaptionButtonIcon,
|
||||
hover_background_color: impl Into<Hsla>,
|
||||
active_background_color: impl Into<Hsla>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
icon,
|
||||
hover_background_color: hover_background_color.into(),
|
||||
active_background_color: active_background_color.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for WindowsCaptionButton {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.justify_center()
|
||||
.content_center()
|
||||
.occlude()
|
||||
.w(px(36.))
|
||||
.h_full()
|
||||
.text_size(px(10.0))
|
||||
.hover(|style| style.bg(self.hover_background_color))
|
||||
.active(|style| style.bg(self.active_background_color))
|
||||
.map(|this| match self.icon {
|
||||
WindowsCaptionButtonIcon::Close => {
|
||||
this.window_control_area(WindowControlArea::Close)
|
||||
}
|
||||
WindowsCaptionButtonIcon::Maximize | WindowsCaptionButtonIcon::Restore => {
|
||||
this.window_control_area(WindowControlArea::Max)
|
||||
}
|
||||
WindowsCaptionButtonIcon::Minimize => {
|
||||
this.window_control_area(WindowControlArea::Min)
|
||||
}
|
||||
})
|
||||
.child(match self.icon {
|
||||
WindowsCaptionButtonIcon::Minimize => "\u{e921}",
|
||||
WindowsCaptionButtonIcon::Restore => "\u{e923}",
|
||||
WindowsCaptionButtonIcon::Maximize => "\u{e922}",
|
||||
WindowsCaptionButtonIcon::Close => "\u{e8bb}",
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user