Skip to main content

facett_core/look/
scroll.rs

1//! **Scrollbars** (§10) — a serialisable [`ScrollSpec`] mapping onto egui's
2//! `ScrollStyle` (all 14 fields incl. the six opacity fields) plus a per-preset
3//! visibility default. Windows: solid, ~14–16 px, visible. macOS: floating,
4//! ~8–10 px, dormant ≈ 0, fade on hover. Device: solid, high-contrast, always
5//! visible.
6
7use egui::style::ScrollStyle;
8use serde::{Deserialize, Serialize};
9
10/// When the scrollbar shows — maps to egui `ScrollBarVisibility`, applied
11/// per-`ScrollArea` (it is not a `ScrollStyle` field in egui 0.34).
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub enum ScrollVisibility {
14    AlwaysVisible,
15    VisibleWhenNeeded,
16    AlwaysHidden,
17}
18
19impl ScrollVisibility {
20    pub fn to_egui(self) -> egui::scroll_area::ScrollBarVisibility {
21        use egui::scroll_area::ScrollBarVisibility as V;
22        match self {
23            ScrollVisibility::AlwaysVisible => V::AlwaysVisible,
24            ScrollVisibility::VisibleWhenNeeded => V::VisibleWhenNeeded,
25            ScrollVisibility::AlwaysHidden => V::AlwaysHidden,
26        }
27    }
28}
29
30/// All 14 `ScrollStyle` knobs + the visibility default, serialisable. The six
31/// opacity fields make a macOS bar dormant-invisible until hover.
32#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
33pub struct ScrollSpec {
34    pub floating: bool,
35    pub bar_width: f32,
36    pub handle_min_length: f32,
37    pub bar_inner_margin: f32,
38    pub bar_outer_margin: f32,
39    pub floating_width: f32,
40    pub floating_allocated_width: f32,
41    pub foreground_color: bool,
42    pub dormant_background_opacity: f32,
43    pub active_background_opacity: f32,
44    pub interact_background_opacity: f32,
45    pub dormant_handle_opacity: f32,
46    pub active_handle_opacity: f32,
47    pub interact_handle_opacity: f32,
48    pub visibility: ScrollVisibility,
49}
50
51impl Default for ScrollSpec {
52    fn default() -> Self {
53        Self::windows()
54    }
55}
56
57impl ScrollSpec {
58    /// Windows: solid, always-present, chunky bar.
59    pub fn windows() -> Self {
60        Self {
61            floating: false,
62            bar_width: 14.0,
63            handle_min_length: 16.0,
64            bar_inner_margin: 4.0,
65            bar_outer_margin: 0.0,
66            floating_width: 2.0,
67            floating_allocated_width: 0.0,
68            foreground_color: false,
69            dormant_background_opacity: 0.6,
70            active_background_opacity: 0.7,
71            interact_background_opacity: 0.9,
72            dormant_handle_opacity: 0.7,
73            active_handle_opacity: 0.9,
74            interact_handle_opacity: 1.0,
75            visibility: ScrollVisibility::AlwaysVisible,
76        }
77    }
78
79    /// macOS: floating, slim, dormant-invisible, fades in on hover/scroll.
80    pub fn macos() -> Self {
81        Self {
82            floating: true,
83            bar_width: 10.0,
84            handle_min_length: 16.0,
85            bar_inner_margin: 2.0,
86            bar_outer_margin: 0.0,
87            floating_width: 8.0,
88            floating_allocated_width: 0.0,
89            foreground_color: true,
90            dormant_background_opacity: 0.0,
91            active_background_opacity: 0.0,
92            interact_background_opacity: 0.4,
93            dormant_handle_opacity: 0.0,
94            active_handle_opacity: 0.6,
95            interact_handle_opacity: 1.0,
96            visibility: ScrollVisibility::VisibleWhenNeeded,
97        }
98    }
99
100    /// Device: solid, high-contrast, always visible (no fade to find in sun).
101    pub fn device() -> Self {
102        Self {
103            floating: false,
104            bar_width: 16.0,
105            handle_min_length: 24.0,
106            bar_inner_margin: 4.0,
107            bar_outer_margin: 0.0,
108            floating_width: 2.0,
109            floating_allocated_width: 0.0,
110            foreground_color: false,
111            dormant_background_opacity: 1.0,
112            active_background_opacity: 1.0,
113            interact_background_opacity: 1.0,
114            dormant_handle_opacity: 1.0,
115            active_handle_opacity: 1.0,
116            interact_handle_opacity: 1.0,
117            visibility: ScrollVisibility::AlwaysVisible,
118        }
119    }
120
121    /// Build an egui [`ScrollStyle`], modelling the 14 work-order fields and
122    /// inheriting egui's defaults for the rest (`content_margin`, `fade`, which
123    /// 0.34 added beyond the spec). Starting from `solid()`/`floating()` keeps us
124    /// forward-compatible with future egui fields.
125    pub fn to_scroll_style(&self) -> ScrollStyle {
126        let mut s = if self.floating { ScrollStyle::floating() } else { ScrollStyle::solid() };
127        s.floating = self.floating;
128        s.bar_width = self.bar_width;
129        s.handle_min_length = self.handle_min_length;
130        s.bar_inner_margin = self.bar_inner_margin;
131        s.bar_outer_margin = self.bar_outer_margin;
132        s.floating_width = self.floating_width;
133        s.floating_allocated_width = self.floating_allocated_width;
134        s.foreground_color = self.foreground_color;
135        s.dormant_background_opacity = self.dormant_background_opacity;
136        s.active_background_opacity = self.active_background_opacity;
137        s.interact_background_opacity = self.interact_background_opacity;
138        s.dormant_handle_opacity = self.dormant_handle_opacity;
139        s.active_handle_opacity = self.active_handle_opacity;
140        s.interact_handle_opacity = self.interact_handle_opacity;
141        s
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn windows_is_solid_and_always_visible() {
151        let s = ScrollSpec::windows();
152        assert!(!s.floating);
153        assert_eq!(s.visibility, ScrollVisibility::AlwaysVisible);
154        assert!(s.bar_width >= 14.0);
155    }
156
157    #[test]
158    fn macos_is_floating_and_dormant_invisible() {
159        let s = ScrollSpec::macos();
160        assert!(s.floating);
161        assert_eq!(s.dormant_handle_opacity, 0.0, "macOS bar fades away when idle");
162    }
163
164    #[test]
165    fn device_is_always_opaque() {
166        let s = ScrollSpec::device();
167        assert_eq!(s.dormant_handle_opacity, 1.0);
168        assert_eq!(s.visibility, ScrollVisibility::AlwaysVisible);
169    }
170
171    #[test]
172    fn to_scroll_style_copies_all_fields() {
173        let s = ScrollSpec::macos();
174        let e = s.to_scroll_style();
175        assert_eq!(e.bar_width, s.bar_width);
176        assert_eq!(e.floating, s.floating);
177        assert_eq!(e.interact_handle_opacity, s.interact_handle_opacity);
178    }
179}