1use std::ops::Deref;
15
16use std::any::{Any, TypeId};
17use std::cell::RefCell;
18use std::collections::HashMap;
19use std::hash::{Hash, Hasher};
20use std::sync::OnceLock;
21
22use parking_lot::RwLock;
23
24use std::rc::Rc;
25
26use crate::Color;
27use crate::animation::{AnimationSpec, Easing};
28use crate::indication::IndicationNodeFactory;
29use web_time::Duration;
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
32pub enum TextDirection {
33 #[default]
34 Ltr,
35 Rtl,
36}
37
38thread_local! {
39 static LOCALS_STACK: RefCell<Vec<HashMap<TypeId, Box<dyn Any>>>> = RefCell::new(Vec::new());
40}
41
42#[derive(Clone, Copy)]
43pub(crate) enum LocalId {
44 Theme,
45 Density,
46 UiScale,
47 TextScale,
48 TextDirection,
49 WindowInsets,
50 WindowSizeClass,
51 ContainerWidth,
52 ContainerHeight,
53 ContentColor,
54 TextSize,
55 Indication,
56 InputMode,
57}
58
59impl LocalId {
60 fn index(self) -> usize {
61 self as usize
62 }
63}
64
65#[derive(Clone, Copy, Debug, Default)]
66struct Defaults {
67 theme: Theme,
68 text_direction: TextDirection,
69 ui_scale: UiScale,
70 text_scale: TextScale,
71 density: Density,
72 window_insets: WindowInsets,
73 window_size_class: WindowSizeClass,
74 container_width: f32,
76 container_height: f32,
77}
78
79static DEFAULTS: OnceLock<RwLock<Defaults>> = OnceLock::new();
80
81fn defaults() -> &'static RwLock<Defaults> {
82 DEFAULTS.get_or_init(|| {
83 RwLock::new(Defaults {
84 container_width: 360.0,
85 container_height: 800.0,
86 ..Default::default()
87 })
88 })
89}
90
91pub fn set_theme_default(t: Theme) {
93 let changed = {
94 let mut defaults = defaults().write();
95 let changed = fingerprint_theme(&defaults.theme) != fingerprint_theme(&t);
96 defaults.theme = t;
97 changed
98 };
99 if changed {
100 crate::request_frame();
101 }
102}
103
104pub fn set_text_direction_default(d: TextDirection) {
106 let changed = {
107 let mut defaults = defaults().write();
108 let changed = defaults.text_direction != d;
109 defaults.text_direction = d;
110 changed
111 };
112 if changed {
113 crate::request_frame();
114 }
115}
116
117pub fn set_ui_scale_default(s: UiScale) {
119 let value = UiScale(s.0.max(0.0));
120 let changed = {
121 let mut defaults = defaults().write();
122 let changed = defaults.ui_scale.0.to_bits() != value.0.to_bits();
123 defaults.ui_scale = value;
124 changed
125 };
126 if changed {
127 crate::request_frame();
128 }
129}
130
131pub fn set_text_scale_default(s: TextScale) {
133 let value = TextScale(s.0.max(0.0));
134 let changed = {
135 let mut defaults = defaults().write();
136 let changed = defaults.text_scale.0.to_bits() != value.0.to_bits();
137 defaults.text_scale = value;
138 changed
139 };
140 if changed {
141 crate::request_frame();
142 }
143}
144
145pub fn set_density_default(d: Density) {
148 let value = Density {
149 scale: d.scale.max(0.0),
150 };
151 let changed = {
152 let mut defaults = defaults().write();
153 let changed = defaults.density.scale.to_bits() != value.scale.to_bits();
154 defaults.density = value;
155 changed
156 };
157 if changed {
158 crate::request_frame();
159 }
160}
161
162pub use crate::units::{Dp, DpOffset, DpRect, DpSize, Px, Sp, UnitExt};
163
164#[inline]
166pub fn effective_density_scale() -> f32 {
167 (density().scale * ui_scale().0).max(0.0001)
168}
169
170#[inline]
172pub fn dp_to_px(dp: Dp) -> Px {
173 dp.to_px()
174}
175
176#[inline]
178pub fn px_to_dp(px: Px) -> Dp {
179 px.to_dp()
180}
181
182fn with_locals_frame<R>(f: impl FnOnce() -> R) -> R {
183 struct Guard;
184 impl Drop for Guard {
185 fn drop(&mut self) {
186 let frame = LOCALS_STACK
187 .try_with(|stack| {
188 stack
189 .try_borrow_mut()
190 .ok()
191 .and_then(|mut stack| stack.pop())
192 })
193 .ok()
194 .flatten();
195 drop(frame);
196 }
197 }
198 LOCALS_STACK.with(|stack| stack.borrow_mut().push(HashMap::new()));
199 let _guard = Guard;
200 f()
201}
202
203fn set_local_boxed(t: TypeId, value: Box<dyn Any>) {
204 let old = LOCALS_STACK
205 .try_with(|stack| {
206 let mut stack = stack.try_borrow_mut().ok()?;
207 let old = stack.last_mut()?.insert(t, value);
208 Some(old)
209 })
210 .ok()
211 .flatten();
212 drop(old);
213}
214
215fn get_local<T: 'static + Copy>() -> Option<T> {
216 LOCALS_STACK.with(|stack| {
217 for frame in stack.borrow().iter().rev() {
218 if let Some(value) = frame.get(&TypeId::of::<T>())
219 && let Some(value) = value.downcast_ref::<T>()
220 {
221 return Some(*value);
222 }
223 }
224 None
225 })
226}
227
228fn record_local<T: 'static>(id: LocalId, value: &T, fingerprint: impl FnOnce(&T) -> u64) {
229 crate::scope_cache::record_scope_local_read(id.index(), fingerprint(value));
230}
231
232fn fingerprint_color(value: &Color) -> u64 {
233 let mut hasher = std::collections::hash_map::DefaultHasher::new();
234 value.0.hash(&mut hasher);
235 value.1.hash(&mut hasher);
236 value.2.hash(&mut hasher);
237 value.3.hash(&mut hasher);
238 hasher.finish()
239}
240
241fn fingerprint_theme(value: &Theme) -> u64 {
242 let mut hasher = std::collections::hash_map::DefaultHasher::new();
243 format!("{value:?}").hash(&mut hasher);
244 hasher.finish()
245}
246
247fn fingerprint_text_size(value: Option<Sp>) -> u64 {
248 value.map(|size| size.0.to_bits() as u64).unwrap_or(0)
249}
250
251fn fingerprint_input_mode(value: crate::input::InputMode) -> u64 {
252 let mut hasher = std::collections::hash_map::DefaultHasher::new();
253 std::mem::discriminant(&value).hash(&mut hasher);
254 hasher.finish()
255}
256
257fn fingerprint_f32(value: f32) -> u64 {
258 value.to_bits() as u64
259}
260
261fn fingerprint_window_insets(value: &WindowInsets) -> u64 {
262 let mut hasher = std::collections::hash_map::DefaultHasher::new();
263 value.top.to_bits().hash(&mut hasher);
264 value.bottom.to_bits().hash(&mut hasher);
265 value.left.to_bits().hash(&mut hasher);
266 value.right.to_bits().hash(&mut hasher);
267 value.ime_bottom.to_bits().hash(&mut hasher);
268 hasher.finish()
269}
270
271fn fingerprint_window_size_class(value: &WindowSizeClass) -> u64 {
272 let mut hasher = std::collections::hash_map::DefaultHasher::new();
273 value.width.hash(&mut hasher);
274 value.height.hash(&mut hasher);
275 hasher.finish()
276}
277
278fn fingerprint_text_direction(value: &TextDirection) -> u64 {
279 let mut hasher = std::collections::hash_map::DefaultHasher::new();
280 std::mem::discriminant(value).hash(&mut hasher);
281 hasher.finish()
282}
283
284fn fingerprint_indication(value: &Option<Rc<dyn IndicationNodeFactory>>) -> u64 {
285 value
286 .as_ref()
287 .map(|value| Rc::as_ptr(value) as *const () as usize as u64)
288 .unwrap_or(0)
289}
290
291#[derive(Clone, Copy, Debug)]
292#[must_use]
293pub struct ColorScheme {
294 pub primary: Color,
295 pub on_primary: Color,
296 pub primary_container: Color,
297 pub on_primary_container: Color,
298
299 pub secondary: Color,
300 pub on_secondary: Color,
301 pub secondary_container: Color,
302 pub on_secondary_container: Color,
303
304 pub tertiary: Color,
305 pub on_tertiary: Color,
306 pub tertiary_container: Color,
307 pub on_tertiary_container: Color,
308
309 pub error: Color,
310 pub on_error: Color,
311 pub error_container: Color,
312 pub on_error_container: Color,
313
314 pub background: Color,
315 pub on_background: Color,
316 pub surface: Color,
317 pub on_surface: Color,
318 pub surface_variant: Color,
319 pub on_surface_variant: Color,
320 pub surface_container_lowest: Color,
321 pub surface_container_low: Color,
322 pub surface_container: Color,
323 pub surface_container_high: Color,
324 pub surface_container_highest: Color,
325 pub surface_bright: Color,
326 pub surface_dim: Color,
327 pub surface_tint: Color,
328
329 pub inverse_surface: Color,
330 pub inverse_on_surface: Color,
331 pub inverse_primary: Color,
332
333 pub outline: Color,
334 pub outline_variant: Color,
335
336 pub scrim: Color,
337 pub shadow: Color,
338 pub focus: Color,
339}
340
341impl ColorScheme {
342 pub fn dark() -> Self {
343 Self {
344 primary: Color::from_hex("#69FDBE"),
345 on_primary: Color::from_hex("#003020"),
346 primary_container: Color::from_hex("#004D40"),
347 on_primary_container: Color::from_hex("#6FF7F6"),
348
349 secondary: Color::from_hex("#B3C9A7"),
350 on_secondary: Color::from_hex("#1C3519"),
351 secondary_container: Color::from_hex("#334D2E"),
352 on_secondary_container: Color::from_hex("#CCE8B3"),
353
354 tertiary: Color::from_hex("#FFC9C1"),
355 on_tertiary: Color::from_hex("#3F1619"),
356 tertiary_container: Color::from_hex("#5D1F22"),
357 on_tertiary_container: Color::from_hex("#FFDBD8"),
358
359 error: Color::from_hex("#F2B8B5"),
360 on_error: Color::from_hex("#601410"),
361 error_container: Color::from_hex("#8C1D18"),
362 on_error_container: Color::from_hex("#F9DEDC"),
363
364 background: Color::from_hex("#1A1C1E"),
365 on_background: Color::from_hex("#E6E1E5"),
366 surface: Color::from_hex("#1A1C1E"),
367 on_surface: Color::from_hex("#E6E1E5"),
368 surface_variant: Color::from_hex("#44474E"),
369 on_surface_variant: Color::from_hex("#C4C6CE"),
370 surface_container_lowest: Color::from_hex("#0A0A0C"),
371 surface_container_low: Color::from_hex("#141115"),
372 surface_container: Color::from_hex("#19131A"),
373 surface_container_high: Color::from_hex("#1F1B22"),
374 surface_container_highest: Color::from_hex("#2A2930"),
375 surface_bright: Color::from_hex("#26292F"),
376 surface_dim: Color::from_hex("#1A1C1E"),
377 surface_tint: Color::from_hex("#69FDBE"),
378
379 inverse_surface: Color::from_hex("#E6E1E5"),
380 inverse_on_surface: Color::from_hex("#2A2930"),
381 inverse_primary: Color::from_hex("#005048"),
382
383 outline: Color::from_hex("#74777F"),
384 outline_variant: Color::from_hex("#44474E"),
385
386 scrim: Color::from_hex("#000000"),
387 shadow: Color::from_hex("#000000"),
388 focus: Color::from_hex("#006A6A"),
389 }
390 }
391
392 pub fn light() -> Self {
393 Self {
394 primary: Color::from_hex("#006A6A"),
395 on_primary: Color::WHITE,
396 primary_container: Color::from_hex("#9EF0EC"),
397 on_primary_container: Color::from_hex("#002020"),
398
399 secondary: Color::from_hex("#586146"),
400 on_secondary: Color::WHITE,
401 secondary_container: Color::from_hex("#D8E3B8"),
402 on_secondary_container: Color::from_hex("#161C0A"),
403
404 tertiary: Color::from_hex("#744639"),
405 on_tertiary: Color::WHITE,
406 tertiary_container: Color::from_hex("#FFD9CD"),
407 on_tertiary_container: Color::from_hex("#2C0E07"),
408
409 error: Color::from_hex("#BA1A1A"),
410 on_error: Color::WHITE,
411 error_container: Color::from_hex("#FFDAD6"),
412 on_error_container: Color::from_hex("#410002"),
413
414 background: Color::from_hex("#FEF7FF"),
415 on_background: Color::from_hex("#1A1C1E"),
416 surface: Color::from_hex("#FEF7FF"),
417 on_surface: Color::from_hex("#1A1C1E"),
418 surface_variant: Color::from_hex("#E1E3DE"),
419 on_surface_variant: Color::from_hex("#44474E"),
420 surface_container_lowest: Color::WHITE,
421 surface_container_low: Color::from_hex("#F4F5F0"),
422 surface_container: Color::from_hex("#EEF0E9"),
423 surface_container_high: Color::from_hex("#E9EAE4"),
424 surface_container_highest: Color::from_hex("#E3E5DF"),
425 surface_bright: Color::from_hex("#FEF7FF"),
426 surface_dim: Color::from_hex("#DEDAD0"),
427 surface_tint: Color::from_hex("#006A6A"),
428
429 inverse_surface: Color::from_hex("#2F3033"),
430 inverse_on_surface: Color::from_hex("#F1F0F4"),
431 inverse_primary: Color::from_hex("#69FDBE"),
432
433 outline: Color::from_hex("#74777F"),
434 outline_variant: Color::from_hex("#C4C6CE"),
435
436 scrim: Color::from_hex("#000000"),
437 shadow: Color::from_hex("#000000"),
438 focus: Color::from_hex("#1D4ED8"),
439 }
440 }
441
442 pub fn content_color_for(self, background: Color) -> Option<Color> {
445 if background == self.primary {
446 Some(self.on_primary)
447 } else if background == self.primary_container {
448 Some(self.on_primary_container)
449 } else if background == self.secondary {
450 Some(self.on_secondary)
451 } else if background == self.secondary_container {
452 Some(self.on_secondary_container)
453 } else if background == self.tertiary {
454 Some(self.on_tertiary)
455 } else if background == self.tertiary_container {
456 Some(self.on_tertiary_container)
457 } else if background == self.background {
458 Some(self.on_background)
459 } else if background == self.surface
460 || background == self.surface_bright
461 || background == self.surface_dim
462 || background == self.surface_container
463 || background == self.surface_container_low
464 || background == self.surface_container_high
465 || background == self.surface_container_highest
466 {
467 Some(self.on_surface)
468 } else if background == self.inverse_surface {
469 Some(self.inverse_on_surface)
470 } else if background == self.error {
471 Some(self.on_error)
472 } else if background == self.error_container {
473 Some(self.on_error_container)
474 } else {
475 None
476 }
477 }
478}
479
480impl Default for ColorScheme {
481 fn default() -> Self {
482 Self::dark()
483 }
484}
485
486#[derive(Clone, Copy, Debug)]
488#[must_use]
489pub struct Typography {
490 pub display_large: Sp,
491 pub display_medium: Sp,
492 pub display_small: Sp,
493 pub headline_large: Sp,
494 pub headline_medium: Sp,
495 pub headline_small: Sp,
496 pub title_large: Sp,
497 pub title_medium: Sp,
498 pub title_small: Sp,
499 pub body_large: Sp,
500 pub body_medium: Sp,
501 pub body_small: Sp,
502 pub label_large: Sp,
503 pub label_medium: Sp,
504 pub label_small: Sp,
505}
506
507impl Default for Typography {
508 fn default() -> Self {
509 Self {
510 display_large: Sp(57.0),
511 display_medium: Sp(45.0),
512 display_small: Sp(36.0),
513 headline_large: Sp(32.0),
514 headline_medium: Sp(28.0),
515 headline_small: Sp(24.0),
516 title_large: Sp(22.0),
517 title_medium: Sp(16.0),
518 title_small: Sp(14.0),
519 body_large: Sp(16.0),
520 body_medium: Sp(14.0),
521 body_small: Sp(12.0),
522 label_large: Sp(14.0),
523 label_medium: Sp(12.0),
524 label_small: Sp(11.0),
525 }
526 }
527}
528
529#[derive(Clone, Copy, Debug)]
531#[must_use]
532pub struct Shapes {
533 pub extra_small: Dp,
534 pub small: Dp,
535 pub medium: Dp,
536 pub large: Dp,
537 pub extra_large: Dp,
538}
539
540impl Default for Shapes {
541 fn default() -> Self {
542 Self {
543 extra_small: Dp(4.0),
544 small: Dp(8.0),
545 medium: Dp(12.0),
546 large: Dp(16.0),
547 extra_large: Dp(28.0),
548 }
549 }
550}
551
552#[derive(Clone, Copy, Debug)]
554#[must_use]
555pub struct Spacing {
556 pub xs: Dp,
557 pub sm: Dp,
558 pub md: Dp,
559 pub lg: Dp,
560 pub xl: Dp,
561 pub xxl: Dp,
562}
563
564impl Default for Spacing {
565 fn default() -> Self {
566 Self {
567 xs: Dp(4.0),
568 sm: Dp(8.0),
569 md: Dp(12.0),
570 lg: Dp(16.0),
571 xl: Dp(24.0),
572 xxl: Dp(32.0),
573 }
574 }
575}
576
577#[derive(Clone, Copy, Debug)]
579#[must_use]
580pub struct Elevation {
581 pub level0: Dp,
582 pub level1: Dp,
583 pub level2: Dp,
584 pub level3: Dp,
585 pub level4: Dp,
586 pub level5: Dp,
587}
588
589impl Default for Elevation {
590 fn default() -> Self {
591 Self {
592 level0: Dp(0.0),
593 level1: Dp(1.0),
594 level2: Dp(3.0),
595 level3: Dp(6.0),
596 level4: Dp(8.0),
597 level5: Dp(12.0),
598 }
599 }
600}
601
602#[derive(Clone, Copy, Debug)]
604#[must_use]
605pub struct MotionScheme {
606 pub shape: AnimationSpec,
609 pub color: AnimationSpec,
612 pub color_fast: AnimationSpec,
615 pub overlay: AnimationSpec,
618 pub spring: AnimationSpec,
621 pub expand: AnimationSpec,
624 pub layout: AnimationSpec,
627}
628
629impl Default for MotionScheme {
630 fn default() -> Self {
631 Self {
632 shape: AnimationSpec::tween(Duration::from_millis(200), Easing::FastOutSlowIn),
633 color: AnimationSpec::tween(Duration::from_millis(150), Easing::FastOutSlowIn),
634 color_fast: AnimationSpec::tween(Duration::from_millis(100), Easing::FastOutSlowIn),
635 overlay: AnimationSpec::tween(Duration::from_millis(120), Easing::FastOutSlowIn),
636 spring: AnimationSpec::spring_gentle(),
637 expand: AnimationSpec::tween(Duration::from_millis(250), Easing::FastOutSlowIn),
638 layout: AnimationSpec::tween(Duration::from_millis(300), Easing::EaseOut),
639 }
640 }
641}
642
643#[derive(Clone, Copy, Debug)]
644#[must_use]
645pub struct Theme {
646 pub colors: ColorScheme,
647 pub typography: Typography,
648 pub shapes: Shapes,
649 pub spacing: Spacing,
650 pub elevation: Elevation,
651 pub motion: MotionScheme,
652
653 pub focus: Color,
654 pub scrollbar_track: Color,
655 pub scrollbar_thumb: Color,
656 pub button_bg: Color,
657 pub button_bg_hover: Color,
658 pub button_bg_pressed: Color,
659}
660
661impl Deref for Theme {
662 type Target = ColorScheme;
663 fn deref(&self) -> &Self::Target {
664 &self.colors
665 }
666}
667
668impl Default for Theme {
669 fn default() -> Self {
670 let colors = ColorScheme::default();
671 Self {
672 colors,
673 typography: Typography::default(),
674 shapes: Shapes::default(),
675 spacing: Spacing::default(),
676 elevation: Elevation::default(),
677 motion: MotionScheme::default(),
678 focus: colors.focus,
679 scrollbar_track: Color::TRANSPARENT,
680 scrollbar_thumb: colors.outline.with_alpha(179),
681 button_bg: colors.primary,
682 button_bg_hover: colors.primary_container,
683 button_bg_pressed: colors.secondary_container,
684 }
685 }
686}
687
688impl Theme {
689 pub fn with_colors(mut self, colors: ColorScheme) -> Self {
690 self.colors = colors;
691 self
692 }
693
694 pub fn dark() -> Self {
696 Self::default().with_colors(ColorScheme::dark())
697 }
698
699 pub fn light() -> Self {
702 let colors = ColorScheme::light();
703 Self {
704 focus: colors.focus,
705 scrollbar_thumb: colors.outline.with_alpha(179),
706 button_bg: colors.primary,
707 button_bg_hover: colors.primary_container,
708 button_bg_pressed: colors.secondary_container,
709 colors,
710 ..Self::default()
711 }
712 }
713
714 pub fn is_dark(&self) -> bool {
717 self.colors.background.is_dark()
718 }
719}
720
721#[derive(Clone, Copy, Debug)]
723pub struct Density {
724 pub scale: f32,
725}
726impl Default for Density {
727 fn default() -> Self {
728 Self { scale: 1.0 }
729 }
730}
731
732#[derive(Clone, Copy, Debug)]
734pub struct UiScale(pub f32);
735impl Default for UiScale {
736 fn default() -> Self {
737 Self(1.0)
738 }
739}
740
741#[derive(Clone, Copy, Debug)]
742pub struct TextScale(pub f32);
743impl Default for TextScale {
744 fn default() -> Self {
745 Self(1.0)
746 }
747}
748
749pub fn with_theme<R>(theme: Theme, f: impl FnOnce() -> R) -> R {
750 with_locals_frame(|| {
751 set_local_boxed(TypeId::of::<Theme>(), Box::new(theme));
752 f()
753 })
754}
755
756pub fn with_density<R>(density: Density, f: impl FnOnce() -> R) -> R {
757 with_locals_frame(|| {
758 set_local_boxed(TypeId::of::<Density>(), Box::new(density));
759 f()
760 })
761}
762
763pub fn with_ui_scale<R>(s: UiScale, f: impl FnOnce() -> R) -> R {
764 with_locals_frame(|| {
765 set_local_boxed(TypeId::of::<UiScale>(), Box::new(s));
766 f()
767 })
768}
769
770pub fn with_text_scale<R>(ts: TextScale, f: impl FnOnce() -> R) -> R {
771 with_locals_frame(|| {
772 set_local_boxed(TypeId::of::<TextScale>(), Box::new(ts));
773 f()
774 })
775}
776
777pub fn with_text_direction<R>(dir: TextDirection, f: impl FnOnce() -> R) -> R {
778 with_locals_frame(|| {
779 set_local_boxed(TypeId::of::<TextDirection>(), Box::new(dir));
780 f()
781 })
782}
783
784pub fn with_window_insets<R>(insets: WindowInsets, f: impl FnOnce() -> R) -> R {
785 with_locals_frame(|| {
786 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
787 f()
788 })
789}
790
791#[derive(Clone, Copy, Debug)]
792pub struct ContentColor(pub Color);
793
794pub fn with_content_color<R>(color: Color, f: impl FnOnce() -> R) -> R {
795 with_locals_frame(|| {
796 set_local_boxed(TypeId::of::<ContentColor>(), Box::new(ContentColor(color)));
797 f()
798 })
799}
800
801pub fn content_color() -> Color {
802 let value = get_local::<ContentColor>()
803 .map(|color| color.0)
804 .unwrap_or_else(|| {
805 get_local::<Theme>()
806 .unwrap_or_else(|| defaults().read().theme)
807 .on_surface
808 });
809 record_local(LocalId::ContentColor, &value, fingerprint_color);
810 value
811}
812
813pub fn content_color_for(background: Color) -> Color {
817 if let Some(c) = theme().colors.content_color_for(background) {
818 return c;
819 }
820 let local = content_color();
821 if (background.relative_luminance() - local.relative_luminance()).abs() < 0.25 {
823 if background.is_dark() {
824 Color::WHITE
825 } else {
826 Color::BLACK
827 }
828 } else {
829 local
830 }
831}
832
833#[derive(Clone, Copy, Debug)]
836pub struct TextSize(pub Sp);
837
838pub fn with_text_size<R>(size: Sp, f: impl FnOnce() -> R) -> R {
839 with_locals_frame(|| {
840 set_local_boxed(TypeId::of::<TextSize>(), Box::new(TextSize(size)));
841 f()
842 })
843}
844
845pub fn text_size() -> Option<Sp> {
846 let value = get_local::<TextSize>().map(|size| size.0);
847 record_local(LocalId::TextSize, &value, |value| {
848 fingerprint_text_size(*value)
849 });
850 value
851}
852
853#[derive(Clone, Debug, Default)]
857pub struct LocalIndication(pub Option<Rc<dyn IndicationNodeFactory>>);
858
859pub fn with_local_indication<R>(
860 indication: Option<Rc<dyn IndicationNodeFactory>>,
861 f: impl FnOnce() -> R,
862) -> R {
863 with_locals_frame(|| {
864 set_local_boxed(
865 std::any::TypeId::of::<LocalIndication>(),
866 Box::new(LocalIndication(indication)),
867 );
868 f()
869 })
870}
871
872#[derive(Clone, Copy, Debug)]
875struct LocalInputMode(pub crate::input::InputMode);
876
877pub fn with_input_mode<R>(mode: crate::input::InputMode, f: impl FnOnce() -> R) -> R {
879 with_locals_frame(|| {
880 set_local_boxed(
881 TypeId::of::<LocalInputMode>(),
882 Box::new(LocalInputMode(mode)),
883 );
884 f()
885 })
886}
887
888pub(crate) fn local_input_mode() -> Option<crate::input::InputMode> {
890 let value = get_local::<LocalInputMode>().map(|mode| mode.0);
891 let effective = value.unwrap_or_else(crate::input::default_input_mode);
892 record_local(LocalId::InputMode, &effective, |value| {
893 fingerprint_input_mode(*value)
894 });
895 value
896}
897
898fn raw_local_indication() -> Option<Rc<dyn IndicationNodeFactory>> {
899 LOCALS_STACK.with(|stack| {
900 for frame in stack.borrow().iter().rev() {
901 if let Some(value) = frame.get(&TypeId::of::<LocalIndication>())
902 && let Some(indication) = value.downcast_ref::<LocalIndication>()
903 {
904 return indication.0.clone();
905 }
906 }
907 None
908 })
909}
910
911pub fn local_indication() -> Option<Rc<dyn IndicationNodeFactory>> {
912 let value = raw_local_indication();
913 record_local(LocalId::Indication, &value, fingerprint_indication);
914 value
915}
916
917#[derive(Clone, Copy, Debug, Default, PartialEq)]
919pub struct WindowInsets {
920 pub top: f32,
921 pub bottom: f32,
922 pub left: f32,
923 pub right: f32,
924 pub ime_bottom: f32,
927}
928
929pub fn set_window_insets_default(insets: WindowInsets) {
931 let changed = {
932 let mut defaults = defaults().write();
933 let old = defaults.window_insets;
934 let changed = old.top.to_bits() != insets.top.to_bits()
935 || old.bottom.to_bits() != insets.bottom.to_bits()
936 || old.left.to_bits() != insets.left.to_bits()
937 || old.right.to_bits() != insets.right.to_bits()
938 || old.ime_bottom.to_bits() != insets.ime_bottom.to_bits();
939 defaults.window_insets = insets;
940 changed
941 };
942 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets));
943 if changed {
944 crate::request_frame();
945 }
946}
947
948pub fn set_ime_inset(height_px: f32) {
951 let insets = {
952 let mut defaults = defaults().write();
953 let changed = defaults.window_insets.ime_bottom.to_bits() != height_px.to_bits();
954 defaults.window_insets.ime_bottom = height_px;
955 (changed, defaults.window_insets)
956 };
957 set_local_boxed(TypeId::of::<WindowInsets>(), Box::new(insets.1));
958 if insets.0 {
959 crate::request_frame();
960 }
961}
962
963pub fn window_insets() -> WindowInsets {
965 let value = get_local::<WindowInsets>().unwrap_or_else(|| defaults().read().window_insets);
966 record_local(LocalId::WindowInsets, &value, fingerprint_window_insets);
967 value
968}
969
970pub fn set_window_container_size(width_dp: f32, height_dp: f32) {
973 let changed = {
974 let mut defaults = defaults().write();
975 let changed = defaults.container_width.to_bits() != width_dp.to_bits()
976 || defaults.container_height.to_bits() != height_dp.to_bits();
977 defaults.container_width = width_dp;
978 defaults.container_height = height_dp;
979 changed
980 };
981 if changed {
982 crate::request_frame();
983 }
984}
985
986pub fn set_window_container_width(w_dp: f32) {
989 let changed = {
990 let mut defaults = defaults().write();
991 let changed = defaults.container_width.to_bits() != w_dp.to_bits();
992 defaults.container_width = w_dp;
993 changed
994 };
995 if changed {
996 crate::request_frame();
997 }
998}
999
1000pub fn set_window_container_height(h_dp: f32) {
1003 let changed = {
1004 let mut defaults = defaults().write();
1005 let changed = defaults.container_height.to_bits() != h_dp.to_bits();
1006 defaults.container_height = h_dp;
1007 changed
1008 };
1009 if changed {
1010 crate::request_frame();
1011 }
1012}
1013
1014pub fn get_window_container_width() -> f32 {
1016 let value = defaults().read().container_width;
1017 record_local(LocalId::ContainerWidth, &value, |value| {
1018 fingerprint_f32(*value)
1019 });
1020 value
1021}
1022
1023pub fn get_window_container_height() -> f32 {
1025 let value = defaults().read().container_height;
1026 record_local(LocalId::ContainerHeight, &value, |value| {
1027 fingerprint_f32(*value)
1028 });
1029 value
1030}
1031
1032#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
1040pub enum WidthClass {
1041 #[default]
1042 Compact,
1043 Medium,
1044 Expanded,
1045}
1046
1047#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
1055pub enum HeightClass {
1056 #[default]
1057 Compact,
1058 Medium,
1059 Expanded,
1060}
1061
1062#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
1068pub struct WindowSizeClass {
1069 pub width: WidthClass,
1070 pub height: HeightClass,
1071}
1072
1073impl WindowSizeClass {
1074 pub fn is_expanded_width(&self) -> bool {
1076 matches!(self.width, WidthClass::Expanded)
1077 }
1078 pub fn is_at_least_medium_width(&self) -> bool {
1081 matches!(self.width, WidthClass::Medium | WidthClass::Expanded)
1082 }
1083}
1084
1085pub fn calculate_window_size_class(
1088 width_px: u32,
1089 height_px: u32,
1090 density_scale: f32,
1091) -> WindowSizeClass {
1092 let density = density_scale.max(0.0001);
1093 let width_dp = (width_px as f32) / density;
1094 let height_dp = (height_px as f32) / density;
1095
1096 let width = if width_dp < 600.0 {
1097 WidthClass::Compact
1098 } else if width_dp < 840.0 {
1099 WidthClass::Medium
1100 } else {
1101 WidthClass::Expanded
1102 };
1103 let height = if height_dp < 480.0 {
1104 HeightClass::Compact
1105 } else if height_dp < 900.0 {
1106 HeightClass::Medium
1107 } else {
1108 HeightClass::Expanded
1109 };
1110
1111 WindowSizeClass { width, height }
1112}
1113
1114pub fn set_window_size_class_default(class: WindowSizeClass) {
1117 let changed = {
1118 let mut defaults = defaults().write();
1119 let changed = defaults.window_size_class != class;
1120 defaults.window_size_class = class;
1121 changed
1122 };
1123 if changed {
1124 crate::request_frame();
1125 }
1126}
1127
1128pub fn with_window_size_class<R>(class: WindowSizeClass, f: impl FnOnce() -> R) -> R {
1130 with_locals_frame(|| {
1131 set_local_boxed(TypeId::of::<WindowSizeClass>(), Box::new(class));
1132 f()
1133 })
1134}
1135
1136pub fn window_size_class() -> WindowSizeClass {
1139 let value =
1140 get_local::<WindowSizeClass>().unwrap_or_else(|| defaults().read().window_size_class);
1141 record_local(
1142 LocalId::WindowSizeClass,
1143 &value,
1144 fingerprint_window_size_class,
1145 );
1146 value
1147}
1148
1149pub fn theme() -> Theme {
1150 let value = get_local::<Theme>().unwrap_or_else(|| defaults().read().theme);
1151 record_local(LocalId::Theme, &value, fingerprint_theme);
1152 value
1153}
1154
1155pub fn theme_fingerprint() -> u64 {
1156 fingerprint_theme(&theme())
1157}
1158
1159pub fn theme_fingerprint_of(theme: &Theme) -> u64 {
1163 fingerprint_theme(theme)
1164}
1165
1166pub fn density() -> Density {
1167 let value = get_local::<Density>().unwrap_or_else(|| defaults().read().density);
1168 record_local(LocalId::Density, &value, |value| {
1169 fingerprint_f32(value.scale)
1170 });
1171 value
1172}
1173
1174pub fn ui_scale() -> UiScale {
1175 let value = get_local::<UiScale>().unwrap_or_else(|| defaults().read().ui_scale);
1176 record_local(LocalId::UiScale, &value, |value| fingerprint_f32(value.0));
1177 value
1178}
1179
1180pub fn text_scale() -> TextScale {
1181 let value = get_local::<TextScale>().unwrap_or_else(|| defaults().read().text_scale);
1182 record_local(LocalId::TextScale, &value, |value| fingerprint_f32(value.0));
1183 value
1184}
1185
1186pub fn text_direction() -> TextDirection {
1187 let value = get_local::<TextDirection>().unwrap_or_else(|| defaults().read().text_direction);
1188 record_local(LocalId::TextDirection, &value, fingerprint_text_direction);
1189 value
1190}
1191
1192pub(crate) fn local_fingerprint(id: usize) -> u64 {
1193 match id {
1194 id if id == LocalId::Theme.index() => {
1195 fingerprint_theme(&get_local::<Theme>().unwrap_or_else(|| defaults().read().theme))
1196 }
1197 id if id == LocalId::Density.index() => fingerprint_f32(
1198 get_local::<Density>()
1199 .unwrap_or_else(|| defaults().read().density)
1200 .scale,
1201 ),
1202 id if id == LocalId::UiScale.index() => fingerprint_f32(
1203 get_local::<UiScale>()
1204 .unwrap_or_else(|| defaults().read().ui_scale)
1205 .0,
1206 ),
1207 id if id == LocalId::TextScale.index() => fingerprint_f32(
1208 get_local::<TextScale>()
1209 .unwrap_or_else(|| defaults().read().text_scale)
1210 .0,
1211 ),
1212 id if id == LocalId::TextDirection.index() => fingerprint_text_direction(
1213 &get_local::<TextDirection>().unwrap_or_else(|| defaults().read().text_direction),
1214 ),
1215 id if id == LocalId::WindowInsets.index() => fingerprint_window_insets(
1216 &get_local::<WindowInsets>().unwrap_or_else(|| defaults().read().window_insets),
1217 ),
1218 id if id == LocalId::WindowSizeClass.index() => fingerprint_window_size_class(
1219 &get_local::<WindowSizeClass>().unwrap_or_else(|| defaults().read().window_size_class),
1220 ),
1221 id if id == LocalId::ContainerWidth.index() => {
1222 fingerprint_f32(defaults().read().container_width)
1223 }
1224 id if id == LocalId::ContainerHeight.index() => {
1225 fingerprint_f32(defaults().read().container_height)
1226 }
1227 id if id == LocalId::ContentColor.index() => fingerprint_color(
1228 &get_local::<ContentColor>()
1229 .map(|value| value.0)
1230 .unwrap_or_else(|| {
1231 get_local::<Theme>()
1232 .unwrap_or_else(|| defaults().read().theme)
1233 .on_surface
1234 }),
1235 ),
1236 id if id == LocalId::TextSize.index() => {
1237 fingerprint_text_size(get_local::<TextSize>().map(|value| value.0))
1238 }
1239 id if id == LocalId::Indication.index() => fingerprint_indication(&raw_local_indication()),
1240 id if id == LocalId::InputMode.index() => {
1241 fingerprint_input_mode(crate::input::input_mode())
1242 }
1243 _ => 0,
1244 }
1245}
1246
1247pub fn locals_stamp() -> u64 {
1248 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1249 for id in 0..=LocalId::InputMode.index() {
1250 id.hash(&mut hasher);
1251 local_fingerprint(id).hash(&mut hasher);
1252 }
1253 hasher.finish()
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258 use super::*;
1259
1260 #[test]
1261 fn width_class_thresholds_match_m3() {
1262 assert_eq!(
1264 calculate_window_size_class(100, 100, 1.0).width,
1265 WidthClass::Compact
1266 );
1267 assert_eq!(
1268 calculate_window_size_class(599, 100, 1.0).width,
1269 WidthClass::Compact
1270 );
1271 assert_eq!(
1272 calculate_window_size_class(600, 100, 1.0).width,
1273 WidthClass::Medium
1274 );
1275 assert_eq!(
1276 calculate_window_size_class(839, 100, 1.0).width,
1277 WidthClass::Medium
1278 );
1279 assert_eq!(
1280 calculate_window_size_class(840, 100, 1.0).width,
1281 WidthClass::Expanded
1282 );
1283 assert_eq!(
1284 calculate_window_size_class(2000, 100, 1.0).width,
1285 WidthClass::Expanded
1286 );
1287 }
1288
1289 #[test]
1290 fn height_class_thresholds_match_m3() {
1291 assert_eq!(
1292 calculate_window_size_class(100, 100, 1.0).height,
1293 HeightClass::Compact
1294 );
1295 assert_eq!(
1296 calculate_window_size_class(100, 479, 1.0).height,
1297 HeightClass::Compact
1298 );
1299 assert_eq!(
1300 calculate_window_size_class(100, 480, 1.0).height,
1301 HeightClass::Medium
1302 );
1303 assert_eq!(
1304 calculate_window_size_class(100, 899, 1.0).height,
1305 HeightClass::Medium
1306 );
1307 assert_eq!(
1308 calculate_window_size_class(100, 900, 1.0).height,
1309 HeightClass::Expanded
1310 );
1311 }
1312
1313 #[test]
1314 fn density_scales_thresholds() {
1315 let c = calculate_window_size_class(1199, 100, 2.0);
1317 assert_eq!(c.width, WidthClass::Compact);
1318 let c = calculate_window_size_class(1200, 100, 2.0);
1319 assert_eq!(c.width, WidthClass::Medium);
1320 }
1321
1322 #[test]
1323 fn is_at_least_medium_width() {
1324 let c = WindowSizeClass {
1325 width: WidthClass::Compact,
1326 height: HeightClass::Compact,
1327 };
1328 assert!(!c.is_at_least_medium_width());
1329 let c = WindowSizeClass {
1330 width: WidthClass::Medium,
1331 height: HeightClass::Compact,
1332 };
1333 assert!(c.is_at_least_medium_width());
1334 let c = WindowSizeClass {
1335 width: WidthClass::Expanded,
1336 height: HeightClass::Compact,
1337 };
1338 assert!(c.is_at_least_medium_width());
1339 }
1340}