Skip to main content

teksilo_widgets/styles/
recipe_banner_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Default `BannerStyle` impl driven by paint-recipe data.
5//!
6//! `RecipeBannerStyle` ships the IntUI banner chrome: a per-severity
7//! status-tinted surface (`StatusInfo` / `StatusSuccess` /
8//! `StatusWarning` / `StatusError`) with rounded corners, the content
9//! inset by the banner padding, and the leading severity glyph placed
10//! to the leading edge of the message/action content.
11//!
12//! The `SeverityGlyph` itself is built by the `Banner` widget — it is
13//! a functional renderer (it draws domain data, the info/warn/error
14//! mark), not chrome (principle 6). Apps that want a different banner
15//! look (full-bleed strip, bordered callout, icon-free) write their
16//! own `impl BannerStyle` block.
17
18use teksilo_core::build_context::BuildContext;
19use teksilo_core::color_prop::ColorProp;
20use teksilo_core::styles::{BannerStyle, BannerStyleConfig};
21use teksilo_core::widget_id::WidgetId;
22use teksilo_tokens::{CornerRadius, VAlignment};
23
24use crate::primitives::{Expand, HStack, Padding, RectWidget, ZStack};
25
26// IntUI design tokens for Banner. The recipe owns its own dimensions.
27// `BANNER_GLYPH_SIZE` and `BANNER_TITLE_DESCRIPTION_GAP` are consumed
28// by the `Banner` widget (it builds the glyph + text column); the rest
29// are consumed here.
30pub const BANNER_PADDING_HORIZONTAL: f32 = 12.0;
31pub const BANNER_PADDING_VERTICAL: f32 = 10.0;
32pub const BANNER_CORNER_RADIUS: f32 = 8.0;
33/// Diameter of the leading severity glyph (info / success / error
34/// circle, warning triangle).
35pub const BANNER_GLYPH_SIZE: f32 = 16.0;
36/// Horizontal gap between glyph, text column, action widget, and
37/// dismiss button.
38pub const BANNER_CONTENT_GAP: f32 = 10.0;
39/// Vertical gap between the title and the optional description text
40/// inside the body column.
41pub const BANNER_TITLE_DESCRIPTION_GAP: f32 = 2.0;
42
43/// Dimension recipe for [`RecipeBannerStyle`]. All fields default to the
44/// corresponding `BANNER_*` constants so callers can override individual
45/// measurements without touching the rest.
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct BannerRecipe {
48    pub padding_horizontal: f32,
49    pub padding_vertical: f32,
50    pub corner_radius: f32,
51    pub glyph_size: f32,
52    pub content_gap: f32,
53    pub title_description_gap: f32,
54}
55
56impl Default for BannerRecipe {
57    fn default() -> Self {
58        Self {
59            padding_horizontal: BANNER_PADDING_HORIZONTAL,
60            padding_vertical: BANNER_PADDING_VERTICAL,
61            corner_radius: BANNER_CORNER_RADIUS,
62            glyph_size: BANNER_GLYPH_SIZE,
63            content_gap: BANNER_CONTENT_GAP,
64            title_description_gap: BANNER_TITLE_DESCRIPTION_GAP,
65        }
66    }
67}
68
69/// Default `BannerStyle` shipped with Teksilo. Surface tint comes from
70/// the per-severity `SurfaceRole` (no border — the status surface
71/// tokens already encode contrast with the page background).
72#[derive(Debug, Default, Clone, Copy)]
73pub struct RecipeBannerStyle {
74    pub recipe: BannerRecipe,
75}
76
77impl RecipeBannerStyle {
78    pub fn new(recipe: BannerRecipe) -> Self {
79        Self { recipe }
80    }
81}
82
83impl BannerStyle for RecipeBannerStyle {
84    fn make_body(&self, cfg: &BannerStyleConfig, ctx: &mut BuildContext) -> WidgetId {
85        let radius = CornerRadius::uniform(self.recipe.corner_radius);
86
87        // Background panel — status surface tint, no border.
88        let bg = ctx.add(
89            RectWidget::new()
90                .background(ColorProp::SurfaceRole(cfg.severity.surface()))
91                .corner_radius(radius),
92        );
93
94        // Row layout: [glyph] [content (expands to fill)].
95        let content = ctx.add(Expand::horizontal().child_id(cfg.content));
96        let row = ctx.add(
97            HStack::new()
98                .spacing(self.recipe.content_gap)
99                .alignment(VAlignment::Center)
100                .add_child(cfg.leading_glyph)
101                .add_child(content),
102        );
103        let padded = ctx.add(
104            Padding::symmetric(self.recipe.padding_vertical, self.recipe.padding_horizontal)
105                .child_id(row),
106        );
107
108        ctx.add(ZStack::new().add_child(bg).add_child(padded))
109    }
110}