teksilo_core/styles/drop_zone_style.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Tier-3 style protocol for `DropZone`. See `docs/styling-system.md`.
5//!
6//! Themes the external-drag "drop files here" target: the surface fill,
7//! border, corner radius, and inner padding for each interaction state
8//! (idle / accepting / rejecting). The `DropZone` widget owns its content
9//! column (prompt, subtitle, the `Live::Polite` status line, the Browse
10//! button) and its drag behaviour + accessibility; the style only paints
11//! the chrome the content sits in.
12//!
13//! Because the chrome reacts to hover, the config carries a
14//! `Signal<DropZoneVisualState>` (the same reactive pattern Button uses for
15//! its interaction signal) rather than a static state — `make_body` binds
16//! the surface/border colors to it so they update without a rebuild.
17
18use std::rc::Rc;
19
20use teksilo_tokens::{BorderRole, SurfaceRole};
21
22use crate::build_context::BuildContext;
23use crate::signal::Signal;
24use crate::widget_id::WidgetId;
25
26/// Interaction state of a drop zone, driving the chrome's surface and border
27/// colors. Defined here (not in `teksilo-widgets`) so the core style trait
28/// and the default recipe can both name it — mirroring `BannerSeverity`.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum DropZoneVisualState {
31 /// At rest — no drag over the zone.
32 Idle,
33 /// A drag is over the zone carrying acceptable data.
34 HoverAccept,
35 /// A drag is over the zone but its data is rejected (wrong type / count).
36 HoverReject,
37}
38
39impl DropZoneVisualState {
40 /// Background surface-tint role for this state.
41 pub fn surface_role(self) -> SurfaceRole {
42 match self {
43 Self::Idle => SurfaceRole::Sunken,
44 Self::HoverAccept => SurfaceRole::AccentSubtle,
45 Self::HoverReject => SurfaceRole::StatusError,
46 }
47 }
48
49 /// Border role for this state.
50 pub fn border_role(self) -> BorderRole {
51 match self {
52 Self::Idle => BorderRole::Strong,
53 Self::HoverAccept => BorderRole::Accent,
54 Self::HoverReject => BorderRole::Error,
55 }
56 }
57}
58
59/// Inputs handed to a [`DropZoneStyle`] to build the zone's chrome.
60#[derive(Clone)]
61pub struct DropZoneStyleConfig {
62 /// Reactive interaction state — bind surface/border colors to it.
63 pub state: Signal<DropZoneVisualState>,
64 /// Pre-built content column (icon / prompt / subtitle / status line /
65 /// Browse button) the chrome centers and pads.
66 pub content: WidgetId,
67}
68
69/// Tier-3 style protocol for [`DropZone`](../../teksilo_widgets/drop_zone).
70/// Produces the bordered, tinted body the content sits in.
71pub trait DropZoneStyle: 'static {
72 fn make_body(&self, cfg: &DropZoneStyleConfig, ctx: &mut BuildContext) -> WidgetId;
73}
74
75/// Shared, theme-installable handle to a [`DropZoneStyle`].
76pub type SharedDropZoneStyle = Rc<dyn DropZoneStyle>;