This commit is contained in:
2026-11-01 15:30:12 +07:00
parent 749b3e4e29
commit 49da45c8ac
16 changed files with 128 additions and 64 deletions
+13 -7
View File
@@ -7,7 +7,7 @@ use gpui::{
Styled as _, Window, deferred, div, ease_out_quint, px,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::DismissHandler;
use crate::common::v_flex;
@@ -128,8 +128,10 @@ impl RenderOnce for Alert {
let surface = theme.colors.surface;
let border = theme.colors.border;
let shadows = theme.elevations.popover.shadows();
let text_size = theme.sizes.small.text_size;
let icon_size = theme.sizes.small.icon_size;
let metrics = theme.sizes.resolve(Size::Medium);
let icon_size = metrics.icon_size;
let title_size = metrics.text_size;
let caption_size = metrics.caption_size;
div()
.w_full()
@@ -160,7 +162,7 @@ impl RenderOnce for Alert {
.when_some(self.title.clone(), |alert, title| {
alert.child(
div()
.text_size(text_size)
.text_size(title_size)
.text_color(foreground)
.when(self.message.is_some(), |title| {
title.font_weight(gpui::FontWeight::SEMIBOLD)
@@ -169,13 +171,17 @@ impl RenderOnce for Alert {
)
})
.when_some(self.message.clone(), |alert, message| {
// With no title, the message is the primary line.
let color = if self.title.is_some() {
muted_foreground
} else {
foreground
};
alert.child(div().text_size(text_size).text_color(color).child(message))
alert.child(
div()
.text_size(caption_size)
.text_color(color)
.child(message),
)
}),
)
.when_some(self.on_dismiss, |alert, on_dismiss| {
@@ -189,7 +195,7 @@ impl RenderOnce for Alert {
.hover(|style| style.bg(muted))
.child(
icon(IconRole::Close)
.size(theme.sizes.small.icon_size * 0.75)
.size(theme.sizes.small.dense_icon_size)
.color(foreground.alpha(0.55)),
)
.on_click(move |_, _window, cx| on_dismiss(cx)),
+2 -5
View File
@@ -83,11 +83,9 @@ impl RenderOnce for Badge {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let metrics = theme.sizes.resolve(self.size);
let label_size = metrics.caption_size;
let (background, foreground) = self.variant.tokens(theme);
// Counts read smaller than control labels, like every native badge.
let label_size = metrics.text_size * 0.8;
h_flex()
.flex_none()
.justify_center()
@@ -95,8 +93,7 @@ impl RenderOnce for Badge {
.bg(background)
.text_color(foreground)
.text_size(label_size)
.line_height(label_size * 1.25)
.px(metrics.padding_x * 0.5)
.px(metrics.dense_padding_x)
.py_0p5()
.child(self.label)
.refine_style(&self.style)
+6 -2
View File
@@ -121,7 +121,7 @@ impl RenderOnce for Checkbox {
h_flex()
.justify_center()
.flex_none()
.size(metrics.icon_size)
.size(metrics.indicator_size)
.rounded(theme.radius.sm)
.border_1()
.when(self.checked, |indicator| {
@@ -129,7 +129,11 @@ impl RenderOnce for Checkbox {
.bg(ramps.primary.base)
.border_color(ramps.primary.base)
.hover(|style| style.bg(ramps.primary.hover))
.child(icon(IconRole::Check).color(colors.primary_foreground))
.child(
icon(IconRole::Check)
.size(metrics.dense_icon_size)
.color(colors.primary_foreground),
)
})
.when(!self.checked, |indicator| {
indicator
+1 -1
View File
@@ -1551,7 +1551,7 @@ impl RenderOnce for Input {
.overflow_hidden()
.w_full()
.h(metrics.input_height)
.px(metrics.padding_x / 2.0)
.px(metrics.dense_padding_x)
.rounded(radius.control)
.border_1()
.border_color(colors.border)
+4 -2
View File
@@ -136,6 +136,7 @@ struct MenuChrome {
radius_overlay: Pixels,
radius_sm: Pixels,
icon_size: Pixels,
dense_icon_size: Pixels,
shadow: Vec<BoxShadow>,
}
@@ -234,6 +235,7 @@ impl RenderOnce for Menu {
radius_overlay: theme.radius.overlay,
radius_sm: theme.radius.sm,
icon_size: theme.sizes.medium.icon_size,
dense_icon_size: theme.sizes.medium.dense_icon_size,
shadow: theme.elevations.popover.shadows(),
},
theme.motion.overlay,
@@ -348,7 +350,7 @@ impl RenderOnce for Menu {
.role(Role::Menu)
.aria_label(label)
.mt_1()
.min_w_40()
.min_w_56()
.p_1()
.bg(chrome.surface)
.text_color(chrome.foreground)
@@ -522,7 +524,7 @@ fn render_item(
.when(checked == Some(true), |row| {
row.child(
icon_glyph(IconRole::Check)
.size(chrome.icon_size * 0.75)
.size(chrome.dense_icon_size)
.color(label_color),
)
})
+2 -2
View File
@@ -189,7 +189,7 @@ where
h_flex()
.justify_center()
.flex_none()
.size(metrics.icon_size)
.size(metrics.indicator_size)
.rounded(radius.full)
.border_1()
.when(is_selected, |indicator| {
@@ -199,7 +199,7 @@ where
.hover(|style| style.bg(ramps.primary.hover))
.child(
div()
.size(metrics.icon_size / 2.0)
.size(metrics.indicator_size / 2.0)
.rounded(radius.full)
.bg(colors.primary_foreground),
)
+6 -23
View File
@@ -5,7 +5,7 @@ use gpui::{
AnimationExt as _, App, Bounds, ElementId, Entity, InteractiveElement as _, IntoElement,
KeyDownEvent, ParentElement as _, Point, RenderOnce, Role, SharedString, SpringAnimation,
SpringConfig, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Toggled, Window,
canvas, div, rems,
canvas, div,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, Size};
@@ -152,29 +152,12 @@ where
let ring = theme.colors.ring;
let radius = theme.radius.full;
let plate_shadow = theme.elevations.control.shadows();
let icon_size = metrics.icon_size * 0.85;
let track_height = match self.size {
Size::Small => metrics.control_height,
Size::Medium => metrics.control_height * 1.15,
Size::Large => metrics.control_height * 1.35,
};
let track_inset = match self.size {
Size::Small => rems(0.125),
Size::Medium => rems(0.25),
Size::Large => rems(0.375),
};
let horizontal_padding = match self.size {
Size::Small => metrics.padding_x * 0.5,
Size::Medium | Size::Large => metrics.padding_x * 0.75,
};
let text_size = match self.size {
Size::Small => metrics.text_size * 0.85,
Size::Medium | Size::Large => metrics.text_size,
};
let track_height = metrics.strip_height;
let track_inset = metrics.plate_inset;
let horizontal_padding = metrics.dense_padding_x;
let text_size = metrics.caption_size;
let icon_size = metrics.icon_size;
let selected_index = self
.options
+2 -2
View File
@@ -168,7 +168,7 @@ where
let foreground = theme.colors.foreground;
let muted_foreground = theme.colors.muted_foreground;
let radius = theme.radius.control;
let icon_size = metrics.icon_size * 0.75;
let icon_size = metrics.dense_icon_size;
let label = self
.a11y_label
@@ -214,7 +214,7 @@ where
.gap_2()
.w_full()
.h(metrics.input_height)
.px(metrics.padding_x / 2.0)
.px(metrics.dense_padding_x)
.rounded(radius)
.border_1()
.border_color(if is_open { ring } else { border })
+4 -4
View File
@@ -96,11 +96,11 @@ impl RenderOnce for Switch {
let ramps = theme.ramps;
let metrics = theme.sizes.resolve(self.size);
let track_height = metrics.icon_size * 1.25;
let track_width = metrics.icon_size * 2.25;
let track_height = metrics.control_height;
let track_width = metrics.control_height * 1.65;
let knob = metrics.icon_size;
let inset = metrics.icon_size * 0.125;
let knob = metrics.indicator_size;
let inset = metrics.plate_inset;
let travel = track_width - knob - inset * 2.;
let next = !self.checked;
+2 -1
View File
@@ -266,6 +266,7 @@ fn pill(entry: &ToastEntry, toasts: &Entity<Toasts>, cx: &App) -> gpui::Stateful
let radius = theme.radius.full;
let shadows = theme.elevations.popover.shadows();
let icon_size = theme.sizes.small.icon_size;
let dense_icon_size = theme.sizes.small.dense_icon_size;
let text_size = theme.sizes.small.text_size;
let toast = &entry.toast;
let toasts = toasts.clone();
@@ -336,7 +337,7 @@ fn pill(entry: &ToastEntry, toasts: &Entity<Toasts>, cx: &App) -> gpui::Stateful
.hover(|style| style.bg(muted))
.child(
icon(IconRole::Close)
.size(icon_size * 0.75)
.size(dense_icon_size)
.color(foreground.alpha(0.55)),
)
.on_click(move |_, _window, cx| {
+1 -1
View File
@@ -100,7 +100,7 @@ impl RenderOnce for Toggle {
.id(self.id)
.justify_center()
.h(metrics.control_height)
.px(metrics.padding_x * 0.75)
.px(metrics.dense_padding_x)
.rounded(theme.radius.control)
.text_size(metrics.text_size)
.text_color(colors.foreground)
+1 -1
View File
@@ -225,7 +225,7 @@ fn render_tab(
.when_some(tab.icon, |strip_tab, role| {
strip_tab.child(
icon(role)
.size(theme.sizes.medium.icon_size * 0.75)
.size(theme.sizes.medium.dense_icon_size)
.color(label_color),
)
}),
+1 -1
View File
@@ -327,7 +327,7 @@ fn render_disclosure(
} else {
IconRole::ChevronRight
})
.size(theme.sizes.medium.icon_size * 0.75)
.size(theme.sizes.medium.dense_icon_size)
.color(glyph_color),
);
+76 -12
View File
@@ -267,8 +267,20 @@ pub struct ControlMetrics {
pub text_size: Pixels,
/// Icons and on/off indicators.
pub icon_size: Pixels,
/// Icons inline with dense content — menu rows, badges, chevrons.
pub dense_icon_size: Pixels,
/// Selection indicators — checkboxes, radios, and switch knobs.
pub indicator_size: Pixels,
/// Avatars.
pub avatar_size: Pixels,
/// Dense label text.
pub caption_size: Pixels,
/// Tight horizontal padding inside pills, segments, and fields.
pub dense_padding_x: Pixels,
/// The inset between a control's edge and a raised inner plate.
pub plate_inset: Pixels,
/// The band height of multi-segment strips.
pub strip_height: Pixels,
}
/// The control metrics for every size class.
@@ -280,20 +292,72 @@ pub struct SizeTable {
}
impl SizeTable {
/// Every metric derives from the root font size; components fetch, and
/// never scale these themselves.
pub fn with_root_font_size(rem_size: Pixels) -> Self {
let class = |height: f32, padding: f32, text: f32, icon: f32, avatar: f32| ControlMetrics {
control_height: rem_size * height,
input_height: rem_size * (height + 0.125),
padding_x: rem_size * padding,
text_size: rem_size * text,
icon_size: rem_size * icon,
avatar_size: rem_size * avatar,
};
Self {
small: class(1.5, 0.75, 0.85, 0.875, 1.5),
medium: class(1.75, 1.0, 1.0, 1.0, 2.0),
large: class(2.25, 1.25, 1.15, 1.25, 3.0),
// One row per size class: control height, padding, label text, icon,
// avatar, strip band, and plate inset — all as root-size multiples.
struct ClassFactors {
height: f32,
padding: f32,
text: f32,
icon: f32,
avatar: f32,
strip: f32,
inset: f32,
}
let small = ClassFactors {
height: 1.5,
padding: 0.75,
text: 0.85,
icon: 0.875,
avatar: 1.5,
strip: 1.5,
inset: 0.125,
};
let medium = ClassFactors {
height: 1.75,
padding: 1.0,
text: 1.0,
icon: 1.0,
avatar: 2.0,
strip: 2.0,
inset: 0.25,
};
let large = ClassFactors {
height: 2.25,
padding: 1.25,
text: 1.15,
icon: 1.25,
avatar: 3.0,
strip: 3.0,
inset: 0.375,
};
let metrics = |f: &ClassFactors| ControlMetrics {
control_height: rem_size * f.height,
input_height: rem_size * (f.height + 0.125),
padding_x: rem_size * f.padding,
text_size: rem_size * f.text,
icon_size: rem_size * f.icon,
dense_icon_size: rem_size * f.icon * 0.75,
indicator_size: rem_size * f.height * 0.7,
avatar_size: rem_size * f.avatar,
caption_size: rem_size * f.text,
dense_padding_x: rem_size * f.padding * 0.75,
plate_inset: rem_size * f.inset,
strip_height: rem_size * f.strip,
};
let mut table = Self {
small: metrics(&small),
medium: metrics(&medium),
large: metrics(&large),
};
// Dense text stops growing with the large class, and small's is
// one step below its label.
table.small.caption_size = table.small.text_size * 0.85;
table.large.caption_size = table.medium.text_size;
table
}
/// The metrics for `size`.
+6
View File
@@ -292,6 +292,12 @@ mod tests {
assert_eq!(resolved.sizes.medium.input_height, px(30.));
assert_eq!(resolved.sizes.medium.text_size, px(16.));
assert_eq!(resolved.sizes.large.avatar_size, px(48.));
assert_eq!(resolved.sizes.small.caption_size, px(13.6 * 0.85));
assert_eq!(resolved.sizes.medium.caption_size, px(16.));
assert_eq!(resolved.sizes.large.caption_size, px(16.));
assert_eq!(resolved.sizes.medium.dense_padding_x, px(16. * 0.75));
assert_eq!(resolved.sizes.medium.plate_inset, px(4.));
assert_eq!(resolved.sizes.medium.strip_height, px(32.));
}
}