1use crate::motion::{
17 animations_enabled, MaterialMotionRole, MaterialMotionScheme, MaterialSpringState,
18};
19use ferroui_base::animation::{IGlobalClock, TimeSpan};
20use ferroui_base::input::InputElementImpl;
21use ferroui_base::interactivity::InteractiveImpl;
22use ferroui_base::layout::LayoutableImpl;
23use ferroui_base::media::immutable::ImmutablePen;
24use ferroui_base::media::{
25 BrushExtensions, DrawingContext, IBrush, IPen, PenLineCap, PenLineJoin, StreamGeometry,
26};
27use ferroui_base::platform::IGeometryContext;
28use ferroui_base::reactive::{AnonymousObserver, IDisposable};
29use ferroui_base::{
30 ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, FerroLocator,
31 FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs,
32 LocatorExtensions, Point, Ref, Size, StyledElementImpl, StyledProperty, Visual, VisualImpl,
33 VisualImplExt, VisualTreeAttachmentEventArgs,
34};
35use ferroui_controls::{Control, ControlImpl};
36use std::cell::{Cell, RefCell};
37use std::rc::Rc;
38
39#[repr(C)]
40pub struct MaterialSelectionMark {
41 base: Control,
42 motion: RefCell<MarkMotion>,
43 clock_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
44 visibility_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
45 last_tick: Cell<Option<f64>>,
46}
47ferro_class!(MaterialSelectionMark:Control);
48ferro_class_info!(MaterialSelectionMark {
49 new: MaterialSelectionMark::new
50});
51ferro_impl_classes!(MaterialSelectionMark:StyledElementImpl,InteractiveImpl,InputElementImpl,ControlImpl);
52impl FerroObjectImpl for MaterialSelectionMark {
53 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
54 Self::parent_on_property_changed(this, change);
55 let property = change.property();
56 if property == Self::is_checked_property().as_property()
57 || property == Self::reduced_motion_property().as_property()
58 || property == Self::motion_scheme_property().as_property()
59 {
60 this.retarget();
61 }
62 this.invalidate_visual();
63 }
64}
65impl LayoutableImpl for MaterialSelectionMark {
66 fn measure_override(_this: &Self, available: Size) -> Size {
67 Size::new(
68 18.0_f64.min(available.width),
69 18.0_f64.min(available.height),
70 )
71 }
72}
73impl VisualImpl for MaterialSelectionMark {
74 fn bypass_flow_direction_policies(_this: &Self) -> bool {
75 true
76 }
77 fn on_attached_to_visual_tree(this: &Self, event: &VisualTreeAttachmentEventArgs) {
78 Self::parent_on_attached_to_visual_tree(this, event);
79 let weak = this.to_ref().downgrade();
80 *this.visibility_subscription.borrow_mut() =
81 Some(this.is_effectively_visible_changed(move || {
82 if let Some(this) = weak.upgrade() {
83 this.update_clock();
84 }
85 }));
86 this.retarget();
87 }
88 fn on_detached_from_visual_tree(this: &Self, event: &VisualTreeAttachmentEventArgs) {
89 this.stop_clock();
90 if let Some(subscription) = this.visibility_subscription.borrow_mut().take() {
91 subscription.dispose();
92 }
93 Self::parent_on_detached_from_visual_tree(this, event);
94 }
95 fn render(this: &Self, dc: &mut DrawingContext) {
96 let size = this.bounds().size();
97 let width = size.width.min(size.height);
98 let brush = this.fill();
99 if !width.is_finite() || width <= 0.0 || brush.is_none() {
100 return;
101 }
102 let motion = this.motion.borrow();
103 let (fraction, gravity) = if animations_enabled(this.reduced_motion()) {
104 (motion.draw.current().value, motion.gravity.current().value)
105 } else {
106 mark_targets(this.is_checked())
107 };
108 let points = mark_path(fraction, gravity, width);
109 if points.len() < 2 {
110 return;
111 }
112 let offset = Point::new((size.width - width) / 2.0, (size.height - width) / 2.0);
113 let point = |p: Point| Point::new(p.x + offset.x, p.y + offset.y);
114 let geometry = StreamGeometry::new();
115 let mut path = geometry.open();
116 path.begin_figure(point(points[0]), false);
117 for p in points.iter().skip(1) {
118 path.line_to(point(*p), true);
119 }
120 path.end_figure(false);
121 path.dispose();
122 let pen: Rc<dyn IPen> = Rc::new(ImmutablePen::new(
123 brush.as_ref().map(BrushExtensions::to_immutable),
124 2.0,
125 None,
126 PenLineCap::Square,
127 PenLineJoin::Miter,
128 4.0,
129 ));
130 dc.draw_geometry(None, Some(&pen), &geometry.upcast());
131 }
132}
133ferro_properties! {impl MaterialSelectionMark {
134 pub fn is_checked_property()->StyledProperty<Option<bool>>{FerroProperty::register::<MaterialSelectionMark,_>("IsChecked",Some(false))}
135 pub fn fill_property()->StyledProperty<Option<Rc<dyn IBrush>>>{FerroProperty::register::<MaterialSelectionMark,_>("Fill",None)}
136 pub fn reduced_motion_property()->StyledProperty<bool>{FerroProperty::register::<MaterialSelectionMark,_>("ReducedMotion",false)}
137 pub fn motion_scheme_property()->StyledProperty<MaterialMotionScheme>{FerroProperty::register::<MaterialSelectionMark,_>("MotionScheme",MaterialMotionScheme::Expressive)}
138}}
139impl MaterialSelectionMark {
140 fn static_constructor() {
141 Visual::affects_render::<MaterialSelectionMark>(&[Self::fill_property().as_property()]);
142 }
143 pub fn construct() -> Self {
144 Self {
145 base: Control::construct(),
146 motion: RefCell::new(MarkMotion::new()),
147 clock_subscription: RefCell::new(None),
148 visibility_subscription: RefCell::new(None),
149 last_tick: Cell::new(None),
150 }
151 }
152 pub fn new() -> Ref<Self> {
153 let this = instantiate(Self::construct());
154 this.set_is_hit_test_visible(false);
155 this
156 }
157 pub fn is_checked(&self) -> Option<bool> {
158 self.get_value(Self::is_checked_property())
159 }
160 pub fn set_is_checked(&self, value: Option<bool>) {
161 self.set_value(Self::is_checked_property(), value);
162 }
163 pub fn fill(&self) -> Option<Rc<dyn IBrush>> {
164 self.get_value(Self::fill_property())
165 }
166 pub fn set_fill(&self, value: Option<Rc<dyn IBrush>>) {
167 self.set_value(Self::fill_property(), value);
168 }
169 pub fn reduced_motion(&self) -> bool {
170 self.get_value(Self::reduced_motion_property())
171 }
172 pub fn set_reduced_motion(&self, value: bool) {
173 self.set_value(Self::reduced_motion_property(), value);
174 }
175 pub fn motion_scheme(&self) -> MaterialMotionScheme {
176 self.get_value(Self::motion_scheme_property())
177 }
178 pub fn set_motion_scheme(&self, value: MaterialMotionScheme) {
179 self.set_value(Self::motion_scheme_property(), value);
180 }
181 fn retarget(&self) {
182 let reduced =
183 !animations_enabled(self.reduced_motion()) || !self.is_attached_to_visual_tree();
184 self.motion
185 .borrow_mut()
186 .retarget(self.is_checked(), self.motion_scheme(), reduced);
187 self.update_clock();
188 }
189 fn update_clock(&self) {
190 if !self.is_attached_to_visual_tree()
191 || !self.is_effectively_visible()
192 || !animations_enabled(self.reduced_motion())
193 || !self.motion.borrow().running()
194 {
195 self.stop_clock();
196 return;
197 }
198 if self.clock_subscription.borrow().is_some() {
199 return;
200 }
201 let Some(clock) = FerroLocator::current().get_service::<dyn IGlobalClock>() else {
202 return;
203 };
204 self.last_tick.set(None);
205 let weak = self.to_ref().downgrade();
206 *self.clock_subscription.borrow_mut() = Some(clock.subscribe(Rc::new(
207 AnonymousObserver::new(move |time: TimeSpan| {
208 if let Some(this) = weak.upgrade() {
209 if !animations_enabled(this.reduced_motion()) {
210 this.retarget();
211 this.invalidate_visual();
212 return;
213 }
214 let time = time.total_seconds();
215 if !time.is_finite() {
216 return;
217 }
218 let delta = this
219 .last_tick
220 .replace(Some(time))
221 .map_or(0.0, |previous| (time - previous).max(0.0));
222 this.motion.borrow_mut().advance(delta);
223 this.invalidate_visual();
224 this.update_clock();
225 }
226 }),
227 )));
228 }
229 fn stop_clock(&self) {
230 if let Some(subscription) = self.clock_subscription.borrow_mut().take() {
231 subscription.dispose();
232 }
233 self.last_tick.set(None);
234 }
235}
236impl Drop for MaterialSelectionMark {
237 fn drop(&mut self) {
238 if let Some(subscription) = self.clock_subscription.get_mut().take() {
239 subscription.dispose();
240 }
241 if let Some(subscription) = self.visibility_subscription.get_mut().take() {
242 subscription.dispose();
243 }
244 }
245}
246fn mark_targets(state: Option<bool>) -> (f64, f64) {
247 match state {
248 Some(false) => (0.0, 0.0),
249 Some(true) => (1.0, 0.0),
250 None => (1.0, 1.0),
251 }
252}
253struct MarkMotion {
254 state: Option<bool>,
255 draw: MaterialSpringState,
256 gravity: MaterialSpringState,
257 off_delay: Option<f64>,
258}
259impl MarkMotion {
260 fn new() -> Self {
261 let spec = MaterialMotionScheme::Expressive.spring(MaterialMotionRole::DefaultSpatial);
262 Self {
263 state: Some(false),
264 draw: MaterialSpringState::new(spec, 0.0),
265 gravity: MaterialSpringState::new(spec, 0.0),
266 off_delay: None,
267 }
268 }
269 fn retarget(&mut self, state: Option<bool>, scheme: MaterialMotionScheme, reduced: bool) {
270 let (draw, gravity) = mark_targets(state);
271 let previous = self.state;
272 let spec = scheme.spring(MaterialMotionRole::DefaultSpatial);
273 self.draw.set_spec(spec);
274 self.gravity.set_spec(spec);
275 self.draw.set_reduced_motion(reduced);
276 self.gravity.set_reduced_motion(reduced);
277 if reduced {
278 self.draw.retarget(draw);
279 self.gravity.retarget(gravity);
280 self.draw.finish();
281 self.gravity.finish();
282 self.off_delay = None;
283 } else if state == Some(false) && previous != Some(false) {
284 self.off_delay = Some(0.1);
285 } else if state != Some(false) {
286 self.off_delay = None;
287 self.draw.retarget(draw);
288 self.gravity.retarget(gravity);
289 if previous == Some(false) {
290 self.gravity.finish();
291 }
292 }
293 self.state = state;
294 }
295 fn advance(&mut self, delta: f64) {
296 if let Some(remaining) = self.off_delay {
297 if remaining - delta <= 1e-9 {
298 self.off_delay = None;
299 self.draw.retarget(0.0);
300 self.gravity.retarget(0.0);
301 self.draw.finish();
302 self.gravity.finish();
303 } else {
304 self.off_delay = Some(remaining - delta);
305 }
306 } else {
307 self.draw.advance(delta);
308 self.gravity.advance(delta);
309 if self.draw.is_at_rest(0.01, 0.625) {
310 self.draw.finish();
311 }
312 if self.gravity.is_at_rest(0.01, 0.625) {
313 self.gravity.finish();
314 }
315 }
316 }
317 fn running(&self) -> bool {
318 self.off_delay.is_some()
319 || !self.draw.is_at_rest(0.01, 0.625)
320 || !self.gravity.is_at_rest(0.01, 0.625)
321 }
322}
323fn mark_path(fraction: f64, gravity: f64, width: f64) -> Vec<Point> {
324 let points = [
325 Point::new(width * 0.25, width * 0.5),
326 Point::new(
327 width * (0.4 + 0.1 * gravity),
328 width * (0.65 - 0.15 * gravity),
329 ),
330 Point::new(width * 0.75, width * (0.3 + 0.2 * gravity)),
331 ];
332 let length = |a: Point, b: Point| (b.x - a.x).hypot(b.y - a.y);
333 let first = length(points[0], points[1]);
334 let second = length(points[1], points[2]);
335 let mut remaining = (first + second) * fraction.clamp(0.0, 1.0);
336 if remaining <= 0.0 {
337 return Vec::new();
338 }
339 let mut output = vec![points[0]];
340 for i in 0..2 {
341 let span = if i == 0 { first } else { second };
342 let t = (remaining / span).clamp(0.0, 1.0);
343 output.push(Point::new(
344 points[i].x + (points[i + 1].x - points[i].x) * t,
345 points[i].y + (points[i + 1].y - points[i].y) * t,
346 ));
347 remaining -= span;
348 if remaining <= 0.0 {
349 break;
350 }
351 }
352 output
353}
354#[cfg(test)]
355mod tests {
356 use super::*;
357 #[test]
358 fn native_check_reveals_by_length_and_gravitates_to_dash() {
359 let check = mark_path(1.0, 0.0, 18.0);
360 assert_eq!(3, check.len());
361 assert_eq!(Point::new(4.5, 9.0), check[0]);
362 assert!((check[1].y - 11.7).abs() < 1e-12);
363 assert_eq!(13.5, check[2].x);
364 assert!((check[2].y - 5.4).abs() < 1e-12);
365 let dash = mark_path(1.0, 1.0, 18.0);
366 assert!(dash.iter().all(|p| (p.y - 9.0).abs() < 1e-12));
367 let half = mark_path(0.5, 1.0, 18.0);
368 assert_eq!(Point::new(9.0, 9.0), *half.last().unwrap());
369 }
370 #[test]
371 fn uncheck_holds_mark_for_native_100ms_then_snaps() {
372 let mut motion = MarkMotion::new();
373 motion.retarget(Some(true), MaterialMotionScheme::Expressive, true);
374 motion.retarget(Some(false), MaterialMotionScheme::Expressive, false);
375 motion.advance(0.099);
376 assert_eq!(1.0, motion.draw.current().value);
377 motion.advance(0.001);
378 assert_eq!(0.0, motion.draw.current().value);
379 assert!(!motion.running());
380 }
381 #[test]
382 fn indeterminate_check_morph_and_reduced_motion_do_not_leave_clock_work() {
383 let mut motion = MarkMotion::new();
384 motion.retarget(Some(true), MaterialMotionScheme::Expressive, true);
385 motion.retarget(None, MaterialMotionScheme::Expressive, false);
386 motion.advance(0.04);
387 assert!(motion.gravity.current().value > 0.0 && motion.gravity.current().value < 1.0);
388 motion.retarget(None, MaterialMotionScheme::Expressive, true);
389 assert_eq!(1.0, motion.gravity.current().value);
390 assert!(!motion.running());
391 }
392}