diff --git a/Cargo.lock b/Cargo.lock index 1c2812cf62..cf1246b082 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3117,8 +3117,11 @@ dependencies = [ "markup5ever_rcdom", "notify", "num-traits", + "objc2 0.6.4", + "objc2-foundation 0.3.2", "once_cell", "paste", + "raw-window-handle", "regex", "ropey", "rust-i18n", @@ -3167,6 +3170,7 @@ dependencies = [ "tree-sitter-zig", "unicode-segmentation", "uuid", + "windows-version", "zed-sum-tree", ] @@ -5381,6 +5385,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.1", + "block2 0.6.2", + "libc", "objc2 0.6.4", "objc2-core-foundation", ] diff --git a/crates/story/src/gallery.rs b/crates/story/src/gallery.rs index adad3e59e2..bfd0b67d20 100644 --- a/crates/story/src/gallery.rs +++ b/crates/story/src/gallery.rs @@ -270,6 +270,9 @@ impl Render for Gallery { v_flex() .flex_1() .h_full() + // Content layer: opaque background covers the window glass + // (only the navigation areas reveal it). + .bg(cx.theme().background) .overflow_x_hidden() .child( h_flex() diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 8fdd6e9a87..2a20fce9a1 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -56,7 +56,8 @@ actions!( Tab, TabPrev, ShowPanelInfo, - ToggleListActiveHighlight + ToggleListActiveHighlight, + ToggleWindowGlass ] ); diff --git a/crates/story/src/stories/alert_story.rs b/crates/story/src/stories/alert_story.rs index bb6e085a1a..3052928e9e 100644 --- a/crates/story/src/stories/alert_story.rs +++ b/crates/story/src/stories/alert_story.rs @@ -21,7 +21,11 @@ pub struct AlertStory { impl AlertStory { fn new(_: &mut Window, cx: &mut Context) -> Self { - Self { size: Size::default(), banner_visible: true, focus_handle: cx.focus_handle() } + Self { + size: Size::default(), + banner_visible: true, + focus_handle: cx.focus_handle(), + } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { @@ -67,13 +71,25 @@ impl Render for AlertStory { .outline() .compact() .child( - Button::new("xsmall").label("XSmall").selected(self.size == Size::XSmall), + Button::new("xsmall") + .label("XSmall") + .selected(self.size == Size::XSmall), ) - .child(Button::new("small").label("Small").selected(self.size == Size::Small)) .child( - Button::new("medium").label("Medium").selected(self.size == Size::Medium), + Button::new("small") + .label("Small") + .selected(self.size == Size::Small), + ) + .child( + Button::new("medium") + .label("Medium") + .selected(self.size == Size::Medium), + ) + .child( + Button::new("large") + .label("Large") + .selected(self.size == Size::Large), ) - .child(Button::new("large").label("Large").selected(self.size == Size::Large)) .on_click(cx.listener(|this, selecteds: &Vec, window, cx| { let size = match selecteds[0] { 0 => Size::XSmall, diff --git a/crates/story/src/stories/badge_story.rs b/crates/story/src/stories/badge_story.rs index b78c9c6d3d..5319d39e5a 100644 --- a/crates/story/src/stories/badge_story.rs +++ b/crates/story/src/stories/badge_story.rs @@ -3,8 +3,8 @@ use gpui::{ Styled, Window, }; use gpui_component::{ - avatar::Avatar, badge::Badge, dock::PanelControl, v_flex, ActiveTheme as _, Icon, IconName, - Sizable as _, + ActiveTheme as _, Icon, IconName, Sizable as _, avatar::Avatar, badge::Badge, + dock::PanelControl, v_flex, }; use crate::section; diff --git a/crates/story/src/stories/combobox_story.rs b/crates/story/src/stories/combobox_story.rs index 86e5eddd9a..1918995b2f 100644 --- a/crates/story/src/stories/combobox_story.rs +++ b/crates/story/src/stories/combobox_story.rs @@ -365,7 +365,7 @@ impl ComboboxStory { fn new(window: &mut Window, cx: &mut App) -> Entity { let basic = cx.new(|cx| { ComboboxState::new(SearchableVec::new(FRAMEWORKS.to_vec()), vec![], window, cx) - .searchable(true) + .searchable(true) }); let basic_multi = cx.new(|cx| { @@ -376,7 +376,7 @@ impl ComboboxStory { let grouped = cx.new(|cx| { ComboboxState::new(food_groups(), vec![IndexPath::default()], window, cx) - .searchable(true) + .searchable(true) }); let disabled_items = cx.new(|cx| { @@ -387,30 +387,26 @@ impl ComboboxStory { FoodItem::new("Carrots"), FoodItem::new("Broccoli").disabled(), ]); - ComboboxState::new(items, vec![], window, cx) - .searchable(true) + ComboboxState::new(items, vec![], window, cx).searchable(true) }); - let with_icon = cx.new(|cx| { - ComboboxState::new(industries(), vec![], window, cx) - .searchable(true) - }); + let with_icon = + cx.new(|cx| ComboboxState::new(industries(), vec![], window, cx).searchable(true)); let custom_check = cx.new(|cx| { ComboboxState::new(SearchableVec::new(FRAMEWORKS.to_vec()), vec![], window, cx) - .searchable(true) + .searchable(true) }); let with_footer = cx.new(|cx| { let items = SearchableVec::new(vec!["Harvard University", "MIT", "Stanford", "Cambridge"]); - ComboboxState::new(items, vec![IndexPath::default()], window, cx) - .searchable(true) + ComboboxState::new(items, vec![IndexPath::default()], window, cx).searchable(true) }); let custom_trigger = cx.new(|cx| { ComboboxState::new(SearchableVec::new(FRAMEWORKS.to_vec()), vec![], window, cx) - .searchable(true) + .searchable(true) }); let multi_badges = cx.new(|cx| { @@ -442,7 +438,7 @@ impl ComboboxStory { window, cx, ) - .searchable(true) + .searchable(true) }); let featured = cx.new(|cx| { @@ -452,7 +448,7 @@ impl ComboboxStory { window, cx, ) - .searchable(true) + .searchable(true) }); let multi_expand = cx.new(|cx| { diff --git a/crates/story/src/stories/dialog_story.rs b/crates/story/src/stories/dialog_story.rs index f4a73d8546..f0188d8eff 100644 --- a/crates/story/src/stories/dialog_story.rs +++ b/crates/story/src/stories/dialog_story.rs @@ -110,7 +110,11 @@ impl DialogStory { let date = cx.new(|cx| DatePickerState::new(window, cx)); let select = cx.new(|cx| { SelectState::new( - vec!["Option 1".to_string(), "Option 2".to_string(), "Option 3".to_string()], + vec![ + "Option 1".to_string(), + "Option 2".to_string(), + "Option 3".to_string(), + ], None, window, cx, @@ -239,20 +243,23 @@ impl DialogStory { } fn render_focus_back_test(&self, _cx: &mut Context) -> impl IntoElement { - section("Focus back test").max_w_md().child(Input::new(&self.input2)).child( - Button::new("test-action") - .outline() - .label("Test Action") - .flex_shrink_0() - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(TestAction), cx); - }) - .tooltip( - "This button for test dispatch action, \ + section("Focus back test") + .max_w_md() + .child(Input::new(&self.input2)) + .child( + Button::new("test-action") + .outline() + .label("Test Action") + .flex_shrink_0() + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(TestAction), cx); + }) + .tooltip( + "This button for test dispatch action, \ to make sure when Dialog close,\ \nthis still can handle the action.", - ), - ) + ), + ) } fn render_dialog_without_title(&self, cx: &mut Context) -> impl IntoElement { @@ -260,16 +267,20 @@ impl DialogStory { let overlay_closable = self.overlay_closable; section("Dialog without Title").child( - Button::new("dialog-no-title").outline().label("Dialog without Title").on_click( - cx.listener(move |_, _, window, cx| { + Button::new("dialog-no-title") + .outline() + .label("Dialog without Title") + .on_click(cx.listener(move |_, _, window, cx| { window.open_dialog(cx, move |dialog, _, _| { - dialog.overlay(dialog_overlay).overlay_closable(overlay_closable).child( - "This is a dialog without title, \ + dialog + .overlay(dialog_overlay) + .overlay_closable(overlay_closable) + .child( + "This is a dialog without title, \ you can use it when the title is not necessary.", - ) + ) }); - }), - ), + })), ) } @@ -278,8 +289,10 @@ impl DialogStory { let overlay_closable = self.overlay_closable; section("Custom buttons").child( - Button::new("confirm-dialog1").outline().label("Custom Buttons").on_click(cx.listener( - move |_, _, window, cx| { + Button::new("confirm-dialog1") + .outline() + .label("Custom Buttons") + .on_click(cx.listener(move |_, _, window, cx| { window.open_dialog(cx, move |dialog, _, cx| { dialog .rounded(cx.theme().radius_lg) @@ -326,8 +339,7 @@ impl DialogStory { true }) }); - }, - )), + })), ) } @@ -336,8 +348,10 @@ impl DialogStory { let overlay_closable = self.overlay_closable; section("Scrollable Dialog").child( - Button::new("scrollable-dialog").outline().label("Scrollable Dialog").on_click( - cx.listener(move |_, _, window, cx| { + Button::new("scrollable-dialog") + .outline() + .label("Scrollable Dialog") + .on_click(cx.listener(move |_, _, window, cx| { window.open_dialog(cx, move |dialog, _, _| { dialog .w(px(720.)) @@ -360,8 +374,7 @@ impl DialogStory { ), ) }); - }), - ), + })), ) } @@ -370,35 +383,40 @@ impl DialogStory { let overlay_closable = self.overlay_closable; section("Table in Dialog").child( - Button::new("table-dialog").outline().label("Table Dialog").on_click(cx.listener({ - move |this, _, window, cx| { - window.open_dialog(cx, { - let table = this.table.clone(); - move |dialog, _, _| { - dialog - .w(px(800.)) - .h(px(600.)) - .overlay(dialog_overlay) - .overlay_closable(overlay_closable) - .title("Dialog with Table") - .child( - v_flex() - .size_full() - .gap_3() - .child("This is a dialog contains a table component.") - .child(DataTable::new(&table)), - ) - } - }); - } - })), + Button::new("table-dialog") + .outline() + .label("Table Dialog") + .on_click(cx.listener({ + move |this, _, window, cx| { + window.open_dialog(cx, { + let table = this.table.clone(); + move |dialog, _, _| { + dialog + .w(px(800.)) + .h(px(600.)) + .overlay(dialog_overlay) + .overlay_closable(overlay_closable) + .title("Dialog with Table") + .child( + v_flex() + .size_full() + .gap_3() + .child("This is a dialog contains a table component.") + .child(DataTable::new(&table)), + ) + } + }); + } + })), ) } fn render_custom_paddings(&self, cx: &mut Context) -> impl IntoElement { section("Custom Paddings").child( - Button::new("custom-dialog-paddings").outline().label("Custom Paddings").on_click( - cx.listener(move |_, _, window, cx| { + Button::new("custom-dialog-paddings") + .outline() + .label("Custom Paddings") + .on_click(cx.listener(move |_, _, window, cx| { window.open_dialog(cx, move |dialog, _, _| { dialog.p_3().title("Custom Dialog Title").child( "This is a custom dialog content, we can use \ @@ -406,15 +424,16 @@ impl DialogStory { the dialog.", ) }); - }), - ), + })), ) } fn render_custom_style(&self, cx: &mut Context) -> impl IntoElement { section("Custom Style").child( - Button::new("custom-dialog-style").outline().label("Custom Dialog Style").on_click( - cx.listener(move |_, _, window, cx| { + Button::new("custom-dialog-style") + .outline() + .label("Custom Dialog Style") + .on_click(cx.listener(move |_, _, window, cx| { window.open_dialog(cx, move |dialog, _, cx| { dialog .rounded(cx.theme().radius_lg) @@ -423,58 +442,60 @@ impl DialogStory { .title("Custom Dialog Title") .child("This is a custom dialog content.") }); - }), - ), + })), ) } fn render_dialog_with_content(&self, cx: &mut Context) -> impl IntoElement { - section("Open Dialog with DialogContent").sub_title("Declarative API").child( - Button::new("custom-width-dialog-btn") - .outline() - .label("Custom Width (400px)") - .on_click(cx.listener(move |_, _, window, cx| { - window.open_dialog(cx, move |dialog, _, _| { - dialog.w(px(400.)).content(|content, _, _| { - content - .child( - DialogHeader::new() - .child(DialogTitle::new().child("Custom Width")) - .child( - DialogDescription::new() - .child("This dialog has a custom width of 400px."), - ), - ) - .child( - "Content area with custom width configuration, \ + section("Open Dialog with DialogContent") + .sub_title("Declarative API") + .child( + Button::new("custom-width-dialog-btn") + .outline() + .label("Custom Width (400px)") + .on_click(cx.listener(move |_, _, window, cx| { + window.open_dialog(cx, move |dialog, _, _| { + dialog.w(px(400.)).content(|content, _, _| { + content + .child( + DialogHeader::new() + .child(DialogTitle::new().child("Custom Width")) + .child( + DialogDescription::new().child( + "This dialog has a custom width of 400px.", + ), + ), + ) + .child( + "Content area with custom width configuration, \ and the footer is used flex 1 button widths.", - ) - .child( - DialogFooter::new() - .justify_center() - .child( - Button::new("cancel") - .flex_1() - .outline() - .label("Cancel") - .on_click(|_, window, cx| { - window.close_dialog(cx); - }), - ) - .child( - Button::new("done") - .flex_1() - .primary() - .label("Done") - .on_click(|_, window, cx| { - window.close_dialog(cx); - }), - ), - ) + ) + .child( + DialogFooter::new() + .justify_center() + .child( + Button::new("cancel") + .flex_1() + .outline() + .label("Cancel") + .on_click(|_, window, cx| { + window.close_dialog(cx); + }), + ) + .child( + Button::new("done") + .flex_1() + .primary() + .label("Done") + .on_click(|_, window, cx| { + window.close_dialog(cx); + }), + ), + ) + }) }) - }) - })), - ) + })), + ) } } diff --git a/crates/story/src/stories/dropdown_button_story.rs b/crates/story/src/stories/dropdown_button_story.rs index f5d2589c62..d30501ca1b 100644 --- a/crates/story/src/stories/dropdown_button_story.rs +++ b/crates/story/src/stories/dropdown_button_story.rs @@ -1,5 +1,5 @@ -use gpui::{ Anchor, - Action, App, AppContext as _, Context, Entity, Focusable, IntoElement, +use gpui::{ + Action, Anchor, App, AppContext as _, Context, Entity, Focusable, IntoElement, ParentElement as _, Render, Styled as _, Window, prelude::FluentBuilder as _, }; use serde::Deserialize; diff --git a/crates/story/src/stories/editor_story.rs b/crates/story/src/stories/editor_story.rs index e12054e81b..cfbb71cc64 100644 --- a/crates/story/src/stories/editor_story.rs +++ b/crates/story/src/stories/editor_story.rs @@ -1,6 +1,6 @@ use gpui::{App, AppContext as _, Context, Entity, IntoElement, Render, Styled, Window}; -use gpui_component::{input::*, ActiveTheme}; +use gpui_component::{ActiveTheme, input::*}; const EXAMPLE_CODE: &str = include_str!("./editor_story.rs"); diff --git a/crates/story/src/stories/hover_card_story.rs b/crates/story/src/stories/hover_card_story.rs index 3172b544ca..71904c9209 100644 --- a/crates/story/src/stories/hover_card_story.rs +++ b/crates/story/src/stories/hover_card_story.rs @@ -1,10 +1,9 @@ -use gpui::{ Anchor, - App, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _, - Window, div, px, relative, +use gpui::{ + Anchor, App, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, + Styled as _, Window, div, px, relative, }; use gpui_component::{ - ActiveTheme, StyledExt, avatar::Avatar, button::Button, h_flex, hover_card::HoverCard, - v_flex, + ActiveTheme, StyledExt, avatar::Avatar, button::Button, h_flex, hover_card::HoverCard, v_flex, }; use std::time::Duration; diff --git a/crates/story/src/stories/icon_story.rs b/crates/story/src/stories/icon_story.rs index a8671b0efd..dca9be3467 100644 --- a/crates/story/src/stories/icon_story.rs +++ b/crates/story/src/stories/icon_story.rs @@ -3,9 +3,10 @@ use gpui::{ Styled, Window, }; use gpui_component::{ + ActiveTheme as _, Icon, IconName, Sizable, button::{Button, ButtonVariant, ButtonVariants}, dock::PanelControl, - h_flex, neutral_500, v_flex, ActiveTheme as _, Icon, IconName, Sizable, + h_flex, neutral_500, v_flex, }; use crate::section; diff --git a/crates/story/src/stories/otp_input_story.rs b/crates/story/src/stories/otp_input_story.rs index 9003d2f607..65c7bf11ec 100644 --- a/crates/story/src/stories/otp_input_story.rs +++ b/crates/story/src/stories/otp_input_story.rs @@ -1,13 +1,14 @@ use gpui::{ - prelude::FluentBuilder as _, px, App, AppContext as _, Context, Entity, Focusable, - InteractiveElement, IntoElement, ParentElement as _, Render, SharedString, Styled, - Subscription, Window, + App, AppContext as _, Context, Entity, Focusable, InteractiveElement, IntoElement, + ParentElement as _, Render, SharedString, Styled, Subscription, Window, + prelude::FluentBuilder as _, px, }; use gpui_component::{ + Disableable as _, Sizable, StyledExt, checkbox::Checkbox, h_flex, input::{InputEvent, OtpInput, OtpState}, - v_flex, Disableable as _, Sizable, StyledExt, + v_flex, }; use crate::section; diff --git a/crates/story/src/stories/sheet_story.rs b/crates/story/src/stories/sheet_story.rs index dfe7e5475d..410123cdd3 100644 --- a/crates/story/src/stories/sheet_story.rs +++ b/crates/story/src/stories/sheet_story.rs @@ -46,7 +46,9 @@ impl ListDelegate for ListItemDeletegate { cx.spawn(async move |this, cx| { // Simulate a slow search. let sleep = (0.05..0.1).fake(); - cx.background_executor().timer(Duration::from_secs_f64(sleep)).await; + cx.background_executor() + .timer(Duration::from_secs_f64(sleep)) + .await; this.update(cx, |this, cx| { this.delegate_mut().matches = this @@ -74,7 +76,12 @@ impl ListDelegate for ListItemDeletegate { let list_item = ListItem::new(("item", ix.row)) .check_icon(IconName::Check) .confirmed(confirmed) - .child(h_flex().items_center().justify_between().child(item.to_string())) + .child( + h_flex() + .items_center() + .justify_between() + .child(item.to_string()), + ) .suffix(|_, _| { Button::new("like") .tab_stop(false) @@ -101,7 +108,11 @@ impl ListDelegate for ListItemDeletegate { ) -> impl IntoElement { v_flex() .size_full() - .child(Icon::new(IconName::Inbox).size(px(50.)).text_color(cx.theme().muted_foreground)) + .child( + Icon::new(IconName::Inbox) + .size(px(50.)) + .text_color(cx.theme().muted_foreground), + ) .child("No matches found") .items_center() .justify_center() @@ -280,12 +291,12 @@ impl SheetStory { .child(Input::new(&input1)) .child(DatePicker::new(&date).placeholder("Date of Birth")) .child( - Button::new("send-notification").child("Test Notification").on_click( - |_, window, cx| { + Button::new("send-notification") + .child("Test Notification") + .on_click(|_, window, cx| { window .push_notification("Hello this is message from Sheet.", cx) - }, - ), + }), ) .child( Button::new("confirm-dialog-from-sheet") @@ -325,9 +336,13 @@ impl SheetStory { window.close_sheet(cx); }, )) - .child(Button::new("cancel").label("Cancel").on_click(|_, window, cx| { - window.close_sheet(cx); - })), + .child( + Button::new("cancel") + .label("Cancel") + .on_click(|_, window, cx| { + window.close_sheet(cx); + }), + ), ) }); } @@ -461,7 +476,9 @@ impl Render for SheetStory { .when_some(self.selected_value.clone(), |this, selected_value| { this.child( h_flex().gap_1().child("You have selected:").child( - div().child(selected_value.to_string()).text_color(gpui::red()), + div() + .child(selected_value.to_string()) + .text_color(gpui::red()), ), ) }), diff --git a/crates/story/src/stories/theme_story/mod.rs b/crates/story/src/stories/theme_story/mod.rs index dff820ef25..9ddf1b3f76 100644 --- a/crates/story/src/stories/theme_story/mod.rs +++ b/crates/story/src/stories/theme_story/mod.rs @@ -2,4 +2,4 @@ mod checkerboard; mod color_theme_story; mod mapper; -pub use color_theme_story::*; \ No newline at end of file +pub use color_theme_story::*; diff --git a/crates/story/src/title_bar.rs b/crates/story/src/title_bar.rs index 4777b06874..21b218625a 100644 --- a/crates/story/src/title_bar.rs +++ b/crates/story/src/title_bar.rs @@ -14,7 +14,10 @@ use gpui_component::{ scroll::ScrollbarShow, }; -use crate::{SelectFont, SelectRadius, SelectScrollbarShow, ToggleListActiveHighlight, app_menus}; +use crate::{ + SelectFont, SelectRadius, SelectScrollbarShow, ToggleListActiveHighlight, ToggleWindowGlass, + app_menus, +}; pub struct AppTitleBar { app_menu_bar: Entity, @@ -152,14 +155,28 @@ impl FontSizeSelector { theme.list.active_highlight = !theme.list.active_highlight; window.refresh(); } + + fn on_toggle_window_glass( + &mut self, + _: &ToggleWindowGlass, + window: &mut Window, + cx: &mut Context, + ) { + if window.is_window_glass_enabled(cx) { + window.set_window_glass(false, cx); + } else if !window.set_window_glass(true, cx) { + window.push_notification("Window glass requires macOS 26+ or Windows 11 22H2+.", cx); + } + } } impl Render for FontSizeSelector { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle.clone(); let font_size = cx.theme().font_size.as_f32() as i32; let radius = cx.theme().radius.as_f32() as i32; let scroll_show = cx.theme().scrollbar_show; + let window_glass = window.is_window_glass_enabled(cx); div() .id("font-size-selector") @@ -168,6 +185,7 @@ impl Render for FontSizeSelector { .on_action(cx.listener(Self::on_select_radius)) .on_action(cx.listener(Self::on_select_scrollbar_show)) .on_action(cx.listener(Self::on_toggle_list_active_highlight)) + .on_action(cx.listener(Self::on_toggle_window_glass)) .child( Button::new("btn") .small() @@ -218,6 +236,12 @@ impl Render for FontSizeSelector { cx.theme().list.active_highlight, Box::new(ToggleListActiveHighlight), ) + .separator() + .menu_with_check( + "Window Glass", + window_glass, + Box::new(ToggleWindowGlass), + ) }) .anchor(Anchor::TopRight), ) diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index ac5ce71985..2220347253 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -185,6 +185,12 @@ tree-sitter-zig = { version = "1.1.2", optional = true } [target.'cfg(target_os = "macos")'.dependencies] core-text = "=21.0.0" +objc2 = "0.6" +objc2-foundation = "0.3" +raw-window-handle = "0.6" + +[target.'cfg(target_os = "windows")'.dependencies] +windows-version = "0.1" [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/ui/src/global_state.rs b/crates/ui/src/global_state.rs index d800d12d98..167b95a2b2 100644 --- a/crates/ui/src/global_state.rs +++ b/crates/ui/src/global_state.rs @@ -1,4 +1,4 @@ -use gpui::{App, ElementId, Entity, FocusHandle, Global, OwnedMenu}; +use gpui::{App, ElementId, Entity, FocusHandle, Global, OwnedMenu, Window, WindowId}; use std::collections::HashSet; use crate::text::TextViewState; @@ -22,6 +22,9 @@ pub struct GlobalState { /// interaction (e.g. `Input`, `Button`); reset by the selection /// controller in the capture phase of every left mouse down. pub(crate) suppress_text_selection: bool, + /// Windows with the glass background effect enabled, + /// see [`crate::WindowExt::set_window_glass`]. + pub(crate) glass_windows: HashSet, } impl GlobalState { @@ -31,9 +34,16 @@ impl GlobalState { open_deferred_popovers: HashSet::new(), app_menus: Vec::new(), suppress_text_selection: false, + glass_windows: HashSet::new(), } } + /// Returns true if the glass background effect is enabled for the window. + pub(crate) fn is_window_glass_enabled(&self, window: &Window) -> bool { + self.glass_windows + .contains(&window.window_handle().window_id()) + } + /// Suppress the window-level text selection for the current mouse down. /// /// Call this from a mouse-down handler (bubble phase) of a component that diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 47d8cf74c7..d127fc37ed 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -18,6 +18,7 @@ mod title_bar; mod virtual_list; mod window_border; mod window_ext; +mod window_glass; pub(crate) mod actions; diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index 3bab9e3c67..4a28053764 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -2,6 +2,7 @@ use crate::{ ActiveTheme, ElementExt, Placement, StyledExt, dialog::{ANIMATION_DURATION, Dialog}, focus_trap::FocusTrapManager, + global_state::GlobalState, input::{Copy, InputState}, notification::{Notification, NotificationList}, sheet::Sheet, @@ -11,15 +12,16 @@ use crate::{ }; use gpui::{ Anchor, AnyView, App, AppContext, ClipboardItem, Context, DefiniteLength, ElementId, Entity, - EntityId, FocusHandle, Hitbox, InteractiveElement, IntoElement, KeyBinding, - ParentElement as _, Pixels, Render, StyleRefinement, Styled, WeakEntity, WeakFocusHandle, - Window, actions, div, prelude::FluentBuilder as _, + EntityId, FocusHandle, Hitbox, InteractiveElement, IntoElement, KeyBinding, ParentElement as _, + Pixels, Render, StyleRefinement, Styled, WeakEntity, WeakFocusHandle, Window, actions, div, + prelude::FluentBuilder as _, }; use std::{any::TypeId, collections::HashMap, rc::Rc}; actions!(root, [Tab, TabPrev]); const CONTEXT: &str = "Root"; + pub(crate) fn init(cx: &mut App) { cx.bind_keys([ KeyBinding::new("tab", Tab, Some(CONTEXT)), @@ -513,6 +515,16 @@ impl Render for Root { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { window.set_rem_size(cx.theme().font_size); + // In glass mode the window background is transparent, so the glass is + // revealed through the navigation areas (sidebar / title bar / tab + // bar), while content areas keep their own opaque surfaces and cover + // it. Otherwise it uses the opaque theme background. + let background = if GlobalState::global(cx).is_window_glass_enabled(window) { + gpui::transparent_black() + } else { + cx.theme().background + }; + window_border().shadow_size(self.window_shadow_size).child( div() .id("root") @@ -523,7 +535,7 @@ impl Render for Root { .relative() .size_full() .font_family(cx.theme().font_family.clone()) - .bg(cx.theme().background) + .bg(background) .text_color(cx.theme().foreground) .refine_style(&self.style) .child(TextSelectionController) diff --git a/crates/ui/src/sidebar/menu.rs b/crates/ui/src/sidebar/menu.rs index 8fd72b0ab6..b0bcba7616 100644 --- a/crates/ui/src/sidebar/menu.rs +++ b/crates/ui/src/sidebar/menu.rs @@ -1,5 +1,5 @@ use crate::{ - ActiveTheme as _, Collapsible, Icon, IconName, Sizable as _, StyledExt, + ActiveTheme as _, Collapsible, Colorize as _, Icon, IconName, Sizable as _, StyledExt, button::{Button, ButtonVariants as _}, h_flex, menu::{ContextMenuExt, PopupMenu}, @@ -269,7 +269,15 @@ impl SidebarItem for SidebarMenuItem { .text_sm() .when(is_hoverable, |this| { this.hover(|this| { - this.bg(cx.theme().sidebar_accent.opacity(0.8)) + // Opaque hover, lighter than the active item: mix + // toward the sidebar for the hue, then force full + // alpha. In glass mode `sidebar` is translucent, so + // the mix would otherwise stay semi-transparent and + // blend with the glass to read darker than active. + let mut hover_bg = + cx.theme().sidebar_accent.mix(cx.theme().sidebar, 0.8); + hover_bg.a = 1.0; + this.bg(hover_bg) .text_color(cx.theme().sidebar_accent_foreground) }) }) diff --git a/crates/ui/src/theme/mod.rs b/crates/ui/src/theme/mod.rs index 82b012d713..fdca31f97b 100644 --- a/crates/ui/src/theme/mod.rs +++ b/crates/ui/src/theme/mod.rs @@ -84,6 +84,13 @@ pub struct Theme { pub list: ListSettings, /// The sheet settings. pub sheet: SheetSettings, + /// Whether the system glass background effect is enabled for any window. + /// + /// Mirrored from [`crate::WindowExt::set_window_glass`] so that every theme + /// change (mode switch or full theme swap) reapplies the glass surface + /// adjustments via [`ThemeColor::apply_window_glass`]. + #[serde(skip)] + pub(crate) window_glass: bool, } impl Default for Theme { @@ -174,6 +181,8 @@ impl Theme { } else { theme.apply_config(&theme.light_theme.clone()); } + // `apply_config` reapplies the glass adjustments when `window_glass` + // is set, so no extra handling is needed here. if let Some(window) = window { window.refresh(); @@ -233,6 +242,7 @@ impl From<&ThemeColor> for Theme { dark_theme: Rc::new(ThemeConfig::default()), highlight_theme: HighlightTheme::default_light(), sheet: SheetSettings::default(), + window_glass: false, } } } diff --git a/crates/ui/src/theme/schema.rs b/crates/ui/src/theme/schema.rs index c936be8740..7a1f8c73b5 100644 --- a/crates/ui/src/theme/schema.rs +++ b/crates/ui/src/theme/schema.rs @@ -711,5 +711,11 @@ impl Theme { self.colors.apply_config(&config, &default_colors); self.mode = config.mode; + + // `apply_config` just rebuilt the colors, so reapply the window glass + // surface adjustments to keep glass active across theme switches. + if self.window_glass { + self.colors.apply_window_glass(); + } } } diff --git a/crates/ui/src/theme/theme_color.rs b/crates/ui/src/theme/theme_color.rs index 9f7c1b79db..01d374c897 100644 --- a/crates/ui/src/theme/theme_color.rs +++ b/crates/ui/src/theme/theme_color.rs @@ -254,4 +254,36 @@ impl ThemeColor { pub fn dark() -> Arc { DEFAULT_THEME_COLORS[&ThemeMode::Dark].0.clone() } + + /// Make the surface colors semi-transparent to let the window glass + /// background show through, + /// see [`crate::WindowExt::set_window_glass`]. + pub(crate) fn apply_window_glass(&mut self) { + /// Opacity for the navigation surfaces that reveal the glass. + /// + /// Note: GPUI has no per-element backdrop blur, so a rounded element on + /// top of a translucent navigation surface has its anti-aliased corner + /// edge bleed the glass through (a faint jaggy). The lower this value, + /// the stronger that bleed — keep it fairly high so the glass tint + /// still reads while the corner artifact stays negligible. + const NAVIGATION_OPACITY: f32 = 0.5; + + // Only the navigation surfaces (sidebar, title bar, tab bar) are made + // translucent to reveal the glass behind them. The content background + // and all controls / cards (secondary, list, table, group_box, popover, + // dialog, …) stay opaque, so content stays readable and never bleeds + // through (ghosting), and their colors keep full alpha when recomputed + // downstream (e.g. `.opacity()`, `.lighten()`, `.alpha()`). The `Root` + // view paints a transparent window background in glass mode so the + // glass shows through these navigation areas, while the content area + // covers it with its opaque background. + let navigation_surfaces = [ + &mut self.title_bar, + &mut self.sidebar, + &mut self.tab_bar, + ]; + for color in navigation_surfaces { + color.a *= NAVIGATION_OPACITY; + } + } } diff --git a/crates/ui/src/window_ext.rs b/crates/ui/src/window_ext.rs index ab19ffdb23..19d7f009d7 100644 --- a/crates/ui/src/window_ext.rs +++ b/crates/ui/src/window_ext.rs @@ -1,9 +1,11 @@ use crate::{ - Placement, Root, + Placement, Root, Theme, dialog::{AlertDialog, Dialog}, + global_state::GlobalState, input::InputState, notification::Notification, sheet::Sheet, + window_glass, }; use gpui::{App, ElementId, Entity, Window}; use std::rc::Rc; @@ -95,6 +97,43 @@ pub trait WindowExt: Sized { /// Clears the window-level text selection and all view-local selections. fn clear_text_selection(&mut self, cx: &mut App); + + /// Enables or disables the system glass effect for the window background. + /// + /// When `enable` is `false`, the opaque background is restored; this is a + /// no-op if the effect is not currently enabled. + /// + /// When enabling: + /// + /// - macOS 26 (Tahoe) or later: Liquid Glass, by embedding a native + /// `NSGlassEffectView` behind the window content. + /// - Windows 11 22H2 (build 22621) or later: Mica backdrop. + /// - Other platforms (older systems, Linux): no-op that returns `false`, + /// the window stays opaque. + /// + /// When enabled, the large surface colors of the theme (e.g. `background`, + /// `title_bar`, `sidebar`) are automatically made semi-transparent to let + /// the glass show through, this applies to all windows of the application. + /// + /// Returns `true` if the requested state was applied successfully. + /// Disabling always returns `true`. + /// + /// # Examples + /// + /// ```ignore + /// let window = cx.open_window(options, |window, cx| { + /// let view = cx.new(|_| Example); + /// cx.new(|cx| Root::new(view, window, cx)) + /// })?; + /// + /// window.update(cx, |_, window, cx| { + /// window.set_window_glass(true, cx); + /// })?; + /// ``` + fn set_window_glass(&mut self, enable: bool, cx: &mut App) -> bool; + + /// Returns true if the system glass effect is enabled for the window. + fn is_window_glass_enabled(&self, cx: &App) -> bool; } impl WindowExt for Window { @@ -239,4 +278,37 @@ impl WindowExt for Window { }; root.update(cx, |root, cx| root.clear_text_selection(cx)); } + + fn set_window_glass(&mut self, enable: bool, cx: &mut App) -> bool { + let window_id = self.window_handle().window_id(); + if enable { + if GlobalState::global(cx).glass_windows.contains(&window_id) { + return true; + } + if !window_glass::enable(self) { + return false; + } + + GlobalState::global_mut(cx).glass_windows.insert(window_id); + } else { + if !GlobalState::global_mut(cx).glass_windows.remove(&window_id) { + return true; + } + + window_glass::disable(self); + } + + // Mirror the glass state onto the Theme so subsequent theme changes + // (mode switch or full theme swap via `apply_config`) reapply the + // glass surface adjustments. + Theme::global_mut(cx).window_glass = !GlobalState::global(cx).glass_windows.is_empty(); + // Reapply the theme to update the surface colors. + Theme::change(Theme::global(cx).mode, Some(self), cx); + true + } + + #[inline] + fn is_window_glass_enabled(&self, cx: &App) -> bool { + GlobalState::global(cx).is_window_glass_enabled(self) + } } diff --git a/crates/ui/src/window_glass/macos.rs b/crates/ui/src/window_glass/macos.rs new file mode 100644 index 0000000000..66933e8ff4 --- /dev/null +++ b/crates/ui/src/window_glass/macos.rs @@ -0,0 +1,98 @@ +//! Liquid Glass, embeds a native `NSGlassEffectView` (macOS 26+) behind the +//! window content. +//! +//! This mirrors how GPUI injects an `NSVisualEffectView` for +//! [`WindowBackgroundAppearance::Blurred`](gpui::WindowBackgroundAppearance): +//! the glass view is added to the window's `contentView`, positioned below +//! the GPUI rendered view, with an autoresizing mask to follow window resize. + +use gpui::{Window, WindowBackgroundAppearance}; +use objc2::ffi::{NSInteger, NSUInteger}; +use objc2::msg_send; +use objc2::rc::Retained; +use objc2::runtime::{AnyClass, AnyObject}; +use objc2_foundation::NSRect; +use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + +/// `NSViewWidthSizable | NSViewHeightSizable` +const AUTORESIZE_WIDTH_HEIGHT: NSUInteger = (1 << 1) | (1 << 4); +/// `NSWindowBelow` +const NS_WINDOW_BELOW: NSInteger = -1; + +/// Returns the `contentView` of the window's native `NSWindow`. +fn content_view(window: &Window) -> Option<*mut AnyObject> { + let handle = HasWindowHandle::window_handle(window).ok()?; + let RawWindowHandle::AppKit(handle) = handle.as_raw() else { + return None; + }; + + unsafe { + let ns_view: *mut AnyObject = handle.ns_view.as_ptr().cast(); + let ns_window: *mut AnyObject = msg_send![ns_view, window]; + if ns_window.is_null() { + return None; + } + let content_view: *mut AnyObject = msg_send![ns_window, contentView]; + if content_view.is_null() { + return None; + } + + Some(content_view) + } +} + +pub(crate) fn enable(window: &mut Window) -> bool { + // The class only exists on macOS 26 (Tahoe) and later, so a runtime + // lookup doubles as the OS version check. + let Some(glass_class) = AnyClass::get(c"NSGlassEffectView") else { + return false; + }; + let Some(content_view) = content_view(window) else { + return false; + }; + + // Let GPUI make the window non-opaque and its Metal layer + // transparent, so the glass below shows through. + window.set_background_appearance(WindowBackgroundAppearance::Transparent); + + unsafe { + let bounds: NSRect = msg_send![content_view, bounds]; + let glass: Retained = msg_send![glass_class, new]; + let _: () = msg_send![&*glass, setFrame: bounds]; + let _: () = msg_send![&*glass, setAutoresizingMask: AUTORESIZE_WIDTH_HEIGHT]; + let _: () = msg_send![ + content_view, + addSubview: &*glass, + positioned: NS_WINDOW_BELOW, + relativeTo: std::ptr::null_mut::() + ]; + } + + true +} + +/// Removes the injected glass views and restores the opaque background. +pub(crate) fn disable(window: &mut Window) { + let Some(glass_class) = AnyClass::get(c"NSGlassEffectView") else { + return; + }; + let Some(content_view) = content_view(window) else { + return; + }; + + unsafe { + // The `subviews` getter returns a copy of the array, so it is + // safe to remove views while iterating over it. + let subviews: Retained = msg_send![content_view, subviews]; + let count: usize = msg_send![&*subviews, count]; + for i in 0..count { + let view: *mut AnyObject = msg_send![&*subviews, objectAtIndex: i]; + let is_glass: bool = msg_send![view, isKindOfClass: glass_class]; + if is_glass { + let _: () = msg_send![view, removeFromSuperview]; + } + } + } + + window.set_background_appearance(WindowBackgroundAppearance::Opaque); +} diff --git a/crates/ui/src/window_glass/mod.rs b/crates/ui/src/window_glass/mod.rs new file mode 100644 index 0000000000..6a23baa9ce --- /dev/null +++ b/crates/ui/src/window_glass/mod.rs @@ -0,0 +1,26 @@ +//! System glass background effect for windows, see +//! [`crate::WindowExt::set_window_glass`]. +//! +//! Platform implementations live in submodules: +//! +//! - macOS 26+ : Liquid Glass via a native `NSGlassEffectView` ([`macos`]). +//! - Windows 11 22H2+ : Mica backdrop ([`windows`]). +//! - Other platforms (older systems, Linux): no-op fallback. + +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +pub(crate) use macos::{disable, enable}; + +#[cfg(target_os = "windows")] +mod windows; +#[cfg(target_os = "windows")] +pub(crate) use windows::{disable, enable}; + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +pub(crate) fn enable(_window: &mut gpui::Window) -> bool { + false +} + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +pub(crate) fn disable(_window: &mut gpui::Window) {} diff --git a/crates/ui/src/window_glass/windows.rs b/crates/ui/src/window_glass/windows.rs new file mode 100644 index 0000000000..c0d053ecac --- /dev/null +++ b/crates/ui/src/window_glass/windows.rs @@ -0,0 +1,21 @@ +//! Mica backdrop (Windows 11 22H2+), via GPUI's +//! [`WindowBackgroundAppearance::MicaBackdrop`](gpui::WindowBackgroundAppearance). + +use gpui::{Window, WindowBackgroundAppearance}; + +/// The Mica backdrop requires Windows 11 22H2 (build 22621) or later, +/// GPUI silently ignores it on older builds. +const MIN_BUILD_NUMBER: u32 = 22621; + +pub(crate) fn enable(window: &mut Window) -> bool { + if windows_version::OsVersion::current().build < MIN_BUILD_NUMBER { + return false; + } + + window.set_background_appearance(WindowBackgroundAppearance::MicaBackdrop); + true +} + +pub(crate) fn disable(window: &mut Window) { + window.set_background_appearance(WindowBackgroundAppearance::Opaque); +} diff --git a/docs/docs/root.md b/docs/docs/root.md index bfe792f4d1..f5689a7572 100644 --- a/docs/docs/root.md +++ b/docs/docs/root.md @@ -56,4 +56,29 @@ impl Render for MyApp { Here the example we used `children` method, it because if there is no opened dialogs, sheets, notifications, these methods will return `None`, so GPUI will not render anything. ::: +## Window Glass + +We can enable the system glass effect for the window background by calling `WindowExt::set_window_glass`: + +- macOS 26 (Tahoe) or later: Liquid Glass (a native `NSGlassEffectView` embedded behind the window content). +- Windows 11 22H2 or later: Mica backdrop. +- Other platforms (older systems, Linux): no-op that returns `false`, the window stays opaque. + +```rs +use gpui_component::WindowExt as _; + +let window = cx.open_window(options, |window, cx| { + let view = cx.new(|_| Example); + cx.new(|cx| Root::new(view, window, cx)) +})?; + +window.update(cx, |_, window, cx| { + window.set_window_glass(true, cx); +})?; +``` + +Following Apple's Liquid Glass guidance (glass belongs to the navigation layer, never the content), only the navigation-layer surfaces (`sidebar`, `title_bar`, `tab_bar`) are made semi-transparent to let the glass show through them. The content area stays opaque so content is readable and never bleeds through (ghosting). The window background itself becomes transparent in glass mode, so **your content areas must paint their own opaque background** (e.g. `bg(cx.theme().background)`) to cover the glass. This applies to all windows of the application. Call `window.set_window_glass(false, cx)` to restore the opaque background. + +The Story gallery has a working demo — toggle it from the title bar's window glass action. + [Root]: https://docs.rs/gpui-component/latest/gpui_component/root/struct.Root.html diff --git a/docs/zh-CN/docs/root.md b/docs/zh-CN/docs/root.md index 4b53e00895..2878cc0842 100644 --- a/docs/zh-CN/docs/root.md +++ b/docs/zh-CN/docs/root.md @@ -56,4 +56,29 @@ impl Render for MyApp { 这里使用的是 `children` 而不是 `child`,因为当没有打开的 dialog、sheet 或 notification 时,这些方法会返回 `None`,GPUI 就不会渲染任何内容。 ::: +## Window Glass(窗口玻璃效果) + +调用 `WindowExt::set_window_glass` 可以为窗口背景启用系统玻璃效果: + +- macOS 26(Tahoe)及以上:Liquid Glass(在窗口内容下方嵌入原生 `NSGlassEffectView`)。 +- Windows 11 22H2 及以上:Mica 背景材质。 +- 其它环境(更低版本系统、Linux):不做任何事并返回 `false`,窗口保持不透明背景。 + +```rs +use gpui_component::WindowExt as _; + +let window = cx.open_window(options, |window, cx| { + let view = cx.new(|_| Example); + cx.new(|cx| Root::new(view, window, cx)) +})?; + +window.update(cx, |_, window, cx| { + window.set_window_glass(true, cx); +})?; +``` + +遵循 Apple Liquid Glass 的原则(玻璃属于导航层,而非内容层),开启后仅导航层表面(`sidebar`、`title_bar`、`tab_bar`)会变为半透明以透出玻璃。内容区保持不透明,以保证内容清晰、不透出下方(重影)。玻璃模式下窗口背景本身会变为透明,因此**你的内容区容器必须自己画不透明背景**(如 `bg(cx.theme().background)`)来盖住玻璃。该行为对应用的所有窗口生效。调用 `window.set_window_glass(false, cx)` 可恢复不透明背景。 + +Story 画廊中有可运行的演示——从标题栏的 window glass 操作切换即可。 + [Root]: https://docs.rs/gpui-component/latest/gpui_component/root/struct.Root.html