Skip to main content

fret_ui_kit/
sizing.rs

1use fret_core::{FontId, Px, TextStyle};
2use fret_ui::Theme;
3
4/// Component sizing vocabulary inspired by Tailwind/shadcn and gpui-component.
5///
6/// This is intentionally a component-ecosystem concept (not a `fret-ui` contract).
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum Size {
9    XSmall,
10    Small,
11    #[default]
12    Medium,
13    Large,
14}
15
16impl Size {
17    pub fn as_str(self) -> &'static str {
18        match self {
19            Self::XSmall => "xs",
20            Self::Small => "sm",
21            Self::Medium => "md",
22            Self::Large => "lg",
23        }
24    }
25
26    fn metric(self, theme: &Theme, suffix: &'static str, fallback: Px) -> Px {
27        let key = match (self, suffix) {
28            (Self::XSmall, "control.text_px") => "component.size.xs.control.text_px",
29            (Self::Small, "control.text_px") => "component.size.sm.control.text_px",
30            (Self::Medium, "control.text_px") => "component.size.md.control.text_px",
31            (Self::Large, "control.text_px") => "component.size.lg.control.text_px",
32
33            (Self::XSmall, "control.radius") => "component.size.xs.control.radius",
34            (Self::Small, "control.radius") => "component.size.sm.control.radius",
35            (Self::Medium, "control.radius") => "component.size.md.control.radius",
36            (Self::Large, "control.radius") => "component.size.lg.control.radius",
37
38            (Self::XSmall, "input.px") => "component.size.xs.input.px",
39            (Self::Small, "input.px") => "component.size.sm.input.px",
40            (Self::Medium, "input.px") => "component.size.md.input.px",
41            (Self::Large, "input.px") => "component.size.lg.input.px",
42
43            (Self::XSmall, "input.py") => "component.size.xs.input.py",
44            (Self::Small, "input.py") => "component.size.sm.input.py",
45            (Self::Medium, "input.py") => "component.size.md.input.py",
46            (Self::Large, "input.py") => "component.size.lg.input.py",
47
48            (Self::XSmall, "input.h") => "component.size.xs.input.h",
49            (Self::Small, "input.h") => "component.size.sm.input.h",
50            (Self::Medium, "input.h") => "component.size.md.input.h",
51            (Self::Large, "input.h") => "component.size.lg.input.h",
52
53            (Self::XSmall, "button.px") => "component.size.xs.button.px",
54            (Self::Small, "button.px") => "component.size.sm.button.px",
55            (Self::Medium, "button.px") => "component.size.md.button.px",
56            (Self::Large, "button.px") => "component.size.lg.button.px",
57
58            (Self::XSmall, "button.py") => "component.size.xs.button.py",
59            (Self::Small, "button.py") => "component.size.sm.button.py",
60            (Self::Medium, "button.py") => "component.size.md.button.py",
61            (Self::Large, "button.py") => "component.size.lg.button.py",
62
63            (Self::XSmall, "button.h") => "component.size.xs.button.h",
64            (Self::Small, "button.h") => "component.size.sm.button.h",
65            (Self::Medium, "button.h") => "component.size.md.button.h",
66            (Self::Large, "button.h") => "component.size.lg.button.h",
67
68            (Self::XSmall, "icon_button.size") => "component.size.xs.icon_button.size",
69            (Self::Small, "icon_button.size") => "component.size.sm.icon_button.size",
70            (Self::Medium, "icon_button.size") => "component.size.md.icon_button.size",
71            (Self::Large, "icon_button.size") => "component.size.lg.icon_button.size",
72
73            (Self::XSmall, "list.px") => "component.size.xs.list.px",
74            (Self::Small, "list.px") => "component.size.sm.list.px",
75            (Self::Medium, "list.px") => "component.size.md.list.px",
76            (Self::Large, "list.px") => "component.size.lg.list.px",
77
78            (Self::XSmall, "list.py") => "component.size.xs.list.py",
79            (Self::Small, "list.py") => "component.size.sm.list.py",
80            (Self::Medium, "list.py") => "component.size.md.list.py",
81            (Self::Large, "list.py") => "component.size.lg.list.py",
82
83            (Self::XSmall, "list.row_h") => "component.size.xs.list.row_h",
84            (Self::Small, "list.row_h") => "component.size.sm.list.row_h",
85            (Self::Medium, "list.row_h") => "component.size.md.list.row_h",
86            (Self::Large, "list.row_h") => "component.size.lg.list.row_h",
87
88            _ => return fallback,
89        };
90
91        theme.metric_by_key(key).unwrap_or(fallback)
92    }
93
94    pub fn control_text_px(self, theme: &Theme) -> Px {
95        let base = theme
96            .metric_by_key("font.size")
97            .unwrap_or_else(|| theme.metric_token("font.size"));
98        let fallback = match self {
99            // Keep the current defaults when `metric.font.size == 13px`,
100            // while allowing themes to scale typography globally.
101            Self::XSmall => base - Px(1.0),
102            Self::Small => base,
103            Self::Medium => base,
104            Self::Large => base + Px(1.0),
105        };
106        self.metric(theme, "control.text_px", fallback)
107    }
108
109    pub fn control_text_style(self, theme: &Theme) -> TextStyle {
110        crate::typography::control_text_style_scaled(
111            theme,
112            FontId::ui(),
113            self.control_text_px(theme),
114        )
115    }
116
117    pub fn control_radius(self, theme: &Theme) -> Px {
118        self.metric(
119            theme,
120            "control.radius",
121            match self {
122                Self::XSmall => theme.metric_token("metric.radius.sm"),
123                Self::Small => theme.metric_token("metric.radius.sm"),
124                Self::Medium => theme.metric_token("metric.radius.md"),
125                Self::Large => theme.metric_token("metric.radius.md"),
126            },
127        )
128    }
129
130    pub fn input_px(self, theme: &Theme) -> Px {
131        self.metric(
132            theme,
133            "input.px",
134            match self {
135                Self::XSmall => Px(8.0),
136                Self::Small => Px(10.0),
137                Self::Medium => Px(12.0),
138                Self::Large => Px(14.0),
139            },
140        )
141    }
142
143    pub fn input_py(self, theme: &Theme) -> Px {
144        self.metric(
145            theme,
146            "input.py",
147            match self {
148                Self::XSmall => Px(4.0),
149                Self::Small => Px(5.0),
150                Self::Medium => Px(6.0),
151                Self::Large => Px(7.0),
152            },
153        )
154    }
155
156    pub fn input_h(self, theme: &Theme) -> Px {
157        self.metric(
158            theme,
159            "input.h",
160            match self {
161                Self::XSmall => Px(24.0),
162                Self::Small => Px(28.0),
163                Self::Medium => Px(32.0),
164                Self::Large => Px(36.0),
165            },
166        )
167    }
168
169    pub fn button_px(self, theme: &Theme) -> Px {
170        self.metric(
171            theme,
172            "button.px",
173            match self {
174                Self::XSmall => Px(8.0),
175                Self::Small => Px(10.0),
176                Self::Medium => Px(12.0),
177                Self::Large => Px(14.0),
178            },
179        )
180    }
181
182    pub fn button_py(self, theme: &Theme) -> Px {
183        self.metric(
184            theme,
185            "button.py",
186            match self {
187                Self::XSmall => Px(4.0),
188                Self::Small => Px(5.0),
189                Self::Medium => Px(6.0),
190                Self::Large => Px(7.0),
191            },
192        )
193    }
194
195    pub fn button_h(self, theme: &Theme) -> Px {
196        self.metric(
197            theme,
198            "button.h",
199            match self {
200                Self::XSmall => Px(24.0),
201                Self::Small => Px(28.0),
202                Self::Medium => Px(32.0),
203                Self::Large => Px(36.0),
204            },
205        )
206    }
207
208    pub fn icon_button_size(self, theme: &Theme) -> Px {
209        self.metric(
210            theme,
211            "icon_button.size",
212            match self {
213                Self::XSmall => Px(24.0),
214                Self::Small => Px(28.0),
215                Self::Medium => Px(32.0),
216                Self::Large => Px(36.0),
217            },
218        )
219    }
220
221    pub fn list_px(self, theme: &Theme) -> Px {
222        self.metric(
223            theme,
224            "list.px",
225            match self {
226                // Align with the Tailwind-like scales used by gpui-component:
227                // - `px-2` for dense lists,
228                // - `px-3` for default/comfortable lists.
229                Self::XSmall => Px(8.0),
230                Self::Small => Px(8.0),
231                Self::Medium => Px(12.0),
232                Self::Large => Px(12.0),
233            },
234        )
235    }
236
237    pub fn list_py(self, theme: &Theme) -> Px {
238        self.metric(
239            theme,
240            "list.py",
241            match self {
242                // Align with gpui-component list defaults (py-0.5/py-1/py-2).
243                Self::XSmall => Px(2.0),
244                Self::Small => Px(2.0),
245                Self::Medium => Px(4.0),
246                Self::Large => Px(8.0),
247            },
248        )
249    }
250
251    pub fn list_row_h(self, theme: &Theme) -> Px {
252        self.metric(
253            theme,
254            "list.row_h",
255            match self {
256                Self::XSmall => Px(24.0),
257                Self::Small => Px(28.0),
258                Self::Medium => Px(32.0),
259                Self::Large => Px(36.0),
260            },
261        )
262    }
263}
264
265/// Shared component API for size configuration.
266pub trait Sizable: Sized {
267    fn with_size(self, size: Size) -> Self;
268
269    fn xsmall(self) -> Self {
270        self.with_size(Size::XSmall)
271    }
272
273    fn small(self) -> Self {
274        self.with_size(Size::Small)
275    }
276
277    fn medium(self) -> Self {
278        self.with_size(Size::Medium)
279    }
280
281    fn large(self) -> Self {
282        self.with_size(Size::Large)
283    }
284}