Skip to main content

teksilo_core/styles/
button_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Tier-3 style protocol for `Button`.
5//!
6//! See `docs/styling-system.md`. The trait is object-safe so
7//! `Rc<dyn ButtonStyle>` can be stored in a theme slot or attached
8//! per-call via `Button::style(...)`.
9
10use std::rc::Rc;
11
12use serde::{Deserialize, Serialize};
13use teksilo_canvas::{EdgeInsets, Size};
14
15use crate::build_context::BuildContext;
16use crate::signal::Signal;
17use crate::styles::recipe::{BorderRecipe, FillRecipe, PerStateRecipe, ShadowRecipe, ShapeRecipe};
18use crate::widget_id::WidgetId;
19
20/// Closed enum naming the design-language variants of `Button`. Set
21/// per-call via `Button::variant(ButtonVariant::Outlined)` or
22/// per-app default via a `ComponentDefaults` extension.
23///
24/// Variants are *hints* the active [`ButtonStyle`] may honour or
25/// remap. The IntUI default `RecipeButtonStyle` collapses some pairs:
26/// Tinted/Outlined → Plain look, Link → Ghost, Destructive → Filled
27/// (the warning lives in the dialog title, not the button).
28#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
29pub enum ButtonVariant {
30    Filled,
31    Tinted,
32    Outlined,
33    #[default]
34    Plain,
35    Ghost,
36    Link,
37    Destructive,
38}
39
40/// Inputs handed to a [`ButtonStyle::make_body`] call.
41///
42/// `label` is a pre-built subtree (the active style only arranges
43/// chrome around it; it never builds the label itself). The four
44/// boolean signals carry the live interaction state — the style can
45/// `.zip` / `.map` them to derive a [`crate::styles::WidgetState`] if
46/// it wants to pick between per-state recipes.
47#[derive(Clone, Debug)]
48pub struct ButtonStyleConfig {
49    pub label: WidgetId,
50    pub is_pressed: Signal<bool>,
51    pub is_hovered: Signal<bool>,
52    pub is_focused: Signal<bool>,
53    pub is_disabled: Signal<bool>,
54    pub variant: ButtonVariant,
55}
56
57/// Style protocol for `Button`. The active style owns *all* paint and
58/// layering — it receives the label subtree pre-built and arranges
59/// background, border, focus ring, padding, etc. around it.
60///
61/// `'static` (no `Send + Sync`) because the rest of teksilo-core is
62/// already single-threaded (`Signal` uses `Rc`); enforcing thread
63/// safety here would be inconsistent and pay no benefit.
64pub trait ButtonStyle: 'static {
65    fn make_body(&self, cfg: &ButtonStyleConfig, ctx: &mut BuildContext) -> WidgetId;
66
67    /// Optional per-variant override of the label/icon text role.
68    ///
69    /// The `Button` picks its label color from its built-in
70    /// variant→role mapping (`OnAccent` for accent-filled variants,
71    /// `Primary` otherwise, `Link` for `Link`) *before* the style runs.
72    /// Returning `Some(role)` here lets a design-language style redirect
73    /// it — e.g. Material 3 paints text/outlined buttons in the accent
74    /// color (`TextRole::Accent`) rather than `Primary`.
75    ///
76    /// Default `None` preserves the built-in mapping, so existing styles
77    /// (and the IntUI default) are unaffected. A per-call
78    /// `Button::text_role(...)` still wins over this.
79    fn label_text_role(&self, _variant: ButtonVariant) -> Option<teksilo_tokens::TextRole> {
80        None
81    }
82}
83
84/// Shared handle for a `ButtonStyle` impl. Cheap to clone; one shared
85/// `Rc` is used per theme slot and per-call override.
86pub type SharedButtonStyle = Rc<dyn ButtonStyle>;
87
88/// Tier-2 paint-recipe for one variant of `Button`. The default
89/// [`crate::styles::ButtonStyle`] impl shipped in `teksilo-widgets`
90/// (`RecipeButtonStyle`) holds a `HashMap<ButtonVariant, ButtonRecipe>`
91/// and looks up the recipe at paint time.
92///
93/// Custom `ButtonStyle` impls can ignore recipes entirely (paint a
94/// glassmorphism gradient, run their own canvas code, etc.); the
95/// recipe layer is the *default* surface, not an obligation.
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
97pub struct ButtonRecipe {
98    pub shape: ShapeRecipe,
99    pub fill: PerStateRecipe<FillRecipe>,
100    pub border: PerStateRecipe<BorderRecipe>,
101    pub shadow: PerStateRecipe<Option<ShadowRecipe>>,
102    pub padding: EdgeInsets,
103    pub min_size: Size,
104}