feat: add native material backgrounds
This commit is contained in:
@@ -96,6 +96,7 @@ impl SidebarEntry {
|
|||||||
pub struct Sidebar {
|
pub struct Sidebar {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
selected: Option<usize>,
|
selected: Option<usize>,
|
||||||
|
transparent: bool,
|
||||||
a11y_label: Option<SharedString>,
|
a11y_label: Option<SharedString>,
|
||||||
on_change: Option<ChangeHandler>,
|
on_change: Option<ChangeHandler>,
|
||||||
entries: Option<EntriesFn>,
|
entries: Option<EntriesFn>,
|
||||||
@@ -110,6 +111,7 @@ impl Sidebar {
|
|||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
selected: None,
|
selected: None,
|
||||||
|
transparent: false,
|
||||||
a11y_label: None,
|
a11y_label: None,
|
||||||
on_change: None,
|
on_change: None,
|
||||||
entries: None,
|
entries: None,
|
||||||
@@ -122,6 +124,17 @@ impl Sidebar {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Paints translucent material fills so the window's native backdrop shows through.
|
||||||
|
pub fn transparent(mut self, transparent: bool) -> Self {
|
||||||
|
self.transparent = transparent;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether translucent material fills are painted.
|
||||||
|
pub fn is_transparent(&self) -> bool {
|
||||||
|
self.transparent
|
||||||
|
}
|
||||||
|
|
||||||
/// Reports the next selected row, from clicks and keys alike.
|
/// Reports the next selected row, from clicks and keys alike.
|
||||||
pub fn on_change<F>(mut self, handler: F) -> Self
|
pub fn on_change<F>(mut self, handler: F) -> Self
|
||||||
where
|
where
|
||||||
@@ -175,6 +188,7 @@ impl RenderOnce for Sidebar {
|
|||||||
.clone()
|
.clone()
|
||||||
});
|
});
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
|
let material = self.transparent;
|
||||||
|
|
||||||
let label = self
|
let label = self
|
||||||
.a11y_label
|
.a11y_label
|
||||||
@@ -195,6 +209,7 @@ impl RenderOnce for Sidebar {
|
|||||||
index,
|
index,
|
||||||
entries.len(),
|
entries.len(),
|
||||||
self.selected == Some(index),
|
self.selected == Some(index),
|
||||||
|
material,
|
||||||
&self.on_change,
|
&self.on_change,
|
||||||
&focus,
|
&focus,
|
||||||
)
|
)
|
||||||
@@ -207,6 +222,7 @@ impl RenderOnce for Sidebar {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
|
.when(material, |sidebar| sidebar.bg(theme.material.background))
|
||||||
.text_size(theme.sizes.medium.text_size)
|
.text_size(theme.sizes.medium.text_size)
|
||||||
.track_focus(&focus)
|
.track_focus(&focus)
|
||||||
.focusable()
|
.focusable()
|
||||||
@@ -237,16 +253,19 @@ impl RenderOnce for Sidebar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_entry(
|
fn render_entry(
|
||||||
cx: &App,
|
cx: &App,
|
||||||
entry: &SidebarEntry,
|
entry: &SidebarEntry,
|
||||||
index: usize,
|
index: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
material: bool,
|
||||||
on_change: &Option<ChangeHandler>,
|
on_change: &Option<ChangeHandler>,
|
||||||
focus: &FocusHandle,
|
focus: &FocusHandle,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
|
|
||||||
match entry {
|
match entry {
|
||||||
SidebarEntry::Header { label } => div()
|
SidebarEntry::Header { label } => div()
|
||||||
.px_2()
|
.px_2()
|
||||||
@@ -277,7 +296,17 @@ fn render_entry(
|
|||||||
.aria_selected(selected)
|
.aria_selected(selected)
|
||||||
.aria_position_in_set(index + 1)
|
.aria_position_in_set(index + 1)
|
||||||
.aria_size_of_set(count)
|
.aria_size_of_set(count)
|
||||||
.map(|row| paint_selectable_row(cx, row, selected, *row_disabled));
|
.map(|row| {
|
||||||
|
if material && !*row_disabled {
|
||||||
|
if selected {
|
||||||
|
row.bg(theme.material.selection)
|
||||||
|
} else {
|
||||||
|
row.hover(|row| row.bg(theme.material.hover))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
paint_selectable_row(cx, row, selected, *row_disabled)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if !*row_disabled {
|
if !*row_disabled {
|
||||||
row = row.cursor_pointer().on_click({
|
row = row.cursor_pointer().on_click({
|
||||||
|
|||||||
@@ -1078,8 +1078,10 @@ fn render_pane(
|
|||||||
.w_full()
|
.w_full()
|
||||||
.h(titlebar.height)
|
.h(titlebar.height)
|
||||||
.items_center()
|
.items_center()
|
||||||
.when(titlebar.traffic_light_pad, |bar| {
|
.pl(if titlebar.traffic_light_pad {
|
||||||
bar.pl(px(TRAFFIC_LIGHT_PAD))
|
px(TRAFFIC_LIGHT_PAD)
|
||||||
|
} else {
|
||||||
|
padding_x
|
||||||
})
|
})
|
||||||
.pr(padding_x)
|
.pr(padding_x)
|
||||||
.child(tabs)
|
.child(tabs)
|
||||||
|
|||||||
@@ -178,9 +178,11 @@ impl RenderOnce for Shell {
|
|||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let metrics = theme.sizes.resolve(Size::default());
|
let metrics = theme.sizes.resolve(Size::default());
|
||||||
|
let background = theme.colors.background;
|
||||||
|
let titlebar_height = theme.titlebar_height;
|
||||||
|
|
||||||
let state = self.state;
|
|
||||||
let id = self.id.clone();
|
let id = self.id.clone();
|
||||||
|
let state = self.state;
|
||||||
|
|
||||||
let mut left = self.left_sidebar;
|
let mut left = self.left_sidebar;
|
||||||
let mut right = self.right_sidebar;
|
let mut right = self.right_sidebar;
|
||||||
@@ -188,6 +190,9 @@ impl RenderOnce for Shell {
|
|||||||
|
|
||||||
let left_visible = state.read(cx).is_left_sidebar_visible();
|
let left_visible = state.read(cx).is_left_sidebar_visible();
|
||||||
let right_visible = state.read(cx).is_right_sidebar_visible();
|
let right_visible = state.read(cx).is_right_sidebar_visible();
|
||||||
|
let left_material = left
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|sidebar| sidebar.is_transparent());
|
||||||
|
|
||||||
let layout = div()
|
let layout = div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -204,8 +209,9 @@ impl RenderOnce for Shell {
|
|||||||
.map(|sidebar| sidebar.into_any_element())
|
.map(|sidebar| sidebar.into_any_element())
|
||||||
.unwrap_or_else(|| div().into_any_element()),
|
.unwrap_or_else(|| div().into_any_element()),
|
||||||
Side::Left,
|
Side::Left,
|
||||||
|
left_material,
|
||||||
&state,
|
&state,
|
||||||
metrics.strip_height,
|
titlebar_height,
|
||||||
metrics.dense_padding_x,
|
metrics.dense_padding_x,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@@ -223,6 +229,7 @@ impl RenderOnce for Shell {
|
|||||||
.h_full()
|
.h_full()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
|
.bg(background)
|
||||||
.child(center.take().unwrap_or_else(|| div().into_any_element())),
|
.child(center.take().unwrap_or_else(|| div().into_any_element())),
|
||||||
)
|
)
|
||||||
.when(right_visible && right.is_some(), |layout| {
|
.when(right_visible && right.is_some(), |layout| {
|
||||||
@@ -238,8 +245,9 @@ impl RenderOnce for Shell {
|
|||||||
width,
|
width,
|
||||||
right.take().unwrap_or_else(|| div().into_any_element()),
|
right.take().unwrap_or_else(|| div().into_any_element()),
|
||||||
Side::Right,
|
Side::Right,
|
||||||
|
false,
|
||||||
&state,
|
&state,
|
||||||
metrics.strip_height,
|
titlebar_height,
|
||||||
metrics.dense_padding_x,
|
metrics.dense_padding_x,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@@ -257,17 +265,26 @@ fn sidebar_region(
|
|||||||
width: Pixels,
|
width: Pixels,
|
||||||
content: AnyElement,
|
content: AnyElement,
|
||||||
side: Side,
|
side: Side,
|
||||||
|
material: bool,
|
||||||
state: &Entity<LayoutState>,
|
state: &Entity<LayoutState>,
|
||||||
strip_height: Pixels,
|
strip_height: Pixels,
|
||||||
padding_x: Pixels,
|
padding_x: Pixels,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
let theme = cx.theme();
|
||||||
|
let background = theme.colors.background;
|
||||||
|
let strip_background = match (side, material) {
|
||||||
|
(Side::Left, true) => theme.material.background,
|
||||||
|
_ => theme.colors.background,
|
||||||
|
};
|
||||||
|
|
||||||
let strip = h_flex()
|
let strip = h_flex()
|
||||||
.id(SharedString::from(format!("sidebar-strip-{}", side.name())))
|
.id(SharedString::from(format!("sidebar-strip-{}", side.name())))
|
||||||
.debug_selector(move || format!("sidebar-strip-{}", side.name()))
|
.debug_selector(move || format!("sidebar-strip-{}", side.name()))
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.h(strip_height)
|
.h(strip_height)
|
||||||
|
.bg(strip_background)
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.map(|strip| titlebar_strip(strip, window, cx))
|
.map(|strip| titlebar_strip(strip, window, cx))
|
||||||
@@ -305,6 +322,13 @@ fn sidebar_region(
|
|||||||
.h_full()
|
.h_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(strip)
|
.child(strip)
|
||||||
.child(div().flex_1().min_h_0().overflow_hidden().child(content))
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.min_h_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.when(!material, |content| content.bg(background))
|
||||||
|
.child(content),
|
||||||
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,12 +146,13 @@ fn the_header_toggle_hides_its_sidebar() {
|
|||||||
// icon-only small button: derive its center from the theme metrics.
|
// icon-only small button: derive its center from the theme metrics.
|
||||||
let metrics = cx.update(|cx| cx.theme().sizes.resolve(Size::default()));
|
let metrics = cx.update(|cx| cx.theme().sizes.resolve(Size::default()));
|
||||||
let small = cx.update(|cx| cx.theme().sizes.resolve(Size::Small));
|
let small = cx.update(|cx| cx.theme().sizes.resolve(Size::Small));
|
||||||
|
let strip_height = cx.update(|cx| cx.theme().titlebar_height);
|
||||||
let region = bounds(&mut visual, "sidebar-test-left").expect("left sidebar missing");
|
let region = bounds(&mut visual, "sidebar-test-left").expect("left sidebar missing");
|
||||||
// The Sidebar fills the content area below the strip; the toggle sits at
|
// The Sidebar fills the content area below the strip; the toggle sits at
|
||||||
// the strip's trailing end, one strip height above the sidebar's top.
|
// the strip's trailing end, one strip height above the sidebar's top.
|
||||||
let toggle = point(
|
let toggle = point(
|
||||||
region.right() - metrics.dense_padding_x - small.control_height / 2.0,
|
region.right() - metrics.dense_padding_x - small.control_height / 2.0,
|
||||||
region.top() - metrics.strip_height / 2.0,
|
region.top() - strip_height / 2.0,
|
||||||
);
|
);
|
||||||
press(&mut visual, toggle, 1);
|
press(&mut visual, toggle, 1);
|
||||||
redraw(&mut visual);
|
redraw(&mut visual);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
use gpui::{Hsla, hsla, rgb};
|
use gpui::{Hsla, hsla, rgb};
|
||||||
|
|
||||||
|
use crate::ThemeVariant;
|
||||||
|
|
||||||
fn hex(hex: u32) -> Hsla {
|
fn hex(hex: u32) -> Hsla {
|
||||||
rgb(hex).into()
|
rgb(hex).into()
|
||||||
}
|
}
|
||||||
@@ -115,3 +117,44 @@ impl ColorTokens {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The translucent paints surfaces lay over a native window material.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct MaterialTokens {
|
||||||
|
/// The window-sized fill the material shows through.
|
||||||
|
pub background: Hsla,
|
||||||
|
/// The hover wash over material surfaces.
|
||||||
|
pub hover: Hsla,
|
||||||
|
/// The selected-row fill over material surfaces.
|
||||||
|
pub selection: Hsla,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaterialTokens {
|
||||||
|
/// Derives the material paints from the resolved palette.
|
||||||
|
pub(crate) fn derive(
|
||||||
|
colors: &ColorTokens,
|
||||||
|
selection: Hsla,
|
||||||
|
variant: ThemeVariant,
|
||||||
|
translucent: bool,
|
||||||
|
) -> Self {
|
||||||
|
if !translucent {
|
||||||
|
return Self {
|
||||||
|
background: colors.background,
|
||||||
|
hover: colors.muted,
|
||||||
|
selection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Light tints heavier than dark: the blur carries less contrast there.
|
||||||
|
let background_alpha = match variant {
|
||||||
|
ThemeVariant::Light => 0.6,
|
||||||
|
ThemeVariant::Dark => 0.5,
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
background: colors.background.alpha(background_alpha),
|
||||||
|
// A foreground wash keeps hover visible over any backdrop hue.
|
||||||
|
hover: colors.foreground.alpha(0.06),
|
||||||
|
selection: selection.alpha(0.85),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ mod resolve;
|
|||||||
mod source;
|
mod source;
|
||||||
mod system;
|
mod system;
|
||||||
|
|
||||||
pub use colors::ColorTokens;
|
pub use colors::{ColorTokens, MaterialTokens};
|
||||||
pub use definition::ThemeDefinition;
|
pub use definition::ThemeDefinition;
|
||||||
pub use ramps::{AccentShades, Oklch};
|
pub use ramps::{AccentShades, Oklch};
|
||||||
pub use resolve::{Ramps, ResolvedTheme};
|
pub use resolve::{Ramps, ResolvedTheme};
|
||||||
pub use system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemTheme};
|
pub use system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemTheme};
|
||||||
|
|
||||||
use gpui::{App, Global, Hsla, Pixels, WindowAppearance, px};
|
use gpui::{App, Global, Hsla, Pixels, WindowAppearance, WindowBackgroundAppearance, px};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// The hairline width of the control border every button and field paints;
|
/// The hairline width of the control border every button and field paints;
|
||||||
@@ -50,6 +50,8 @@ impl ThemeVariant {
|
|||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub variant: ThemeVariant,
|
pub variant: ThemeVariant,
|
||||||
pub colors: ColorTokens,
|
pub colors: ColorTokens,
|
||||||
|
/// The translucent paints over the window's native material.
|
||||||
|
pub material: MaterialTokens,
|
||||||
pub ramps: Ramps,
|
pub ramps: Ramps,
|
||||||
pub radius: Radius,
|
pub radius: Radius,
|
||||||
/// Per-platform border and focus chrome.
|
/// Per-platform border and focus chrome.
|
||||||
@@ -68,6 +70,8 @@ pub struct Theme {
|
|||||||
pub mono_font: &'static str,
|
pub mono_font: &'static str,
|
||||||
pub sizes: SizeTable,
|
pub sizes: SizeTable,
|
||||||
pub accessibility: AccessibilityPreferences,
|
pub accessibility: AccessibilityPreferences,
|
||||||
|
/// The material a window should open with, after accessibility.
|
||||||
|
pub window_background: WindowBackgroundAppearance,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Global for Theme {}
|
impl Global for Theme {}
|
||||||
@@ -524,6 +528,7 @@ impl Theme {
|
|||||||
Self {
|
Self {
|
||||||
variant: resolved.variant,
|
variant: resolved.variant,
|
||||||
colors: resolved.colors,
|
colors: resolved.colors,
|
||||||
|
material: resolved.material,
|
||||||
ramps: resolved.ramps,
|
ramps: resolved.ramps,
|
||||||
radius: resolved.radius,
|
radius: resolved.radius,
|
||||||
borders: resolved.borders,
|
borders: resolved.borders,
|
||||||
@@ -536,6 +541,7 @@ impl Theme {
|
|||||||
mono_font: resolved.mono_font,
|
mono_font: resolved.mono_font,
|
||||||
sizes: resolved.sizes,
|
sizes: resolved.sizes,
|
||||||
accessibility: resolved.accessibility,
|
accessibility: resolved.accessibility,
|
||||||
|
window_background: resolved.window_background,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+89
-19
@@ -2,9 +2,10 @@ use crate::definition::ThemeDefinition;
|
|||||||
use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix};
|
use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix};
|
||||||
use crate::system::SystemTheme;
|
use crate::system::SystemTheme;
|
||||||
use crate::{
|
use crate::{
|
||||||
Borders, ColorTokens, Elevations, Motion, Radius, Scrollbar, SizeTable, Spacing, ThemeVariant,
|
Borders, ColorTokens, Elevations, MaterialTokens, Motion, Radius, Scrollbar, SizeTable,
|
||||||
|
Spacing, ThemeVariant,
|
||||||
};
|
};
|
||||||
use gpui::px;
|
use gpui::{WindowBackgroundAppearance, px};
|
||||||
|
|
||||||
/// The floor of a custom titlebar's height, in px at 100% scaling: the height
|
/// The floor of a custom titlebar's height, in px at 100% scaling: the height
|
||||||
/// macOS and Linux titlebars draw at regardless of the control scale.
|
/// macOS and Linux titlebars draw at regardless of the control scale.
|
||||||
@@ -26,6 +27,8 @@ pub struct Ramps {
|
|||||||
pub struct ResolvedTheme {
|
pub struct ResolvedTheme {
|
||||||
pub variant: ThemeVariant,
|
pub variant: ThemeVariant,
|
||||||
pub colors: ColorTokens,
|
pub colors: ColorTokens,
|
||||||
|
/// The translucent paints over the window's native material.
|
||||||
|
pub material: MaterialTokens,
|
||||||
pub ramps: Ramps,
|
pub ramps: Ramps,
|
||||||
pub radius: Radius,
|
pub radius: Radius,
|
||||||
pub borders: Borders,
|
pub borders: Borders,
|
||||||
@@ -41,6 +44,8 @@ pub struct ResolvedTheme {
|
|||||||
pub mono_font: &'static str,
|
pub mono_font: &'static str,
|
||||||
pub sizes: SizeTable,
|
pub sizes: SizeTable,
|
||||||
pub accessibility: crate::system::AccessibilityPreferences,
|
pub accessibility: crate::system::AccessibilityPreferences,
|
||||||
|
/// The material a window should open with, after accessibility.
|
||||||
|
pub window_background: WindowBackgroundAppearance,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Overlay order (later wins): neutral base → system → preset definition.
|
/// Overlay order (later wins): neutral base → system → preset definition.
|
||||||
@@ -187,6 +192,15 @@ pub(crate) fn resolve(
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reduced transparency forces the window opaque.
|
||||||
|
let window_background = if system.accessibility.reduce_transparency {
|
||||||
|
WindowBackgroundAppearance::Opaque
|
||||||
|
} else {
|
||||||
|
system
|
||||||
|
.window_background
|
||||||
|
.unwrap_or(WindowBackgroundAppearance::Opaque)
|
||||||
|
};
|
||||||
|
|
||||||
let accessibility = system.accessibility;
|
let accessibility = system.accessibility;
|
||||||
if accessibility.high_contrast {
|
if accessibility.high_contrast {
|
||||||
colors.border = mix(
|
colors.border = mix(
|
||||||
@@ -255,25 +269,35 @@ pub(crate) fn resolve(
|
|||||||
radius.sm = sm;
|
radius.sm = sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ramps = Ramps {
|
||||||
|
primary: derive_shades(primary, colors.background, variant == ThemeVariant::Dark),
|
||||||
|
secondary: derive_shades(
|
||||||
|
colors.secondary,
|
||||||
|
colors.background,
|
||||||
|
variant == ThemeVariant::Dark,
|
||||||
|
),
|
||||||
|
destructive: derive_shades(
|
||||||
|
destructive,
|
||||||
|
colors.background,
|
||||||
|
variant == ThemeVariant::Dark,
|
||||||
|
),
|
||||||
|
success: derive_shades(success, colors.background, variant == ThemeVariant::Dark),
|
||||||
|
warning: derive_shades(warning, colors.background, variant == ThemeVariant::Dark),
|
||||||
|
info: derive_shades(info, colors.background, variant == ThemeVariant::Dark),
|
||||||
|
};
|
||||||
|
|
||||||
|
let material = MaterialTokens::derive(
|
||||||
|
&colors,
|
||||||
|
ramps.primary.base,
|
||||||
|
variant,
|
||||||
|
window_background != WindowBackgroundAppearance::Opaque,
|
||||||
|
);
|
||||||
|
|
||||||
ResolvedTheme {
|
ResolvedTheme {
|
||||||
variant,
|
variant,
|
||||||
ramps: Ramps {
|
ramps,
|
||||||
primary: derive_shades(primary, colors.background, variant == ThemeVariant::Dark),
|
|
||||||
secondary: derive_shades(
|
|
||||||
colors.secondary,
|
|
||||||
colors.background,
|
|
||||||
variant == ThemeVariant::Dark,
|
|
||||||
),
|
|
||||||
destructive: derive_shades(
|
|
||||||
destructive,
|
|
||||||
colors.background,
|
|
||||||
variant == ThemeVariant::Dark,
|
|
||||||
),
|
|
||||||
success: derive_shades(success, colors.background, variant == ThemeVariant::Dark),
|
|
||||||
warning: derive_shades(warning, colors.background, variant == ThemeVariant::Dark),
|
|
||||||
info: derive_shades(info, colors.background, variant == ThemeVariant::Dark),
|
|
||||||
},
|
|
||||||
colors,
|
colors,
|
||||||
|
material,
|
||||||
radius,
|
radius,
|
||||||
borders: Borders::for_platform(system.source, variant),
|
borders: Borders::for_platform(system.source, variant),
|
||||||
spacing: Spacing {
|
spacing: Spacing {
|
||||||
@@ -290,6 +314,7 @@ pub(crate) fn resolve(
|
|||||||
mono_font: mono_font(),
|
mono_font: mono_font(),
|
||||||
sizes,
|
sizes,
|
||||||
accessibility,
|
accessibility,
|
||||||
|
window_background,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +333,7 @@ fn mono_font() -> &'static str {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind};
|
use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind};
|
||||||
use gpui::{Rgba, rgb};
|
use gpui::{Rgba, WindowBackgroundAppearance, rgb};
|
||||||
|
|
||||||
fn hsla(hex: u32) -> gpui::Hsla {
|
fn hsla(hex: u32) -> gpui::Hsla {
|
||||||
gpui::Hsla::from(rgb(hex))
|
gpui::Hsla::from(rgb(hex))
|
||||||
@@ -335,6 +360,7 @@ mod tests {
|
|||||||
titlebar_height: None,
|
titlebar_height: None,
|
||||||
root_font_size: None,
|
root_font_size: None,
|
||||||
accessibility: AccessibilityPreferences::default(),
|
accessibility: AccessibilityPreferences::default(),
|
||||||
|
window_background: None,
|
||||||
source: SourceKind::Macos,
|
source: SourceKind::Macos,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,6 +563,50 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_platforms_window_material_resolves_into_the_theme() {
|
||||||
|
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
|
||||||
|
stated.window_background = Some(WindowBackgroundAppearance::MicaBackdrop);
|
||||||
|
let resolved = resolve(&stated, None, None);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolved.window_background,
|
||||||
|
WindowBackgroundAppearance::MicaBackdrop
|
||||||
|
);
|
||||||
|
// Material surfaces stay translucent over the stated backdrop.
|
||||||
|
assert!(resolved.material.background.a < resolved.colors.background.a);
|
||||||
|
assert!(resolved.material.selection.a < 1.0);
|
||||||
|
|
||||||
|
let unstated = resolve(
|
||||||
|
&system(ThemeVariant::Light, PartialPalette::default(), None),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unstated.window_background,
|
||||||
|
WindowBackgroundAppearance::Opaque
|
||||||
|
);
|
||||||
|
assert_eq!(unstated.material.background, unstated.colors.background);
|
||||||
|
assert_eq!(unstated.material.hover, unstated.colors.muted);
|
||||||
|
assert_eq!(unstated.material.selection, unstated.ramps.primary.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reduced_transparency_forces_an_opaque_window_and_material() {
|
||||||
|
let mut stated = system(ThemeVariant::Dark, PartialPalette::default(), None);
|
||||||
|
stated.window_background = Some(WindowBackgroundAppearance::Blurred);
|
||||||
|
stated.accessibility.reduce_transparency = true;
|
||||||
|
let resolved = resolve(&stated, None, None);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolved.window_background,
|
||||||
|
WindowBackgroundAppearance::Opaque
|
||||||
|
);
|
||||||
|
assert_eq!(resolved.material.background, resolved.colors.background);
|
||||||
|
assert_eq!(resolved.material.hover, resolved.colors.muted);
|
||||||
|
assert_eq!(resolved.material.selection, resolved.ramps.primary.base);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn overlay_is_translucent_and_overridable() {
|
fn overlay_is_translucent_and_overridable() {
|
||||||
let light = resolve(
|
let light = resolve(
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ impl KdeScheme {
|
|||||||
titlebar_height: Some(px(44.)),
|
titlebar_height: Some(px(44.)),
|
||||||
root_font_size: Some(self.root_font_size.unwrap_or(px(13.33))),
|
root_font_size: Some(self.root_font_size.unwrap_or(px(13.33))),
|
||||||
accessibility,
|
accessibility,
|
||||||
|
window_background: None,
|
||||||
source: SourceKind::Kde,
|
source: SourceKind::Kde,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ fn home_dir() -> Option<PathBuf> {
|
|||||||
std::env::var_os("HOME").map(PathBuf::from)
|
std::env::var_os("HOME").map(PathBuf::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the session runs on Wayland, where compositors state blur protocols.
|
||||||
|
fn wayland_session() -> bool {
|
||||||
|
std::env::var_os("WAYLAND_DISPLAY").is_some()
|
||||||
|
|| std::env::var("XDG_SESSION_TYPE").is_ok_and(|session| session == "wayland")
|
||||||
|
}
|
||||||
|
|
||||||
fn detect(home: &Path, portal_reachable: bool, gsettings_reachable: bool) -> Backend {
|
fn detect(home: &Path, portal_reachable: bool, gsettings_reachable: bool) -> Backend {
|
||||||
let omarchy_marker = home
|
let omarchy_marker = home
|
||||||
.join(".local/state/omarchy/current/theme.name")
|
.join(".local/state/omarchy/current/theme.name")
|
||||||
@@ -164,6 +170,7 @@ impl LinuxSource {
|
|||||||
.unwrap_or(gpui::px(14.67)),
|
.unwrap_or(gpui::px(14.67)),
|
||||||
),
|
),
|
||||||
accessibility,
|
accessibility,
|
||||||
|
window_background: None,
|
||||||
source: SourceKind::Gnome,
|
source: SourceKind::Gnome,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,15 +210,24 @@ impl ThemeSource for LinuxSource {
|
|||||||
|
|
||||||
match self.backend {
|
match self.backend {
|
||||||
Backend::Omarchy => omarchy::io::read_current()
|
Backend::Omarchy => omarchy::io::read_current()
|
||||||
.map(|theme| theme.into_system(accessibility))
|
.map(|theme| {
|
||||||
|
let mut system = theme.into_system(accessibility);
|
||||||
|
// Hyprland blurs transparent surfaces on its own.
|
||||||
|
system.window_background = Some(gpui::WindowBackgroundAppearance::Blurred);
|
||||||
|
system
|
||||||
|
})
|
||||||
.unwrap_or_else(|| SystemTheme::fallback(variant)),
|
.unwrap_or_else(|| SystemTheme::fallback(variant)),
|
||||||
Backend::Kde => kde::io::read_current()
|
Backend::Kde => kde::io::read_current()
|
||||||
.map(|scheme| {
|
.map(|scheme| {
|
||||||
scheme.into_system(
|
let mut system = scheme.into_system(
|
||||||
accessibility,
|
accessibility,
|
||||||
portal_appearance.and_then(|appearance| appearance.accent),
|
portal_appearance.and_then(|appearance| appearance.accent),
|
||||||
portal_appearance.map(|appearance| appearance.variant(variant)),
|
portal_appearance.map(|appearance| appearance.variant(variant)),
|
||||||
)
|
);
|
||||||
|
// KWin states the blur protocols; X11 compositors do not.
|
||||||
|
system.window_background =
|
||||||
|
wayland_session().then_some(gpui::WindowBackgroundAppearance::Blurred);
|
||||||
|
system
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| SystemTheme::fallback(variant)),
|
.unwrap_or_else(|| SystemTheme::fallback(variant)),
|
||||||
Backend::Portal => self.portal_snapshot(variant, portal_appearance, accessibility),
|
Backend::Portal => self.portal_snapshot(variant, portal_appearance, accessibility),
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ impl ThemeSource for MacosSource {
|
|||||||
variant,
|
variant,
|
||||||
accent: Self::color_to_hsla(&NSColor::controlAccentColor()),
|
accent: Self::color_to_hsla(&NSColor::controlAccentColor()),
|
||||||
palette,
|
palette,
|
||||||
|
// macOS blurs what is behind the window natively.
|
||||||
|
window_background: Some(gpui::WindowBackgroundAppearance::Blurred),
|
||||||
control_radius: Some(gpui::px(CONTROL_RADIUS_PTS)),
|
control_radius: Some(gpui::px(CONTROL_RADIUS_PTS)),
|
||||||
overlay_radius: None,
|
overlay_radius: None,
|
||||||
sm_radius: None,
|
sm_radius: None,
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ impl OmarchyTheme {
|
|||||||
titlebar_height: None,
|
titlebar_height: None,
|
||||||
root_font_size: None,
|
root_font_size: None,
|
||||||
accessibility,
|
accessibility,
|
||||||
|
window_background: None,
|
||||||
source: SourceKind::Omarchy,
|
source: SourceKind::Omarchy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ const TITLEBAR_HEIGHT_PX: f32 = 32.0;
|
|||||||
/// WinUI 11's default body text size, in pixels at 100% scaling.
|
/// WinUI 11's default body text size, in pixels at 100% scaling.
|
||||||
const ROOT_FONT_SIZE_PX: f32 = 14.0;
|
const ROOT_FONT_SIZE_PX: f32 = 14.0;
|
||||||
|
|
||||||
/// The `Personalize` registry values, read once at startup as the fallback
|
/// The `Personalize` registry values and the OS build number.
|
||||||
/// when the WinRT surface is unavailable.
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
struct RegistrySettings {
|
struct RegistrySettings {
|
||||||
apps_use_light_theme: Option<bool>,
|
apps_use_light_theme: Option<bool>,
|
||||||
enable_transparency: Option<bool>,
|
enable_transparency: Option<bool>,
|
||||||
|
current_build: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegistrySettings {
|
impl RegistrySettings {
|
||||||
@@ -45,6 +45,9 @@ impl RegistrySettings {
|
|||||||
windows_registry::CURRENT_USER
|
windows_registry::CURRENT_USER
|
||||||
.open(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")
|
.open(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")
|
||||||
};
|
};
|
||||||
|
let current_version = || {
|
||||||
|
windows_registry::LOCAL_MACHINE.open(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion")
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
apps_use_light_theme: personalize()
|
apps_use_light_theme: personalize()
|
||||||
.and_then(|key| key.get_u32("AppsUseLightTheme"))
|
.and_then(|key| key.get_u32("AppsUseLightTheme"))
|
||||||
@@ -54,6 +57,10 @@ impl RegistrySettings {
|
|||||||
.and_then(|key| key.get_u32("EnableTransparency"))
|
.and_then(|key| key.get_u32("EnableTransparency"))
|
||||||
.ok()
|
.ok()
|
||||||
.map(|value| value != 0),
|
.map(|value| value != 0),
|
||||||
|
current_build: current_version()
|
||||||
|
.and_then(|key| key.get_string("CurrentBuildNumber"))
|
||||||
|
.ok()
|
||||||
|
.and_then(|build| build.parse::<u32>().ok()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,6 +137,17 @@ impl WindowsSource {
|
|||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The native backdrop: Mica on Windows 11, acrylic on Windows 10 1809+.
|
||||||
|
fn window_material(&self) -> Option<gpui::WindowBackgroundAppearance> {
|
||||||
|
match self.registry.current_build {
|
||||||
|
// 22621: Windows 11 22H2, where the DWM backdrop types exist.
|
||||||
|
Some(build) if build >= 22621 => Some(gpui::WindowBackgroundAppearance::MicaBackdrop),
|
||||||
|
// 17763: Windows 10 1809, where acrylic blur-behind exists.
|
||||||
|
Some(build) if build >= 17763 => Some(gpui::WindowBackgroundAppearance::Blurred),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeSource for WindowsSource {
|
impl ThemeSource for WindowsSource {
|
||||||
@@ -180,6 +198,7 @@ impl ThemeSource for WindowsSource {
|
|||||||
titlebar_height: Some(gpui::px(TITLEBAR_HEIGHT_PX)),
|
titlebar_height: Some(gpui::px(TITLEBAR_HEIGHT_PX)),
|
||||||
root_font_size: Some(gpui::px(ROOT_FONT_SIZE_PX * text_scale as f32)),
|
root_font_size: Some(gpui::px(ROOT_FONT_SIZE_PX * text_scale as f32)),
|
||||||
accessibility: self.accessibility(),
|
accessibility: self.accessibility(),
|
||||||
|
window_background: self.window_material(),
|
||||||
source: SourceKind::Windows,
|
source: SourceKind::Windows,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! The normalized snapshot a platform adapter produces.
|
//! The normalized snapshot a platform adapter produces.
|
||||||
|
|
||||||
use gpui::{Hsla, Pixels, px};
|
use gpui::{Hsla, Pixels, WindowBackgroundAppearance, px};
|
||||||
|
|
||||||
use crate::ThemeVariant;
|
use crate::ThemeVariant;
|
||||||
|
|
||||||
@@ -115,6 +115,8 @@ pub struct SystemTheme {
|
|||||||
/// The platform's native root font size, if it has one.
|
/// The platform's native root font size, if it has one.
|
||||||
pub root_font_size: Option<Pixels>,
|
pub root_font_size: Option<Pixels>,
|
||||||
pub accessibility: AccessibilityPreferences,
|
pub accessibility: AccessibilityPreferences,
|
||||||
|
/// The window material the platform natively applies, if any.
|
||||||
|
pub window_background: Option<WindowBackgroundAppearance>,
|
||||||
pub source: SourceKind,
|
pub source: SourceKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +138,7 @@ impl SystemTheme {
|
|||||||
titlebar_height: None,
|
titlebar_height: None,
|
||||||
root_font_size: None,
|
root_font_size: None,
|
||||||
accessibility: AccessibilityPreferences::default(),
|
accessibility: AccessibilityPreferences::default(),
|
||||||
|
window_background: None,
|
||||||
source: SourceKind::Fallback,
|
source: SourceKind::Fallback,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,6 +223,8 @@ struct Gallery {
|
|||||||
palette_runs: Vec<SharedString>,
|
palette_runs: Vec<SharedString>,
|
||||||
dialog_open: bool,
|
dialog_open: bool,
|
||||||
alert_open: bool,
|
alert_open: bool,
|
||||||
|
/// Whether the app sidebar paints the window's native material.
|
||||||
|
sidebar_transparent: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gallery {
|
impl Gallery {
|
||||||
@@ -254,6 +256,7 @@ impl Gallery {
|
|||||||
|
|
||||||
let left_sidebar = Sidebar::new("gallery-sidebar")
|
let left_sidebar = Sidebar::new("gallery-sidebar")
|
||||||
.selected(selected)
|
.selected(selected)
|
||||||
|
.transparent(self.sidebar_transparent)
|
||||||
.accessibility_label("Component catalog")
|
.accessibility_label("Component catalog")
|
||||||
.entries(|_, _| {
|
.entries(|_, _| {
|
||||||
CATALOG
|
CATALOG
|
||||||
@@ -404,10 +407,12 @@ impl Gallery {
|
|||||||
PanelDescriptor::new("welcome", "Welcome", {
|
PanelDescriptor::new("welcome", "Welcome", {
|
||||||
let layout_state = layout_state.clone();
|
let layout_state = layout_state.clone();
|
||||||
let dock_state = dock_state.clone();
|
let dock_state = dock_state.clone();
|
||||||
|
let gallery = gallery.clone();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
cx.new(|_| WelcomePanel {
|
cx.new(|_| WelcomePanel {
|
||||||
layout_state: layout_state.clone(),
|
layout_state: layout_state.clone(),
|
||||||
dock_state: dock_state.clone(),
|
dock_state: dock_state.clone(),
|
||||||
|
gallery: gallery.clone(),
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
@@ -729,7 +734,6 @@ impl Render for Gallery {
|
|||||||
palette.update(cx, |palette, cx| palette.toggle(window, cx));
|
palette.update(cx, |palette, cx| palette.toggle(window, cx));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.bg(theme.colors.background)
|
|
||||||
.text_color(theme.colors.foreground)
|
.text_color(theme.colors.foreground)
|
||||||
.child(self.render_shell(cx))
|
.child(self.render_shell(cx))
|
||||||
.child(dialog)
|
.child(dialog)
|
||||||
@@ -745,6 +749,7 @@ impl Render for Gallery {
|
|||||||
struct WelcomePanel {
|
struct WelcomePanel {
|
||||||
layout_state: Entity<LayoutState>,
|
layout_state: Entity<LayoutState>,
|
||||||
dock_state: Entity<DockState>,
|
dock_state: Entity<DockState>,
|
||||||
|
gallery: WeakEntity<Gallery>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for WelcomePanel {
|
impl Render for WelcomePanel {
|
||||||
@@ -753,6 +758,7 @@ impl Render for WelcomePanel {
|
|||||||
let layout_left = self.layout_state.clone();
|
let layout_left = self.layout_state.clone();
|
||||||
let layout_right = self.layout_state.clone();
|
let layout_right = self.layout_state.clone();
|
||||||
let dock_state = self.dock_state.clone();
|
let dock_state = self.dock_state.clone();
|
||||||
|
let gallery = self.gallery.clone();
|
||||||
|
|
||||||
ScrollArea::new("welcome-demo")
|
ScrollArea::new("welcome-demo")
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -811,6 +817,21 @@ impl Render for WelcomePanel {
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("welcome-toggle-blur")
|
||||||
|
.label("Toggle sidebar blur")
|
||||||
|
.variant(ButtonVariant::Secondary)
|
||||||
|
.small()
|
||||||
|
.on_click(move |_, _, cx| {
|
||||||
|
gallery
|
||||||
|
.update(cx, |gallery, cx| {
|
||||||
|
gallery.sidebar_transparent =
|
||||||
|
!gallery.sidebar_transparent;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("welcome-split")
|
Button::new("welcome-split")
|
||||||
.label("Split active right")
|
.label("Split active right")
|
||||||
@@ -3774,6 +3795,7 @@ impl Render for SidebarDemo {
|
|||||||
.child(
|
.child(
|
||||||
Sidebar::new("sidebar-demo-list")
|
Sidebar::new("sidebar-demo-list")
|
||||||
.selected(self.selected)
|
.selected(self.selected)
|
||||||
|
.transparent(true)
|
||||||
.on_change(move |selected, _, cx| {
|
.on_change(move |selected, _, cx| {
|
||||||
view.update(cx, |demo, cx| {
|
view.update(cx, |demo, cx| {
|
||||||
demo.selected = selected;
|
demo.selected = selected;
|
||||||
@@ -4077,6 +4099,9 @@ fn main() {
|
|||||||
// The strips move the window themselves, so a tab dragged inside
|
// The strips move the window themselves, so a tab dragged inside
|
||||||
// the titlebar band never drags the window with it.
|
// the titlebar band never drags the window with it.
|
||||||
app_owns_titlebar_drag: true,
|
app_owns_titlebar_drag: true,
|
||||||
|
// The platform's native backdrop; the theme's material paints
|
||||||
|
// resolve over it and go opaque under reduced transparency.
|
||||||
|
window_background: cx.theme().window_background,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|window, cx| {
|
|window, cx| {
|
||||||
@@ -4106,6 +4131,7 @@ fn main() {
|
|||||||
palette_runs: Vec::new(),
|
palette_runs: Vec::new(),
|
||||||
dialog_open: false,
|
dialog_open: false,
|
||||||
alert_open: false,
|
alert_open: false,
|
||||||
|
sidebar_transparent: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let palette = cx.new(|cx| {
|
let palette = cx.new(|cx| {
|
||||||
|
|||||||
Reference in New Issue
Block a user