Skip to main content

tui_lipan/widgets/button/
mod.rs

1//! Button widget.
2
3mod layout;
4mod node;
5mod reconcile;
6
7pub use layout::measure_button;
8pub use node::ButtonNode;
9pub use reconcile::reconcile_button;
10
11use std::sync::Arc;
12
13use crate::callback::{Callback, KeyHandler};
14use crate::core::element::{Element, ElementKind};
15use crate::core::event::MouseEvent;
16use crate::input::KeyBindings;
17use crate::style::{Align, BorderStyle, LayoutConstraints, Length, Padding, Style, StyleSlot};
18
19/// Visual variant for a [`Button`].
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
21pub enum ButtonVariant {
22    /// Rendered as `[ Label ]`.
23    #[default]
24    Bracket,
25    /// Background-filled button (no brackets or border).
26    Filled,
27    /// Border-only button (no background fill).
28    Outlined,
29}
30
31/// A button element.
32#[derive(Clone)]
33pub struct Button {
34    /// Button label.
35    pub label: Arc<str>,
36    /// Optional icon displayed before the label.
37    pub icon: Option<Arc<str>>,
38    /// Style applied to the icon.
39    pub icon_style: Style,
40    /// Gap between icon and label.
41    pub icon_gap: u16,
42    /// Optional shortcut hint displayed after the label.
43    pub shortcut: Option<Arc<str>>,
44    /// Style applied to the shortcut hint.
45    pub shortcut_style: Style,
46    /// Gap between label and shortcut.
47    pub shortcut_gap: u16,
48    /// Base style.
49    pub style: Style,
50    /// Style applied when the button is hovered.
51    pub hover_style: StyleSlot,
52    /// Style applied when the button is focused.
53    pub focus_style: StyleSlot,
54    /// Label alignment inside the allocated rect.
55    /// Default: `Align::Center`.
56    pub align: Align,
57    /// Requested width.
58    /// Default: `Length::Auto`.
59    pub width: Length,
60    /// Requested height.
61    /// Default: `Length::Auto`.
62    pub height: Length,
63    /// Visual variant.
64    pub variant: ButtonVariant,
65    /// Border style used by `ButtonVariant::Outlined`.
66    /// Default: `BorderStyle::Plain`.
67    pub border_style: BorderStyle,
68    /// Optional border style override when hovered.
69    pub hover_border_style: Option<BorderStyle>,
70    /// Optional border style override when focused.
71    pub focus_border_style: Option<BorderStyle>,
72    /// Padding inside the button.
73    /// Default: `Padding { left: 1, right: 1, top: 0, bottom: 0 }`.
74    pub padding: Padding,
75    /// Whether the button is disabled.
76    pub disabled: bool,
77    /// Style applied when disabled.
78    pub disabled_style: Style,
79    /// Activation handler for mouse clicks and focused plain Enter/Space.
80    ///
81    /// Keyboard activation emits a synthetic left-button mouse-up event at the
82    /// button rect center. A custom [`Button::on_key`] handler runs first and
83    /// can consume the key by returning `true`.
84    pub on_click: Option<Callback<MouseEvent>>,
85    /// Keyboard handler (only for focused node), called before default activation.
86    pub on_key: Option<KeyHandler>,
87    /// Whether the button participates in focus traversal.
88    pub focusable: bool,
89    /// Whether the button participates in tab traversal when focusable.
90    pub tab_stop: bool,
91    /// Callback fired when the button gains focus.
92    pub on_focus: Option<Callback<()>>,
93    /// Callback fired when the button loses focus.
94    pub on_blur: Option<Callback<()>>,
95}
96
97impl Button {
98    /// Create a new bracket button (`[ Label ]`).
99    pub fn new(label: impl Into<Arc<str>>) -> Self {
100        Self {
101            label: label.into(),
102            icon: None,
103            icon_style: Style::default(),
104            icon_gap: 1,
105            shortcut: None,
106            shortcut_style: Style::default(),
107            shortcut_gap: 1,
108            style: Style::default(),
109            hover_style: StyleSlot::Inherit,
110            focus_style: StyleSlot::Inherit,
111            align: Align::Center,
112            width: Length::Auto,
113            height: Length::Auto,
114            variant: ButtonVariant::Bracket,
115            border_style: BorderStyle::Plain,
116            hover_border_style: None,
117            focus_border_style: None,
118            padding: Padding {
119                left: 1,
120                right: 1,
121                top: 0,
122                bottom: 0,
123            },
124            disabled: false,
125            disabled_style: Style::default(),
126            on_click: None,
127            on_key: None,
128            focusable: true,
129            tab_stop: true,
130            on_focus: None,
131            on_blur: None,
132        }
133    }
134
135    /// Create a background-filled button.
136    pub fn filled(label: impl Into<Arc<str>>) -> Self {
137        let mut button = Self::new(label);
138        button.variant = ButtonVariant::Filled;
139        button
140    }
141
142    /// Create a border-only button.
143    pub fn outlined(label: impl Into<Arc<str>>) -> Self {
144        let mut button = Self::new(label);
145        button.variant = ButtonVariant::Outlined;
146        button.border_style = BorderStyle::Plain;
147        button.hover_border_style = None;
148        button.focus_border_style = None;
149        button
150    }
151
152    /// Set base style.
153    pub fn style(mut self, style: Style) -> Self {
154        self.style = style;
155        self
156    }
157
158    /// Set icon displayed before the label.
159    pub fn icon(mut self, icon: impl Into<Arc<str>>) -> Self {
160        self.icon = Some(icon.into());
161        self
162    }
163
164    /// Set icon style.
165    pub fn icon_style(mut self, style: Style) -> Self {
166        self.icon_style = style;
167        self
168    }
169
170    /// Set gap between icon and label.
171    pub fn icon_gap(mut self, gap: u16) -> Self {
172        self.icon_gap = gap;
173        self
174    }
175
176    /// Set shortcut hint displayed after the label.
177    pub fn shortcut(mut self, shortcut: impl Into<Arc<str>>) -> Self {
178        self.shortcut = Some(shortcut.into());
179        self
180    }
181
182    /// Set shortcut hint from parsed alternative bindings.
183    pub fn shortcut_bindings(mut self, bindings: KeyBindings) -> Self {
184        self.shortcut = Some(bindings.to_string().into());
185        self
186    }
187
188    /// Set shortcut style.
189    pub fn shortcut_style(mut self, style: Style) -> Self {
190        self.shortcut_style = style;
191        self
192    }
193
194    /// Set gap between label and shortcut.
195    pub fn shortcut_gap(mut self, gap: u16) -> Self {
196        self.shortcut_gap = gap;
197        self
198    }
199
200    /// Set hover style.
201    pub fn hover_style(mut self, style: Style) -> Self {
202        self.hover_style = StyleSlot::Replace(style);
203        self
204    }
205
206    /// Extend the active theme's hover style with additional fields.
207    pub fn extend_hover_style(mut self, style: Style) -> Self {
208        self.hover_style = StyleSlot::Extend(style);
209        self
210    }
211
212    /// Inherit hover style from the active theme.
213    pub fn inherit_hover_style(mut self) -> Self {
214        self.hover_style = StyleSlot::Inherit;
215        self
216    }
217
218    /// Set hover style slot directly for composite forwarding.
219    pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
220        self.hover_style = slot;
221        self
222    }
223
224    /// Set focus style.
225    pub fn focus_style(mut self, style: Style) -> Self {
226        self.focus_style = StyleSlot::Replace(style);
227        self
228    }
229
230    /// Extend the active theme's focus style with additional fields.
231    pub fn extend_focus_style(mut self, style: Style) -> Self {
232        self.focus_style = StyleSlot::Extend(style);
233        self
234    }
235
236    /// Inherit focus style from the active theme.
237    pub fn inherit_focus_style(mut self) -> Self {
238        self.focus_style = StyleSlot::Inherit;
239        self
240    }
241
242    /// Set focus style slot directly for composite forwarding.
243    pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
244        self.focus_style = slot;
245        self
246    }
247
248    /// Set label alignment.
249    pub fn align(mut self, align: Align) -> Self {
250        self.align = align;
251        self
252    }
253
254    /// Override requested width.
255    pub fn width(mut self, width: Length) -> Self {
256        self.width = width;
257        self
258    }
259
260    /// Override requested height.
261    pub fn height(mut self, height: Length) -> Self {
262        self.height = height;
263        self
264    }
265
266    /// Set visual variant.
267    pub fn variant(mut self, variant: ButtonVariant) -> Self {
268        self.variant = variant;
269        self
270    }
271
272    /// Set border style (only used by `ButtonVariant::Outlined`).
273    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
274        self.border_style = border_style;
275        self
276    }
277
278    /// Set hover border style override.
279    pub fn hover_border_style(mut self, border_style: Option<BorderStyle>) -> Self {
280        self.hover_border_style = border_style;
281        self
282    }
283
284    /// Set focus border style override.
285    pub fn focus_border_style(mut self, border_style: Option<BorderStyle>) -> Self {
286        self.focus_border_style = border_style;
287        self
288    }
289
290    /// Set padding.
291    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
292        self.padding = padding.into();
293        self
294    }
295
296    /// Set disabled state.
297    pub fn disabled(mut self, disabled: bool) -> Self {
298        self.disabled = disabled;
299        self
300    }
301
302    /// Set disabled style.
303    pub fn disabled_style(mut self, style: Style) -> Self {
304        self.disabled_style = style;
305        self
306    }
307
308    /// Convenience for toggling full-width behavior.
309    pub fn full_width(mut self, full_width: bool) -> Self {
310        self.width = if full_width {
311            Length::Flex(1)
312        } else {
313            Length::Auto
314        };
315        self
316    }
317
318    /// Set activation handler for mouse clicks and focused plain Enter/Space.
319    pub fn on_click(mut self, cb: Callback<MouseEvent>) -> Self {
320        self.on_click = Some(cb);
321        self
322    }
323
324    /// Set focused key handler. Returning `true` consumes the key before default activation.
325    pub fn on_key(mut self, handler: KeyHandler) -> Self {
326        self.on_key = Some(handler);
327        self
328    }
329
330    /// Control whether the node is focusable.
331    pub fn focusable(mut self, focusable: bool) -> Self {
332        self.focusable = focusable;
333        self
334    }
335
336    /// Control whether the button participates in tab traversal.
337    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
338        self.tab_stop = tab_stop;
339        self
340    }
341
342    /// Set the callback fired when the button gains focus.
343    pub fn on_focus(mut self, cb: Callback<()>) -> Self {
344        self.on_focus = Some(cb);
345        self
346    }
347
348    /// Set the callback fired when the button loses focus.
349    pub fn on_blur(mut self, cb: Callback<()>) -> Self {
350        self.on_blur = Some(cb);
351        self
352    }
353}
354
355impl From<Button> for Element {
356    fn from(value: Button) -> Self {
357        let (min_w, min_h) = measure_button(&value);
358        Element::new(ElementKind::Button(Box::new(value))).with_layout(
359            LayoutConstraints::default()
360                .min_width(Length::Px(min_w))
361                .min_height(Length::Px(min_h)),
362        )
363    }
364}
365
366impl crate::layout::hash::LayoutHash for Button {
367    fn layout_hash(
368        &self,
369        hasher: &mut impl std::hash::Hasher,
370        _recurse: &dyn Fn(&Element) -> Option<u64>,
371    ) -> Option<()> {
372        use std::hash::Hash;
373        self.width.hash(hasher);
374        self.height.hash(hasher);
375        self.variant.hash(hasher);
376        self.padding.hash(hasher);
377        self.label.hash(hasher);
378        self.icon.hash(hasher);
379        self.icon_gap.hash(hasher);
380        self.shortcut.hash(hasher);
381        self.shortcut_gap.hash(hasher);
382        Some(())
383    }
384}