Skip to main content

teksilo_widgets/styles/
recipe_toast_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Default `ToastStyle` impl driven by paint-recipe data.
5//!
6//! `RecipeToastStyle` ships the IntUI toast chrome: a per-severity
7//! status-tinted surface (`StatusInfo` / `StatusSuccess` /
8//! `StatusWarning` / `StatusError`) with rounded corners, padding,
9//! a leading severity glyph, and an optional trailing close `IconButton`.
10//!
11//! Tokens are co-located here.
12//! Apps that want a different look (full-bleed strip, frosted glass,
13//! icon-free) write their own `impl ToastStyle` block. The widget
14//! always builds the functional pieces (severity glyph, close button,
15//! body content) — the recipe is pure chrome.
16
17use teksilo_core::build_context::BuildContext;
18use teksilo_core::color_prop::ColorProp;
19use teksilo_core::styles::{ToastStyle, ToastStyleConfig};
20use teksilo_core::widget_id::WidgetId;
21use teksilo_tokens::{CornerRadius, VAlignment};
22
23use crate::primitives::{Expand, HStack, Padding, RectWidget, ZStack};
24
25/// Outer horizontal padding inside the toast surface.
26pub const TOAST_PADDING_HORIZONTAL: f32 = 14.0;
27/// Outer vertical padding inside the toast surface.
28pub const TOAST_PADDING_VERTICAL: f32 = 12.0;
29/// Rounded corner radius of the toast surface (matches IntUI
30/// `radius_popup`).
31pub const TOAST_CORNER_RADIUS: f32 = 8.0;
32/// Diameter of the leading severity glyph (consumed by the widget, not
33/// the recipe — exposed here so the widget pulls one constant).
34pub const TOAST_GLYPH_SIZE: f32 = 16.0;
35/// Horizontal gap between leading glyph, body column, and trailing
36/// close button.
37pub const TOAST_CONTENT_GAP: f32 = 12.0;
38/// Vertical gap between title and body lines inside the body column.
39pub const TOAST_TITLE_BODY_GAP: f32 = 2.0;
40/// Vertical gap between body and action row (when actions are present).
41pub const TOAST_BODY_ACTIONS_GAP: f32 = 8.0;
42
43/// Dimension recipe for [`RecipeToastStyle`].
44///
45/// All fields default to the matching `TOAST_*` constants so a plain
46/// `ToastRecipe::default()` reproduces the stock IntUI look. Override
47/// individual fields to tune padding, gap, or corner radius without
48/// writing a full custom `ToastStyle`.
49#[derive(Debug, Clone, Copy, PartialEq)]
50pub struct ToastRecipe {
51    /// Outer horizontal padding inside the toast surface.
52    pub padding_horizontal: f32,
53    /// Outer vertical padding inside the toast surface.
54    pub padding_vertical: f32,
55    /// Rounded corner radius of the toast surface.
56    pub corner_radius: f32,
57    /// Diameter of the leading severity glyph.
58    pub glyph_size: f32,
59    /// Horizontal gap between leading glyph, body column, and trailing
60    /// close button.
61    pub content_gap: f32,
62    /// Vertical gap between title and body lines inside the body column.
63    pub title_body_gap: f32,
64    /// Vertical gap between body and action row (when actions are present).
65    pub body_actions_gap: f32,
66}
67
68impl Default for ToastRecipe {
69    fn default() -> Self {
70        Self {
71            padding_horizontal: TOAST_PADDING_HORIZONTAL,
72            padding_vertical: TOAST_PADDING_VERTICAL,
73            corner_radius: TOAST_CORNER_RADIUS,
74            glyph_size: TOAST_GLYPH_SIZE,
75            content_gap: TOAST_CONTENT_GAP,
76            title_body_gap: TOAST_TITLE_BODY_GAP,
77            body_actions_gap: TOAST_BODY_ACTIONS_GAP,
78        }
79    }
80}
81
82/// Default `ToastStyle` shipped with Teksilo. Surface tint comes from
83/// the per-severity `SurfaceRole`. The recipe ignores
84/// `cfg.priority` — High/Urgent toasts look identical to Normal at
85/// this default styling tier (apps that want a heavier shadow on
86/// Urgent provide their own `impl ToastStyle`).
87#[derive(Debug, Default, Clone, Copy)]
88pub struct RecipeToastStyle {
89    pub recipe: ToastRecipe,
90}
91
92impl RecipeToastStyle {
93    pub fn new(recipe: ToastRecipe) -> Self {
94        Self { recipe }
95    }
96}
97
98impl ToastStyle for RecipeToastStyle {
99    fn make_body(&self, cfg: &ToastStyleConfig, ctx: &mut BuildContext) -> WidgetId {
100        let radius = CornerRadius::uniform(self.recipe.corner_radius);
101
102        // Background panel — status surface tint, no border (status
103        // surface tokens already encode contrast with the page bg).
104        let bg = ctx.add(
105            RectWidget::new()
106                .background(ColorProp::SurfaceRole(cfg.severity.surface()))
107                .corner_radius(radius),
108        );
109
110        // Row layout: [glyph] [body (expands)] [close?].
111        let body = ctx.add(Expand::horizontal().child_id(cfg.content));
112        let mut row = HStack::new()
113            .spacing(self.recipe.content_gap)
114            .alignment(VAlignment::Top)
115            .add_child(cfg.leading_glyph)
116            .add_child(body);
117        if let Some(close_id) = cfg.trailing_close {
118            row = row.add_child(close_id);
119        }
120        let row_id = ctx.add(row);
121        let padded = ctx.add(
122            Padding::symmetric(self.recipe.padding_vertical, self.recipe.padding_horizontal)
123                .child_id(row_id),
124        );
125
126        ctx.add(ZStack::new().add_child(bg).add_child(padded))
127    }
128}