teksilo_widgets/styles/
recipe_splitter_style.rs1use teksilo_canvas::{Canvas, Rect, SizeProposal};
17use teksilo_core::accessibility::AccessNodeBuilder;
18use teksilo_core::binding::BindingLevel;
19use teksilo_core::build_context::BuildContext;
20use teksilo_core::focus::FocusOrigin;
21use teksilo_core::signal::Signal;
22use teksilo_core::styles::{SplitterStyle, SplitterStyleConfig};
23use teksilo_core::widget::{LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement};
24use teksilo_core::widget_id::WidgetId;
25use teksilo_tokens::Orientation;
26
27pub const SPLITTER_DIVIDER_LINE_THICKNESS: f32 = 1.0;
29
30const HOVER_DWELL_DELAY_FRAC: f32 = 0.75;
34
35#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct SplitterRecipe {
38 pub divider_line_thickness: f32,
40}
41
42impl Default for SplitterRecipe {
43 fn default() -> Self {
44 Self {
45 divider_line_thickness: SPLITTER_DIVIDER_LINE_THICKNESS,
46 }
47 }
48}
49
50#[derive(Debug, Default, Clone, Copy)]
53pub struct RecipeSplitterStyle {
54 pub recipe: SplitterRecipe,
55}
56
57impl RecipeSplitterStyle {
58 pub fn new(recipe: SplitterRecipe) -> Self {
59 Self { recipe }
60 }
61}
62
63impl SplitterStyle for RecipeSplitterStyle {
64 fn make_handle(&self, cfg: &SplitterStyleConfig, ctx: &mut BuildContext) -> WidgetId {
65 ctx.add(SplitterHandleBody {
66 orientation: cfg.orientation,
67 is_dragging: cfg.is_dragging.clone(),
68 is_disabled: cfg.is_disabled.clone(),
69 focus_origin: cfg.focus_origin.clone(),
70 hover_progress: cfg.hover_progress.clone(),
71 recipe: self.recipe,
72 })
73 }
74}
75
76struct SplitterHandleBody {
78 orientation: Orientation,
79 is_dragging: Signal<bool>,
80 is_disabled: Signal<bool>,
81 focus_origin: Signal<Option<FocusOrigin>>,
82 hover_progress: Signal<f32>,
83 recipe: SplitterRecipe,
84}
85
86impl std::fmt::Debug for SplitterHandleBody {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.debug_struct("SplitterHandleBody")
89 .field("orientation", &self.orientation)
90 .finish()
91 }
92}
93
94impl Widget for SplitterHandleBody {
95 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
96 let id = ctx.self_id();
97 let registry = ctx.binding_registry();
98 self.is_dragging
99 .bind_to(id, registry, BindingLevel::RepaintOnly);
100 self.is_disabled
101 .bind_to(id, registry, BindingLevel::RepaintOnly);
102 self.focus_origin
103 .bind_to(id, registry, BindingLevel::RepaintOnly);
104 self.hover_progress
105 .bind_to(id, registry, BindingLevel::RepaintOnly);
106 vec![]
107 }
108
109 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
110 proposal.resolve(0.0, 0.0).into()
112 }
113
114 fn place_children(
115 &self,
116 _bounds: Rect,
117 _proposal: SizeProposal,
118 _children: &mut [WidgetPlacement],
119 _ctx: &LayoutContext,
120 ) {
121 }
122
123 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
124 let colors = &ctx.theme.colors;
125 let enabled = !self.is_disabled.get();
126
127 let line_thickness = self.recipe.divider_line_thickness.max(1.0);
128 let focus_thickness = (line_thickness * 3.0).max(line_thickness + 2.0);
129
130 let line_rect = |thickness: f32| match self.orientation {
131 Orientation::Horizontal => Rect::new(
133 bounds.x + (bounds.width - thickness) / 2.0,
134 bounds.y,
135 thickness,
136 bounds.height,
137 ),
138 Orientation::Vertical => Rect::new(
139 bounds.x,
140 bounds.y + (bounds.height - thickness) / 2.0,
141 bounds.width,
142 thickness,
143 ),
144 };
145
146 canvas.fill_rect(line_rect(line_thickness), colors.border);
148
149 let focus_alpha = if !enabled {
152 0.0
153 } else if self.focus_origin.get() == Some(FocusOrigin::Keyboard) || self.is_dragging.get() {
154 1.0
155 } else {
156 let p = self.hover_progress.get();
157 ((p - HOVER_DWELL_DELAY_FRAC) / (1.0 - HOVER_DWELL_DELAY_FRAC)).clamp(0.0, 1.0)
158 };
159
160 if focus_alpha > 0.0 {
161 canvas.fill_rect(
162 line_rect(focus_thickness),
163 colors.focus_ring.with_alpha(focus_alpha),
164 );
165 }
166 }
167
168 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
169 builder.set_hidden();
171 }
172}