Skip to main content

teksilo_widgets/styles/
recipe_popover_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Default `PopoverStyle` impl driven by paint-recipe data.
5//!
6//! `RecipePopoverStyle` constructs the IntUI `PopoverSurface`
7//! (`crates/teksilo-widgets/src/popover_surface.rs`) — an elevated panel with
8//! `surface_main` background, accent-aware shadow whose attached side
9//! is suppressed (so the panel reads as connected to its trigger),
10//! and an optional directional caret that points at the trigger.
11//!
12//! Apps wanting a different chrome (frosted-glass popover, brutalist
13//! flat panel, custom caret) write their own `impl PopoverStyle`
14//! block and install per-call (`Popover::style(...)`) or theme-wide
15//! (`theme.style_slots.popover = Some(Rc::new(MyPopover))`).
16
17use teksilo_canvas::EdgeInsets;
18use teksilo_core::build_context::BuildContext;
19use teksilo_core::styles::{PopoverStyle, PopoverStyleConfig, PopoverVariant};
20use teksilo_core::widget::PendingChild;
21use teksilo_core::widget_id::WidgetId;
22use teksilo_tokens::{InputTokens, SurfaceRole};
23
24use crate::popover_surface::PopoverSurface;
25use teksilo_core::styles::density::spacing;
26
27// IntUI design tokens for Popover. The recipe and surface own their
28// own dimensions.
29//
30// `POPOVER_PADDING` is the content inset for Default/Tooltip-flavoured
31// popovers. The `Menu` variant uses zero content padding so menu rows
32// (which carry their own row padding) reach the surface edge.
33pub const POPOVER_PADDING: f32 = 16.0;
34pub const POPOVER_CORNER_RADIUS: f32 = 8.0;
35pub const POPOVER_BORDER_WIDTH: f32 = 1.0;
36/// Corner radius for the `Menu`-flavoured popup surface (menu lists,
37/// combo-box dropdowns, search-field suggestion panels).
38pub const MENU_POPUP_CORNER_RADIUS: f32 = 8.0;
39/// 0..=1 multiplier on `shape.shadow_inner_sm.color.a` at paint time.
40pub const POPOVER_SHADOW_DENSITY: f32 = 0.5;
41
42/// Configurable dimensions for [`RecipePopoverStyle`].
43///
44/// [`Default`] fills every field from the corresponding `POPOVER_*` /
45/// `MENU_POPUP_*` constant, so existing code that relies on the defaults
46/// is unaffected.
47#[derive(Debug, Clone, Copy, PartialEq)]
48pub struct PopoverRecipe {
49    pub padding: f32,
50    pub corner_radius: f32,
51    pub border_width: f32,
52    pub menu_popup_corner_radius: f32,
53    pub shadow_density: f32,
54}
55
56impl PopoverRecipe {
57    /// This recipe's dimensions resolved against a density's [`InputTokens`].
58    ///
59    /// [`Default`] is `for_tokens(&InputTokens::default())` — the Compact
60    /// ladder — so the shipped values below are the Compact column by
61    /// construction and cannot drift from it.
62    pub fn for_tokens(tokens: &InputTokens) -> Self {
63        Self {
64            padding: spacing(POPOVER_PADDING, tokens),
65            corner_radius: POPOVER_CORNER_RADIUS,
66            border_width: POPOVER_BORDER_WIDTH,
67            menu_popup_corner_radius: MENU_POPUP_CORNER_RADIUS,
68            shadow_density: POPOVER_SHADOW_DENSITY,
69        }
70    }
71}
72
73impl Default for PopoverRecipe {
74    fn default() -> Self {
75        Self::for_tokens(&InputTokens::default())
76    }
77}
78
79/// Default `PopoverStyle` shipped with Teksilo. The `Default` and
80/// `Tooltip` variants produce the elevated `surface_main` panel with
81/// 16 px content padding; the `Menu` variant produces a `surface_raised`
82/// panel with zero content padding (so menu rows reach the edge) and a
83/// presentational a11y node (the caller owns the container role).
84#[derive(Debug, Default, Clone, Copy)]
85pub struct RecipePopoverStyle {
86    pub recipe: PopoverRecipe,
87}
88
89impl RecipePopoverStyle {
90    pub fn new(recipe: PopoverRecipe) -> Self {
91        Self { recipe }
92    }
93
94    /// This style with every dimension resolved against a density's
95    /// [`InputTokens`], as `RecipePopoverStyle::for_tokens(&ctx.theme().input)` at
96    /// the widget's own build site.
97    ///
98    /// [`Default`] is the `TargetDensity::Compact` projection, so a Compact
99    /// tree gets exactly the values this module documents.
100    pub fn for_tokens(tokens: &InputTokens) -> Self {
101        Self {
102            recipe: PopoverRecipe::for_tokens(tokens),
103        }
104    }
105}
106
107impl PopoverStyle for RecipePopoverStyle {
108    fn make_body(&self, cfg: &PopoverStyleConfig, ctx: &mut BuildContext) -> WidgetId {
109        let (content_padding, background, corner_radius, presentational) = match cfg.variant {
110            PopoverVariant::Menu => (
111                EdgeInsets::ZERO,
112                SurfaceRole::Raised,
113                self.recipe.menu_popup_corner_radius,
114                true,
115            ),
116            PopoverVariant::Default | PopoverVariant::Tooltip => (
117                EdgeInsets::uniform(self.recipe.padding),
118                SurfaceRole::Main,
119                self.recipe.corner_radius,
120                false,
121            ),
122        };
123
124        ctx.add(PopoverSurface::new(
125            PendingChild::Id(cfg.content),
126            cfg.placement.clone(),
127            cfg.show_caret,
128            cfg.caret_size,
129            cfg.name.clone(),
130            content_padding,
131            background,
132            corner_radius,
133            presentational,
134        ))
135    }
136}