Skip to main content

teksilo_core/styles/
radio_tile_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Tier-3 style protocol for `RadioTile`. See `docs/styling-system.md`.
5//!
6//! A `RadioTile` is a "selectable card" — a bordered, rounded surface that
7//! behaves as a single radio option (`Role::RadioButton`, `set_toggled`)
8//! while rendering a leading icon, a bold title, an inline radio indicator,
9//! and a muted description. The host widget composes the tile's *content*
10//! (icon / title / indicator / description) and hands it to the style as a
11//! single `content` id; the style owns only the card chrome — background,
12//! border, corner radius, padding, and any elevation shadow — driven by the
13//! selection / hover / press / focus / disabled cascade.
14//!
15//! This mirrors [`StandardItemStyleConfig`](crate::styles::StandardItemStyleConfig)
16//! (content-plus-state) rather than [`RadioStyleConfig`](crate::styles::RadioStyleConfig)
17//! (a bare glyph), because a tile is a container whose chrome wraps arbitrary
18//! content, not a self-drawn mark.
19
20use std::rc::Rc;
21
22use serde::{Deserialize, Serialize};
23
24use crate::build_context::BuildContext;
25use crate::signal::Signal;
26use crate::widget_id::WidgetId;
27
28/// Design-language variant for a `RadioTile` (in `teksilo-widgets`).
29/// The active `RadioTileStyle` decides what each variant means visually.
30#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
31pub enum RadioTileVariant {
32    /// IntUI default: a subtle surface with a 1 dp border; the selected
33    /// tile gains an accent border. No shadow. Matches the flat
34    /// "selectable card" look.
35    #[default]
36    Outlined,
37    /// A raised card with a drop shadow (the theme's `shadow_md`). Use
38    /// when tiles float above the surrounding surface.
39    Elevated,
40    /// A solid filled surface (no border in the resting state). The
41    /// selection cue is the fill tint and the radio indicator.
42    Filled,
43}
44
45/// Everything a [`RadioTileStyle`] needs to render one tile's chrome.
46///
47/// All state is delivered as reactive `Signal`s so the chrome repaints on
48/// interaction without a rebuild. The window-active / focus split matches
49/// [`StandardItemStyleConfig`](crate::styles::StandardItemStyleConfig): a
50/// selected tile shows the vivid selection surface only while its group
51/// holds keyboard focus **and** the window is active, and the keyboard focus
52/// ring appears only under `is_focused && is_focus_visible`.
53#[derive(Clone, Debug)]
54pub struct RadioTileStyleConfig {
55    /// Pre-composed tile content — typically a `VStack` of
56    /// `[HStack { icon?, title, Spacer, indicator? }, description|body?]`
57    /// built by the host `RadioTile`. The style wraps it in the card chrome
58    /// but does not lay it out internally.
59    pub content: WidgetId,
60    pub is_selected: Signal<bool>,
61    pub is_hovered: Signal<bool>,
62    pub is_pressed: Signal<bool>,
63    /// Whether the tile's group (or the tile itself, when standalone) holds
64    /// keyboard focus. Combined with `is_window_active` to pick the vivid vs
65    /// muted selection surface.
66    pub is_focused: Signal<bool>,
67    /// Input-modality "focus-visible": `true` after keyboard input. The focus
68    /// ring renders only when this and `is_focused` are both true, so a mouse
69    /// click selects without a ring while keyboard navigation reveals one.
70    pub is_focus_visible: Signal<bool>,
71    pub is_disabled: Signal<bool>,
72    /// Whether the host window is active (`focused AND not occluded`).
73    /// Populated from [`BuildContext::window_active_signal`].
74    pub is_window_active: Signal<bool>,
75    pub variant: RadioTileVariant,
76    /// The tile is in the compact single-line arrangement (a
77    /// `TileLayout::Vertical` row): the style should center its content in the
78    /// fixed row height rather than pad-and-top-anchor it, so a short fixed
79    /// height never over-constrains the content.
80    pub is_compact: bool,
81}
82
83/// Tier-3 style protocol for `RadioTile`. Implement this to fully replace the
84/// card chrome (per-call `RadioTile::style(...)` or theme-wide
85/// `theme.style_slots.radio_tile = Some(Rc::new(MyTile))`).
86pub trait RadioTileStyle: 'static {
87    fn make_body(&self, cfg: &RadioTileStyleConfig, ctx: &mut BuildContext) -> WidgetId;
88
89    /// Fixed row height, in logical pixels, for a
90    /// `RadioTileGroup` in `TileLayout::Vertical` (the compact settings-list
91    /// arrangement). A theme-driven value — override it in a custom style to
92    /// change how tall the compact rows are. The default recipe reads it from
93    /// its `RadioTileRecipe`.
94    fn vertical_row_height(&self) -> f32 {
95        44.0
96    }
97}
98
99pub type SharedRadioTileStyle = Rc<dyn RadioTileStyle>;