teksilo_widgets/styles/recipe_drop_zone_style.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Default `DropZoneStyle` impl driven by paint-recipe data.
5//!
6//! `RecipeDropZoneStyle` ships the IntUI drop-zone chrome: a rounded,
7//! bordered region whose surface tint and border color track the
8//! interaction state (idle / accepting / rejecting) via the config's
9//! reactive `Signal<DropZoneVisualState>`. The content column (prompt,
10//! subtitle, status line, Browse button) is centered and inset by the
11//! zone padding.
12//!
13//! Apps wanting a different look (dashed border, full-bleed dropzone,
14//! image-backed) write their own `impl DropZoneStyle` block and install it
15//! per-call (`DropZone::style(...)`) or theme-wide
16//! (`theme.style_slots.drop_zone = Some(Rc::new(...))`).
17
18use teksilo_core::build_context::BuildContext;
19use teksilo_core::styles::{DropZoneStyle, DropZoneStyleConfig};
20use teksilo_core::widget_id::WidgetId;
21use teksilo_tokens::{CornerRadius, InputTokens};
22
23use crate::primitives::{Center, Padding, RectWidget, ZStack};
24use teksilo_core::styles::density::spacing;
25
26/// Corner radius of the zone's rounded rectangle.
27pub const DROP_ZONE_CORNER_RADIUS: f32 = 12.0;
28/// Border thickness in logical pixels.
29pub const DROP_ZONE_BORDER_WIDTH: f32 = 2.0;
30/// Inner padding between the border and the content column.
31pub const DROP_ZONE_PADDING: f32 = 20.0;
32
33/// Configurable dimensions for [`RecipeDropZoneStyle`].
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct DropZoneRecipe {
36 /// Corner radius of the zone's rounded rectangle.
37 pub corner_radius: f32,
38 /// Border thickness in logical pixels.
39 pub border_width: f32,
40 /// Inner padding between the border and the content column.
41 pub padding: f32,
42}
43
44impl DropZoneRecipe {
45 /// This recipe's dimensions resolved against a density's [`InputTokens`].
46 ///
47 /// [`Default`] is `for_tokens(&InputTokens::default())` — the Compact
48 /// ladder — so the shipped values below are the Compact column by
49 /// construction and cannot drift from it.
50 pub fn for_tokens(tokens: &InputTokens) -> Self {
51 Self {
52 corner_radius: DROP_ZONE_CORNER_RADIUS,
53 border_width: DROP_ZONE_BORDER_WIDTH,
54 padding: spacing(DROP_ZONE_PADDING, tokens),
55 }
56 }
57}
58
59impl Default for DropZoneRecipe {
60 fn default() -> Self {
61 Self::for_tokens(&InputTokens::default())
62 }
63}
64
65/// Default `DropZoneStyle` shipped with Teksilo.
66#[derive(Debug, Default, Clone, Copy)]
67pub struct RecipeDropZoneStyle {
68 /// Dimension recipe used when painting the drop zone chrome.
69 pub recipe: DropZoneRecipe,
70}
71
72impl RecipeDropZoneStyle {
73 /// Create a style with a custom dimension recipe.
74 pub fn new(recipe: DropZoneRecipe) -> Self {
75 Self { recipe }
76 }
77
78 /// This style with every dimension resolved against a density's
79 /// [`InputTokens`], as `RecipeDropZoneStyle::for_tokens(&ctx.theme().input)` at
80 /// the widget's own build site.
81 ///
82 /// [`Default`] is the `TargetDensity::Compact` projection, so a Compact
83 /// tree gets exactly the values this module documents.
84 pub fn for_tokens(tokens: &InputTokens) -> Self {
85 Self {
86 recipe: DropZoneRecipe::for_tokens(tokens),
87 }
88 }
89}
90
91impl DropZoneStyle for RecipeDropZoneStyle {
92 fn make_body(&self, cfg: &DropZoneStyleConfig, ctx: &mut BuildContext) -> WidgetId {
93 // Surface + border colors track the reactive interaction state.
94 let bg = cfg.state.map(|s| s.surface_role());
95 let border = cfg.state.map(|s| s.border_role());
96
97 let rect = ctx.add(
98 RectWidget::new()
99 .background(bg)
100 .border_color(border)
101 .border_width(self.recipe.border_width)
102 .corner_radius(CornerRadius::uniform(self.recipe.corner_radius)),
103 );
104
105 let centered =
106 Center::new().child(Padding::uniform(self.recipe.padding).child(cfg.content));
107
108 ctx.add(ZStack::new().child(rect).child(centered))
109 }
110}