Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-widgets/src/control.rs
StratoSDK / crates / strato-widgets / src / control.rs
1//! Common control interaction and accessibility primitives.
2//!
3//! This module provides a lightweight state machine that widgets can embed to
4//! unify pressed/hover/disabled handling and animate visual responses. It also
5//! carries accessibility semantics (role, label, hint) so controls can expose
6//! intent to higher level tooling.
7 
8use crate::animation::Tween;
9use crate::widget::WidgetState;
10use strato_core::event::{Event, EventResult, KeyCode, MouseButton};
11use strato_core::state::Signal;
12use strato_core::types::{Point, Rect};
13 
14/// The ARIA-like role associated with a control.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ControlRole {
17 Button,
18 Checkbox,
19 Radio,
20 Slider,
21 Input,
22 Toggle,
23}
24 
25impl Default for ControlRole {
26 fn default() -> Self {
27 ControlRole::Button
28 }
29}
30 
31/// Accessibility semantics for a control.
32#[derive(Debug, Clone, Default)]
33pub struct ControlSemantics {
34 pub role: ControlRole,
35 pub label: Option<String>,
36 pub hint: Option<String>,
37 pub value: Option<String>,
38 pub toggled: Option<bool>,
39}
40 
41impl ControlSemantics {
42 pub fn new(role: ControlRole) -> Self {
43 Self {
44 role,
45 ..Default::default()
46 }
47 }
48}
49 
50/// Shared interaction state for focusable/pressable controls.
51#[derive(Debug, Clone)]
52pub struct ControlState {
53 state: Signal<WidgetState>,
54 interaction_progress: Signal<f32>,
55 focus_progress: Signal<f32>,
56 semantics: ControlSemantics,
57}
58 
59impl ControlState {
60 /// Create a new control state for the given role.
61 pub fn new(role: ControlRole) -> Self {
62 Self {
63 state: Signal::new(WidgetState::Normal),
64 interaction_progress: Signal::new(0.0),
65 focus_progress: Signal::new(0.0),
66 semantics: ControlSemantics::new(role),
67 }
68 }
69 
70 /// Current widget state.
71 pub fn state(&self) -> WidgetState {
72 self.state.get()
73 }
74 
75 /// Set the widget state directly (used by tests/demo code).
76 pub fn set_state(&self, state: WidgetState) {
77 self.state.set(state);
78 }
79 
80 /// Mark the control as disabled/enabled.
81 pub fn set_disabled(&self, disabled: bool) {
82 if disabled {
83 self.state.set(WidgetState::Disabled);
84 } else if self.state.get() == WidgetState::Disabled {
85 self.state.set(WidgetState::Normal);
86 }
87 }
88 
89 /// Update the semantics label.
90 pub fn set_label(&mut self, label: impl Into<String>) {
91 self.semantics.label = Some(label.into());
92 }
93 
94 /// Update the semantics hint/description.
95 pub fn set_hint(&mut self, hint: impl Into<String>) {
96 self.semantics.hint = Some(hint.into());
97 }
98 
99 /// Update the semantics value (e.g., slider percentage).
100 pub fn set_value(&mut self, value: impl Into<String>) {
101 self.semantics.value = Some(value.into());
102 }
103 
104 /// Mark whether the control is toggled/checked.
105 pub fn set_toggled(&mut self, toggled: bool) {
106 self.semantics.toggled = Some(toggled);
107 }
108 
109 /// Access semantics for inspectors or higher-level layers.
110 pub fn semantics(&self) -> &ControlSemantics {
111 &self.semantics
112 }
113 
114 /// Smooth interaction animation state toward the target.
115 pub fn update(&self, delta_time: f32) {
116 let target_interaction = match self.state.get() {
117 WidgetState::Pressed => 1.0,
118 WidgetState::Hovered => 0.65,
119 WidgetState::Focused => 0.4,
120 WidgetState::Disabled => 0.0,
121 WidgetState::Normal => 0.0,
122 };
123 let target_focus = if matches!(self.state.get(), WidgetState::Focused) {
124 1.0
125 } else {
126 0.0
127 };
128 
129 let smooth = |current: f32, target: f32| {
130 let step = (target - current) * (delta_time * 8.0).clamp(0.0, 1.0);
131 (current + step).clamp(0.0, 1.0)
132 };
133 
134 self.interaction_progress
135 .set(smooth(self.interaction_progress.get(), target_interaction));
136 self.focus_progress
137 .set(smooth(self.focus_progress.get(), target_focus));
138 }
139 
140 /// Overall interaction factor used for color/opacity blending.
141 pub fn interaction_factor(&self) -> f32 {
142 Tween::new(0.0, 1.0).transform(
143 self.interaction_progress
144 .get()
145 .max(self.focus_progress.get()),
146 )
147 }
148 
149 /// Update hover state when the cursor enters/leaves the control bounds.
150 pub fn hover(&self, within: bool) {
151 if self.state.get() == WidgetState::Disabled {
152 return;
153 }
154 match (within, self.state.get()) {
155 (true, WidgetState::Normal) => self.state.set(WidgetState::Hovered),
156 (false, WidgetState::Hovered) => self.state.set(WidgetState::Normal),
157 (false, WidgetState::Pressed) => self.state.set(WidgetState::Normal),
158 _ => {}
159 }
160 }
161 
162 /// Start a press interaction if the point is inside.
163 pub fn press(&self, point: Point, bounds: Rect) -> bool {
164 if self.state.get() == WidgetState::Disabled {
165 return false;
166 }
167 if bounds.contains(point) {
168 self.state.set(WidgetState::Pressed);
169 return true;
170 }
171 false
172 }
173 
174 /// Finish a press interaction and report whether activation should occur.
175 pub fn release(&self, point: Point, bounds: Rect) -> bool {
176 if self.state.get() == WidgetState::Disabled {
177 return false;
178 }
179 if self.state.get() == WidgetState::Pressed {
180 let is_hovered = bounds.contains(point);
181 self.state.set(if is_hovered {
182 WidgetState::Hovered
183 } else {
184 WidgetState::Normal
185 });
186 return is_hovered;
187 }
188 false
189 }
190 
191 /// Update focus state based on keyboard navigation.
192 pub fn focus(&self) {
193 if self.state.get() != WidgetState::Disabled {
194 self.state.set(WidgetState::Focused);
195 }
196 }
197 
198 /// Handle blur.
199 pub fn blur(&self) {
200 if self.state.get() == WidgetState::Focused {
201 self.state.set(WidgetState::Normal);
202 }
203 }
204 
205 /// Keyboard activations for accessible triggers.
206 pub fn handle_keyboard_activation(&self, event: &Event) -> EventResult {
207 match event {
208 Event::KeyDown(key) if matches!(key.key_code, KeyCode::Enter | KeyCode::Space) => {
209 if self.state.get() != WidgetState::Disabled {
210 self.state.set(WidgetState::Pressed);
211 return EventResult::Handled;
212 }
213 }
214 Event::KeyUp(key) if matches!(key.key_code, KeyCode::Enter | KeyCode::Space) => {
215 if self.state.get() == WidgetState::Pressed {
216 self.state.set(WidgetState::Focused);
217 return EventResult::Handled;
218 }
219 }
220 _ => {}
221 }
222 EventResult::Ignored
223 }
224 
225 /// Pointer-based interaction dispatcher.
226 pub fn handle_pointer_event(&self, event: &Event, bounds: Rect) -> EventResult {
227 match event {
228 Event::MouseMove(mouse) => {
229 let point = Point::new(mouse.position.x, mouse.position.y);
230 self.hover(bounds.contains(point));
231 EventResult::Ignored
232 }
233 Event::MouseDown(mouse) => {
234 if let Some(MouseButton::Left) = mouse.button {
235 let point = Point::new(mouse.position.x, mouse.position.y);
236 if self.press(point, bounds) {
237 return EventResult::Handled;
238 }
239 }
240 EventResult::Ignored
241 }
242 Event::MouseUp(mouse) => {
243 if let Some(MouseButton::Left) = mouse.button {
244 let point = Point::new(mouse.position.x, mouse.position.y);
245 if self.release(point, bounds) {
246 return EventResult::Handled;
247 }
248 }
249 EventResult::Ignored
250 }
251 _ => EventResult::Ignored,
252 }
253 }
254}
255