diff --git a/crates/openlogi-gui/src/app.rs b/crates/openlogi-gui/src/app.rs index 5a171e9d..0d6310ec 100644 --- a/crates/openlogi-gui/src/app.rs +++ b/crates/openlogi-gui/src/app.rs @@ -7,7 +7,7 @@ use gpui::{ img, point, prelude::FluentBuilder as _, px, relative, rgb, svg, }; use gpui_component::{ - Icon, IconName, Sizable as _, + Icon, IconName, Sizable as _, TitleBar, description_list::{DescriptionItem, DescriptionList}, h_flex, scroll::ScrollableElement as _, @@ -341,7 +341,13 @@ impl Render for AppView { .track_focus(&self.focus_handle) .on_action(|_: &CloseWindow, window, _| window.remove_window()) .on_action(|_: &Minimize, window, _| window.minimize_window()) - .on_action(|_: &Zoom, window, _| window.zoom_window()); + .on_action(|_: &Zoom, window, _| window.zoom_window()) + // Linux only: a client-side titlebar (window controls + drag region) + // on every frame, so the chrome is present from the first connecting + // frame on. macOS / Windows keep their native titlebar. + .when(cfg!(target_os = "linux"), |this| { + this.child(app_title_bar(pal)) + }); // The agent is the source of truth for both the permission state and // the device list; `AgentLink` is everything the GUI knows about it. @@ -454,6 +460,25 @@ impl Render for AppView { } } +/// Client-side window titlebar: window controls (minimize / maximize / close on +/// Linux + Windows), the drag region, and the app name centred. Replaces the +/// native titlebar so Linux — where the compositor declines server-side +/// decorations and gpui falls back to client-side ones it doesn't paint — still +/// gets a titlebar and window controls. On macOS the widget reserves the +/// traffic-light space. +fn app_title_bar(pal: Palette) -> impl IntoElement { + TitleBar::new().child( + div() + .flex_1() + .flex() + .items_center() + .justify_center() + .text_sm() + .text_color(pal.text_muted) + .child("OpenLogi"), + ) +} + /// Home (gallery) top bar: the "Devices" title, a Settings gear, and the /// Add-Device button — the entry points the old carousel header used to carry. fn home_header(pal: Palette) -> impl IntoElement { diff --git a/crates/openlogi-gui/src/main.rs b/crates/openlogi-gui/src/main.rs index b1a38617..ee410180 100644 --- a/crates/openlogi-gui/src/main.rs +++ b/crates/openlogi-gui/src/main.rs @@ -56,8 +56,7 @@ use std::time::{Duration, Instant}; use anyhow::Result; use backon::{BackoffBuilder, ExponentialBuilder}; use gpui::{ - AppContext, BorrowAppContext as _, Bounds, SharedString, Size, Styled, TitlebarOptions, - WindowBounds, WindowOptions, px, + AppContext, BorrowAppContext as _, Bounds, Size, Styled, WindowBounds, WindowOptions, px, }; use gpui_component::{ActiveTheme, Root}; use openlogi_core::brand::DeeplinkCommand; @@ -559,11 +558,10 @@ fn main_window_options(cx: &mut gpui::App) -> WindowOptions { // overlap; below this the model can't shrink further without crowding. window_min_size: Some(Size::new(px(720.), px(680.))), app_id: Some("openlogi".to_string()), - titlebar: Some(TitlebarOptions { - title: Some(SharedString::from("OpenLogi")), - appears_transparent: false, - traffic_light_position: None, - }), + // Linux: transparent chrome so `AppView::render` can draw a client-side + // `TitleBar` (the compositor declines server-side decorations and gpui's + // fallback is unpainted). macOS/Windows keep their native titlebar. + titlebar: Some(windows::titlebar_options("OpenLogi")), ..WindowOptions::default() } } diff --git a/crates/openlogi-gui/src/windows/add_device.rs b/crates/openlogi-gui/src/windows/add_device.rs index 051e83c4..435153cb 100644 --- a/crates/openlogi-gui/src/windows/add_device.rs +++ b/crates/openlogi-gui/src/windows/add_device.rs @@ -17,7 +17,7 @@ use gpui::{ App, Context, FocusHandle, FontWeight, Global, InteractiveElement, IntoElement, ParentElement as _, Render, SharedString, Size, StatefulInteractiveElement as _, Styled as _, - Subscription, Window, div, px, rgb, + Subscription, Window, div, prelude::FluentBuilder as _, px, rgb, }; use gpui_component::v_flex; use openlogi_agent_core::ipc::{FoundDevice, PairingFailure, PairingUpdate}; @@ -183,15 +183,23 @@ impl Render for AddDeviceView { .on_action(|_: &CloseWindow, window, _| window.remove_window()) .on_action(|_: &Minimize, window, _| window.minimize_window()) .on_action(|_: &Zoom, window, _| window.zoom_window()) - .p_6() - .gap_5() + .when(cfg!(target_os = "linux"), |this| { + this.child(windows::aux_title_bar(tr!("Add Device"), cx)) + }) .child( - div() - .text_lg() - .font_weight(FontWeight::SEMIBOLD) - .child(tr!("Add Device")), + v_flex() + .flex_1() + .w_full() + .p_6() + .gap_5() + .child( + div() + .text_lg() + .font_weight(FontWeight::SEMIBOLD) + .child(tr!("Add Device")), + ) + .child(body(&state, pal)), ) - .child(body(&state, pal)) } } diff --git a/crates/openlogi-gui/src/windows/mod.rs b/crates/openlogi-gui/src/windows/mod.rs index 9d6053cb..2e5a71bc 100644 --- a/crates/openlogi-gui/src/windows/mod.rs +++ b/crates/openlogi-gui/src/windows/mod.rs @@ -14,10 +14,11 @@ pub mod settings; pub mod update_consent; use gpui::{ - App, AppContext as _, Bounds, Context, Global, Pixels, Render, SharedString, Size, Styled as _, - Subscription, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, + App, AppContext as _, Bounds, Context, Global, IntoElement, ParentElement as _, Pixels, Render, + SharedString, Size, Styled as _, Subscription, TitlebarOptions, WindowBounds, WindowHandle, + WindowOptions, div, }; -use gpui_component::{ActiveTheme as _, Root}; +use gpui_component::{ActiveTheme as _, Root, TitleBar}; use tracing::warn; /// One live handle per auxiliary window, stored as a GPUI global so the menu @@ -35,6 +36,45 @@ pub struct WindowRegistry { impl Global for WindowRegistry {} +/// Titlebar options for an app window. +/// +/// On Linux this returns transparent options so the view can draw a client-side +/// [`TitleBar`] (see [`aux_title_bar`]); the compositor declines server-side +/// decorations there and gpui's client-side fallback is otherwise unpainted, +/// leaving the window with no titlebar or controls. On macOS / Windows it keeps +/// the native titlebar carrying `title`, unchanged. +pub fn titlebar_options(title: impl Into) -> TitlebarOptions { + if cfg!(target_os = "linux") { + TitleBar::title_bar_options() + } else { + TitlebarOptions { + title: Some(title.into()), + appears_transparent: false, + traffic_light_position: None, + } + } +} + +/// Client-side window titlebar for auxiliary windows: window controls +/// (minimize / maximize / close on Linux + Windows), the drag region, and the +/// window `title` centred. Each auxiliary view renders this as the top of its +/// layout so the window has a titlebar and controls on Linux, where the +/// compositor declines server-side decorations and gpui's client-side fallback +/// is otherwise unpainted. On macOS the widget reserves the traffic-light space. +pub fn aux_title_bar(title: impl Into, cx: &App) -> impl IntoElement { + let title = title.into(); + TitleBar::new().child( + div() + .flex_1() + .flex() + .items_center() + .justify_center() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child(title), + ) +} + /// Implemented by every auxiliary root view so [`open_or_focus`] can hand it /// the appearance observer to hold onto — dropping the [`Subscription`] would /// detach the OS light/dark tracking and leave the window stuck on one theme. @@ -73,11 +113,7 @@ pub fn open_or_focus( let options = WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), app_id: Some("openlogi".to_string()), - titlebar: Some(TitlebarOptions { - title: Some(title.clone()), - appears_transparent: false, - traffic_light_position: None, - }), + titlebar: Some(titlebar_options(title.clone())), ..WindowOptions::default() }; diff --git a/crates/openlogi-gui/src/windows/settings.rs b/crates/openlogi-gui/src/windows/settings.rs index 37b42964..afc36f5a 100644 --- a/crates/openlogi-gui/src/windows/settings.rs +++ b/crates/openlogi-gui/src/windows/settings.rs @@ -18,8 +18,8 @@ pub(super) use gpui::{ prelude::FluentBuilder, px, rgb, }; pub(super) use gpui_component::{ - ActiveTheme, Disableable, Icon, IconName, IndexPath, Selectable, Sizable, Theme, ThemeColor, - ThemeMode, ThemeRegistry, + ActiveTheme, Disableable, Icon, IconName, IndexPath, Selectable, Sizable, TITLE_BAR_HEIGHT, + Theme, ThemeColor, ThemeMode, ThemeRegistry, button::{Button, ButtonGroup, ButtonVariants}, group_box::GroupBoxVariant, h_flex, @@ -247,7 +247,7 @@ pub fn open(cx: &mut App) { pub fn open_at(page: SettingsPage, cx: &mut App) { windows::open_or_focus( |reg| &mut reg.settings, - "Settings", + tr!("Settings"), Size::new(px(840.), px(600.)), move |window, cx| SettingsView::new(page, window, cx), cx, @@ -261,12 +261,27 @@ impl Render for SettingsView { div() .size_full() + .relative() .bg(pal.bg) .text_color(pal.text_primary) .track_focus(&self.focus_handle) .on_action(|_: &CloseWindow, window, _| window.remove_window()) .on_action(|_: &Minimize, window, _| window.minimize_window()) .on_action(|_: &Zoom, window, _| window.zoom_window()) + // Linux only: a client-side titlebar as an absolute overlay (with + // matching top padding) rather than a flex-column row — the + // `Settings` sidebar uses `h_resizable` percentage sizing, which a + // flex column would break. macOS / Windows keep their native titlebar. + .when(cfg!(target_os = "linux"), |this| { + this.pt(TITLE_BAR_HEIGHT).child( + div() + .absolute() + .top_0() + .left_0() + .right_0() + .child(windows::aux_title_bar(tr!("Settings"), cx)), + ) + }) .child( // Outline group boxes give every page bordered cards (depth / // definition that the flat Fill variant lacked); the hero / diff --git a/crates/openlogi-gui/src/windows/update_consent.rs b/crates/openlogi-gui/src/windows/update_consent.rs index ef8e33a3..100ab508 100644 --- a/crates/openlogi-gui/src/windows/update_consent.rs +++ b/crates/openlogi-gui/src/windows/update_consent.rs @@ -8,7 +8,8 @@ use gpui::{ App, BorrowAppContext as _, Context, FocusHandle, FontWeight, InteractiveElement, IntoElement, - ParentElement as _, Render, Size, Styled as _, Subscription, Window, div, px, + ParentElement as _, Render, Size, Styled as _, Subscription, Window, div, + prelude::FluentBuilder as _, px, }; use gpui_component::{ button::{Button, ButtonVariants as _}, @@ -77,16 +78,23 @@ impl Render for UpdateConsentView { .on_action(|_: &CloseWindow, window, _| window.remove_window()) .on_action(|_: &Minimize, window, _| window.minimize_window()) .on_action(|_: &Zoom, window, _| window.zoom_window()) - .items_center() - .justify_center() - .gap_4() - .p_6() + .when(cfg!(target_os = "linux"), |this| { + this.child(windows::aux_title_bar(tr!("OpenLogi"), cx)) + }) .child( - div() - .text_lg() - .font_weight(FontWeight::SEMIBOLD) - .child(tr!("Check for updates?")), - ) + v_flex() + .flex_1() + .w_full() + .items_center() + .justify_center() + .gap_4() + .p_6() + .child( + div() + .text_lg() + .font_weight(FontWeight::SEMIBOLD) + .child(tr!("Check for updates?")), + ) .child( div() .max_w(px(320.)) @@ -116,5 +124,6 @@ impl Render for UpdateConsentView { .on_click(|_, window, cx| answer(true, window, cx)), ), ) + ) } }