add sizable trait

This commit is contained in:
2026-10-29 07:56:23 +07:00
parent 3808b1519f
commit 1b29c2c940
19 changed files with 442 additions and 69 deletions
+19 -5
View File
@@ -6,9 +6,9 @@ use gpui::{
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _,
Window, div,
};
use gpui_native_theme::{ActiveTheme as _, Oklch, ThemeVariant};
use gpui_native_theme::{ActiveTheme as _, Oklch, Size, ThemeVariant};
use crate::common::h_flex;
use crate::common::{Sizable, h_flex};
/// Number of rows and columns in the pixel grid.
const GRID_SIZE: usize = 8;
@@ -27,6 +27,7 @@ pub struct Avatar {
id: SharedString,
name: Option<SharedString>,
picture: Option<SharedString>,
size: Size,
style: StyleRefinement,
}
@@ -42,6 +43,7 @@ impl Avatar {
id: id.to_string().into(),
name: None,
picture: None,
size: Size::default(),
style: StyleRefinement::default(),
}
}
@@ -65,6 +67,12 @@ impl Avatar {
}
}
impl Sizable for Avatar {
fn apply_size(&mut self, size: Size) {
self.size = size;
}
}
impl gpui::Styled for Avatar {
fn style(&mut self) -> &mut gpui::StyleRefinement {
&mut self.style
@@ -75,11 +83,15 @@ impl RenderOnce for Avatar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let radius = theme.radius.control;
let side = theme.sizes.resolve(self.size).avatar_size;
let mut base = h_flex()
.id(ElementId::from(self.id.clone()))
.flex_none()
.justify_center()
.overflow_hidden()
.size(side)
.text_size(side * 0.44)
.bg(theme.colors.secondary)
.rounded(radius)
.when_some(
@@ -91,13 +103,14 @@ impl RenderOnce for Avatar {
base.style().refine(&self.style);
match self.picture {
Some(url) => base.size_8().child(
Some(url) => base.child(
gpui::img(gpui::SharedUri::from(url))
.size_full()
.rounded(radius),
),
None => base.size_8().child(PixelAvatar {
None => base.child(PixelAvatar {
seed: fnv1a(self.id.as_bytes()),
side,
style: self.style,
}),
}
@@ -109,6 +122,7 @@ impl RenderOnce for Avatar {
#[derive(IntoElement)]
struct PixelAvatar {
seed: u64,
side: gpui::Pixels,
style: StyleRefinement,
}
@@ -164,7 +178,7 @@ impl RenderOnce for PixelAvatar {
.grid()
.grid_cols(GRID_SIZE as u16)
.grid_rows(GRID_SIZE as u16)
.size_8()
.size(self.side)
.flex_none()
.rounded(radius)
.overflow_hidden()
+21 -7
View File
@@ -4,12 +4,12 @@ use gpui::prelude::FluentBuilder as _;
use gpui::{
App, BoxShadow, ClickEvent, ElementId, InteractiveElement as _, IntoElement,
ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
Styled as _, Window, px,
Styled as _, Window, px, relative,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::h_flex;
use crate::common::{Sizable, h_flex};
/// The single activation handler, shared by pointer, Enter, and Space.
type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
@@ -35,6 +35,7 @@ pub struct Button {
label: Option<SharedString>,
icon: Option<IconRole>,
variant: ButtonVariant,
size: Size,
disabled: bool,
a11y_label: Option<SharedString>,
on_click: Option<ClickHandler>,
@@ -51,6 +52,7 @@ impl Button {
label: None,
icon: None,
variant: ButtonVariant::default(),
size: Size::default(),
disabled: false,
a11y_label: None,
on_click: None,
@@ -103,11 +105,18 @@ impl Button {
}
}
impl Sizable for Button {
fn apply_size(&mut self, size: Size) {
self.size = size;
}
}
impl RenderOnce for Button {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors;
let ramps = theme.ramps;
let metrics = theme.sizes.resolve(self.size);
let on_click = self.on_click;
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
@@ -143,11 +152,12 @@ impl RenderOnce for Button {
.id(self.id)
.role(Role::Button)
.justify_center()
.h_7()
.px_4()
.h(metrics.control_height)
.px(metrics.padding_x)
.line_height(relative(1.0))
.gap_1p5()
.rounded(theme.radius.control)
.text_sm()
.text_size(metrics.text_size)
.when(self.variant == ButtonVariant::Secondary, |button| {
button.border_1().border_color(colors.border)
})
@@ -155,7 +165,11 @@ impl RenderOnce for Button {
.text_color(foreground)
.when_some(a11y_label, |button, label| button.aria_label(label))
.when_some(self.icon, |button, role| {
button.child(icon(role).color(foreground))
button.child(
icon(role)
.size(f32::from(metrics.icon_size))
.color(foreground),
)
})
.when_some(self.label, |button, label| button.child(label))
.when(self.disabled, |button| button.opacity(0.5))
+15 -6
View File
@@ -7,19 +7,20 @@ use gpui::{
px,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::h_flex;
use crate::common::{Sizable, h_flex};
/// The change reporter, fired with the next value by pointer, Enter, and Space.
type ChangeHandler = Box<dyn Fn(bool, &mut Window, &mut App)>;
/// Checkbox component.
/// A controlled checkbox component.
#[derive(IntoElement)]
pub struct Checkbox {
id: ElementId,
label: Option<SharedString>,
checked: bool,
size: Size,
disabled: bool,
a11y_label: Option<SharedString>,
on_change: Option<ChangeHandler>,
@@ -35,6 +36,7 @@ impl Checkbox {
id: id.into(),
label: None,
checked: false,
size: Size::default(),
disabled: false,
a11y_label: None,
on_change: None,
@@ -81,20 +83,27 @@ impl Checkbox {
}
}
impl Sizable for Checkbox {
fn apply_size(&mut self, size: Size) {
self.size = size;
}
}
impl RenderOnce for Checkbox {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors;
let ramps = theme.ramps;
let metrics = theme.sizes.resolve(self.size);
let on_change = self.on_change;
let next = !self.checked;
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
h_flex()
.id(self.id)
.role(Role::CheckBox)
.gap_1p5()
.text_sm()
.text_size(metrics.text_size)
.role(Role::CheckBox)
.when_some(a11y_label, |checkbox, label| checkbox.aria_label(label))
.aria_toggled(Toggled::from(self.checked))
.when(self.disabled, |checkbox| checkbox.opacity(0.5))
@@ -118,7 +127,7 @@ impl RenderOnce for Checkbox {
h_flex()
.justify_center()
.flex_none()
.size_4()
.size(metrics.icon_size)
.rounded(theme.radius.sm)
.border_1()
.when(self.checked, |indicator| {
+30
View File
@@ -2,6 +2,8 @@
use gpui::{Div, Styled as _, div};
pub use gpui_native_theme::Size;
/// A row whose children center on the cross axis.
pub fn h_flex() -> Div {
div().flex().flex_row().items_center()
@@ -11,3 +13,31 @@ pub fn h_flex() -> Div {
pub fn v_flex() -> Div {
div().flex().flex_col()
}
/// A component that can be told which size to render at.
pub trait Sizable: Sized {
/// Sets the size.
fn with_size(mut self, size: Size) -> Self {
self.apply_size(size);
self
}
/// Applies the size to the component's fields.
fn apply_size(&mut self, size: Size);
/// Sets `Size::Small`.
fn small(self) -> Self
where
Self: Sized,
{
self.with_size(Size::Small)
}
/// Sets `Size::Large`.
fn large(self) -> Self
where
Self: Sized,
{
self.with_size(Size::Large)
}
}
+17 -8
View File
@@ -12,9 +12,9 @@ use gpui::{
SharedString, StatefulInteractiveElement as _, Styled as _, TextAlign, TextRun, UTF16Selection,
Window, canvas, div, fill, point, px,
};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::h_flex;
use crate::common::{Sizable, h_flex};
/// Input layout information for hit testing and caret positioning.
struct InputLayout {
@@ -374,6 +374,7 @@ pub struct Input {
id: ElementId,
state: Entity<InputState>,
placeholder: SharedString,
size: Size,
disabled: bool,
a11y_label: Option<SharedString>,
}
@@ -388,6 +389,7 @@ impl Input {
id: id.into(),
state,
placeholder: SharedString::default(),
size: Size::default(),
disabled: false,
a11y_label: None,
}
@@ -418,11 +420,18 @@ impl Input {
}
}
impl Sizable for Input {
fn apply_size(&mut self, size: Size) {
self.size = size;
}
}
impl RenderOnce for Input {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors;
let radius = theme.radius;
let metrics = theme.sizes.resolve(self.size);
let focus_handle = self.state.read(cx).focus_handle.clone();
let layout_cell = self.state.read(cx).layout.clone();
@@ -447,19 +456,19 @@ impl RenderOnce for Input {
h_flex()
.id(self.id.clone())
.role(Role::TextInput)
.aria_label(fallback_label)
.relative()
.overflow_hidden()
.w_full()
.h_7()
.px_2()
.h(metrics.input_height)
.px(metrics.padding_x / 2.0)
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.bg(colors.input)
.text_sm()
.text_size(metrics.text_size)
.text_color(colors.foreground)
.role(Role::TextInput)
.aria_label(fallback_label)
.when(self.disabled, |input| input.opacity(0.5))
.when(!self.disabled, |input| {
input.track_focus(&focus_handle).tab_stop(true)
@@ -506,6 +515,7 @@ impl RenderOnce for Input {
let layout_cell = layout_cell.clone();
let focus = focus_handle.clone();
let dragging = paint_dragging;
let padding = metrics.padding_x / 2.0;
let disabled = self.disabled;
// `line` comes from the prepaint closure above.
move |bounds, line, window, cx| {
@@ -519,7 +529,6 @@ impl RenderOnce for Input {
)
};
let padding = px(8.);
let selection = state.read(cx).selection();
let caret_x = line.x_for_index(selection.end);
+13 -3
View File
@@ -4,9 +4,9 @@ use gpui::{
ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
Styled as _, Window, px,
};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::h_flex;
use crate::common::{Sizable, h_flex};
/// The click handler. When set it replaces the default `href` opening.
type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
@@ -17,6 +17,7 @@ pub struct Link {
id: ElementId,
label: Option<SharedString>,
href: Option<SharedString>,
size: Size,
disabled: bool,
a11y_label: Option<SharedString>,
on_click: Option<ClickHandler>,
@@ -32,6 +33,7 @@ impl Link {
id: id.into(),
label: None,
href: None,
size: Size::default(),
disabled: false,
a11y_label: None,
on_click: None,
@@ -81,18 +83,26 @@ impl Link {
}
}
impl Sizable for Link {
fn apply_size(&mut self, size: Size) {
self.size = size;
}
}
impl RenderOnce for Link {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors;
let ramps = theme.ramps;
let metrics = theme.sizes.resolve(self.size);
let on_click = self.on_click;
let href = self.href;
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
h_flex()
.id(self.id)
.text_sm()
.text_size(metrics.text_size)
.text_color(colors.accent)
.underline()
.role(Role::Link)
+15 -2
View File
@@ -8,7 +8,9 @@ use gpui::{
ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
Styled as _, Window, div, relative,
};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::Sizable;
/// A progress bar. With a `value` in `0..=1` it paints a determinate fill.
#[derive(IntoElement)]
@@ -16,6 +18,7 @@ pub struct Progress {
id: ElementId,
value: f32,
indeterminate: bool,
size: Size,
a11y_label: Option<SharedString>,
}
@@ -29,6 +32,7 @@ impl Progress {
id: id.into(),
value,
indeterminate: false,
size: Size::default(),
a11y_label: None,
}
}
@@ -49,12 +53,21 @@ impl Progress {
}
}
impl Sizable for Progress {
fn apply_size(&mut self, size: Size) {
self.size = size;
}
}
impl RenderOnce for Progress {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors;
let ramps = theme.ramps;
let radius = theme.radius;
let metrics = theme.sizes.resolve(self.size);
let track_height = metrics.icon_size / 4.0;
let value = self.value.clamp(0.0, 1.0);
div()
@@ -73,7 +86,7 @@ impl RenderOnce for Progress {
.child(
div()
.w_full()
.h_1p5()
.h(track_height)
.rounded(radius.full)
.bg(colors.border)
.child(if self.indeterminate {
+17 -5
View File
@@ -4,9 +4,9 @@ use gpui::{
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Toggled, Window,
div, px,
};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::{h_flex, v_flex};
use crate::common::{Sizable, h_flex, v_flex};
/// The group's change reporter, fired with the clicked radio's value.
type ChangeHandler<V> = std::rc::Rc<dyn Fn(V, &mut Window, &mut App)>;
@@ -72,6 +72,7 @@ where
id: ElementId,
selected: Option<V>,
children: Vec<Radio<V>>,
size: Size,
a11y_label: Option<SharedString>,
on_change: Option<ChangeHandler<V>>,
}
@@ -89,6 +90,7 @@ where
id: id.into(),
selected: None,
children: Vec::new(),
size: Size::default(),
a11y_label: None,
on_change: None,
}
@@ -125,6 +127,15 @@ where
}
}
impl<V> Sizable for RadioGroup<V>
where
V: Clone + PartialEq + 'static,
{
fn apply_size(&mut self, size: Size) {
self.size = size;
}
}
impl<V> RenderOnce for RadioGroup<V>
where
V: Clone + PartialEq + 'static,
@@ -134,6 +145,7 @@ where
let colors = theme.colors;
let ramps = theme.ramps;
let radius = theme.radius;
let metrics = theme.sizes.resolve(self.size);
let selected = self.selected;
let on_change = self.on_change;
let group_label = self.a11y_label;
@@ -141,7 +153,7 @@ where
v_flex()
.id(self.id)
.gap_2()
.text_sm()
.text_size(metrics.text_size)
.role(Role::RadioGroup)
.when_some(group_label.clone(), |group, label| group.aria_label(label))
.children(self.children.into_iter().map(move |radio| {
@@ -182,7 +194,7 @@ where
h_flex()
.justify_center()
.flex_none()
.size_4()
.size(metrics.icon_size)
.rounded(radius.full)
.border_1()
.when(is_selected, |indicator| {
@@ -192,7 +204,7 @@ where
.hover(|style| style.bg(ramps.primary.hover))
.child(
div()
.size_2()
.size(metrics.icon_size / 2.0)
.rounded(radius.full)
.bg(colors.primary_foreground),
)
+23 -10
View File
@@ -6,11 +6,11 @@ use gpui::{
App, Bounds, BoxShadow, DispatchPhase, ElementId, InteractiveElement as _, IntoElement,
KeyDownEvent, MouseButton, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels,
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas,
div, px, relative, rems,
div, px, relative,
};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::h_flex;
use crate::common::{Sizable, h_flex};
/// The change reporter, fired with the next value by click, drag, and arrows.
type ChangeHandler = Rc<dyn Fn(f32, &mut Window, &mut App)>;
@@ -36,6 +36,7 @@ pub struct Slider {
max: f32,
step: f32,
value: f32,
size: Size,
disabled: bool,
a11y_label: Option<SharedString>,
on_change: Option<ChangeHandler>,
@@ -53,6 +54,7 @@ impl Slider {
max: 1.0,
step: 0.0,
value,
size: Size::default(),
disabled: false,
a11y_label: None,
on_change: None,
@@ -108,12 +110,23 @@ impl Slider {
}
}
impl Sizable for Slider {
fn apply_size(&mut self, size: Size) {
self.size = size;
}
}
impl RenderOnce for Slider {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors;
let ramps = theme.ramps;
let radius = theme.radius;
let metrics = theme.sizes.resolve(self.size);
let row_height = metrics.icon_size;
let track_height = metrics.icon_size / 4.0;
let knob = metrics.icon_size * 1.25;
let value = self.value.clamp(self.min, self.max);
let fraction = if self.max > self.min {
@@ -162,7 +175,7 @@ impl RenderOnce for Slider {
.id(self.id)
.relative()
.w_full()
.h_5()
.h(row_height)
.role(Role::Slider)
.when_some(self.a11y_label.clone(), |slider, label| {
slider.aria_label(label)
@@ -251,8 +264,8 @@ impl RenderOnce for Slider {
div()
.absolute()
.left_0()
.top_2()
.h_1()
.top((row_height - track_height) / 2.0)
.h(track_height)
.w_full()
.rounded(radius.full)
.bg(colors.border),
@@ -261,8 +274,8 @@ impl RenderOnce for Slider {
div()
.absolute()
.left_0()
.top_2()
.h_1()
.top((row_height - track_height) / 2.0)
.h(track_height)
.w(relative(fraction))
.rounded(radius.full)
.bg(ramps.primary.base),
@@ -278,13 +291,13 @@ impl RenderOnce for Slider {
.items_center()
.child(
div()
.size_5()
.size(knob)
.flex_none()
.rounded(radius.full)
.bg(colors.primary_foreground)
.border_1()
.border_color(colors.border)
.mr(rems(-0.625)),
.mr(-knob / 2.0),
),
)
}
+24 -8
View File
@@ -6,9 +6,9 @@ use gpui::{
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Toggled, Window,
div, px,
};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::h_flex;
use crate::common::{Sizable, h_flex};
/// The change reporter, fired with the next value by pointer, Enter, and Space.
type ChangeHandler = Box<dyn Fn(bool, &mut Window, &mut App)>;
@@ -19,6 +19,7 @@ pub struct Switch {
id: ElementId,
label: Option<SharedString>,
checked: bool,
size: Size,
disabled: bool,
a11y_label: Option<SharedString>,
on_change: Option<ChangeHandler>,
@@ -34,6 +35,7 @@ impl Switch {
id: id.into(),
label: None,
checked: false,
size: Size::default(),
disabled: false,
a11y_label: None,
on_change: None,
@@ -80,19 +82,33 @@ impl Switch {
}
}
impl Sizable for Switch {
fn apply_size(&mut self, size: Size) {
self.size = size;
}
}
impl RenderOnce for Switch {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors;
let ramps = theme.ramps;
let on_change = self.on_change;
let metrics = theme.sizes.resolve(self.size);
let track_height = metrics.icon_size * 1.25;
let track_width = metrics.icon_size * 2.25;
let knob = metrics.icon_size;
let inset = metrics.icon_size * 0.125;
let next = !self.checked;
let on_change = self.on_change;
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
h_flex()
.id(self.id)
.gap_1p5()
.text_sm()
.text_size(metrics.text_size)
.role(Role::Switch)
.when_some(a11y_label, |switch, label| switch.aria_label(label))
.aria_toggled(Toggled::from(self.checked))
@@ -114,9 +130,9 @@ impl RenderOnce for Switch {
.child(
h_flex()
.flex_none()
.w_9()
.h_5()
.p_0p5()
.w(track_width)
.h(track_height)
.p(inset)
.rounded(theme.radius.full)
.when(self.checked, |track| {
track
@@ -129,7 +145,7 @@ impl RenderOnce for Switch {
})
.child(
div()
.size_4()
.size(knob)
.flex_none()
.rounded(theme.radius.full)
.bg(colors.primary_foreground)
+14 -5
View File
@@ -4,9 +4,9 @@ use gpui::{
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Toggled, Window,
px,
};
use gpui_native_theme::ActiveTheme as _;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::common::h_flex;
use crate::common::{Sizable, h_flex};
/// The change handler, fired with the next pressed state.
type ChangeHandler = Box<dyn Fn(bool, &mut Window, &mut App)>;
@@ -17,6 +17,7 @@ pub struct Toggle {
id: ElementId,
label: Option<SharedString>,
pressed: bool,
size: Size,
disabled: bool,
a11y_label: Option<SharedString>,
on_change: Option<ChangeHandler>,
@@ -32,6 +33,7 @@ impl Toggle {
id: id.into(),
label: None,
pressed: false,
size: Size::default(),
disabled: false,
a11y_label: None,
on_change: None,
@@ -78,11 +80,18 @@ impl Toggle {
}
}
impl Sizable for Toggle {
fn apply_size(&mut self, size: Size) {
self.size = size;
}
}
impl RenderOnce for Toggle {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors;
let ramps = theme.ramps;
let metrics = theme.sizes.resolve(self.size);
let on_change = self.on_change;
let next = !self.pressed;
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
@@ -90,10 +99,10 @@ impl RenderOnce for Toggle {
h_flex()
.id(self.id)
.justify_center()
.h_7()
.px_3()
.h(metrics.control_height)
.px(metrics.padding_x * 0.75)
.rounded(theme.radius.control)
.text_sm()
.text_size(metrics.text_size)
.text_color(colors.foreground)
.role(Role::Button)
.when_some(a11y_label, |toggle, label| toggle.aria_label(label))