kas_theme/
dim.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! A shared implementation of [`ThemeSize`]
7
8use linear_map::LinearMap;
9use std::any::Any;
10use std::f32;
11use std::rc::Rc;
12
13use crate::anim::AnimState;
14use kas::cast::traits::*;
15use kas::dir::Directional;
16use kas::geom::{Rect, Size, Vec2};
17use kas::layout::{AlignPair, AxisInfo, FrameRules, Margins, SizeRules, Stretch};
18use kas::text::{fonts::FontId, TextApi, TextApiExt};
19use kas::theme::{Feature, FrameStyle, MarginStyle, MarkStyle, TextClass, ThemeSize};
20
21kas::impl_scope! {
22    /// Parameterisation of [`Dimensions`]
23    ///
24    /// All dimensions are multiplied by the DPI factor, then rounded to the
25    /// nearest integer. Example: `(2.0 * 1.25).round() = 3.0`.
26    ///
27    /// "Reasonable defaults" are provided for all values.
28    #[derive(Clone, Debug)]
29    #[impl_default]
30    pub struct Parameters {
31        /// Inner margin, used to draw highlight/selection boxes
32        ///
33        /// Guide size: 1px at 100%, 2px at 125%, 2px at 150%, 3px at 200%.
34        ///
35        /// This is the smallest of the fixed margin sizes, and only really
36        /// useful to reserve space for drawing selection boxes.
37        pub m_inner: f32 = 1.25,
38        /// A small margin for inner layout
39        ///
40        /// Guide size: 2px at 100%, 3px at 125%, 4px at 150%, 5px at 200%.
41        pub m_tiny: f32 = 2.4,
42        /// Small external margin size
43        ///
44        /// Guide size: 4px at 100%, 5px at 125%, 7px at 150%, 9px at 200%.
45        pub m_small: f32 = 4.35,
46        /// Large margin, used between elements such as buttons
47        ///
48        /// Guide size: 7px at 100%, 9px at 125%, 11px at 150%, 15px at 200%.
49        pub m_large: f32 = 7.4,
50        /// Margin around text elements (horiz, vert)
51        pub m_text: (f32, f32) = (3.3, 0.8),
52        /// Frame size
53        pub frame: f32 = 2.4,
54        /// Popup frame size
55        pub popup_frame: f32 = 0.0,
56        /// MenuEntry frame size (horiz, vert)
57        pub menu_frame: f32 = 2.4,
58        /// Button frame size (non-flat outer region)
59        pub button_frame: f32 = 2.4,
60        /// Button inner margin (also drawable)
61        pub button_inner: f32 = 0.0,
62        /// CheckBox size
63        pub check_box: f32 = 18.0,
64        /// Larger size of a mark
65        pub mark: f32 = 10.0,
66        /// Length of a slider handle; minimum length of a scroll bar handle
67        pub handle_len: f32 = 16.0,
68        /// Minimum size for a horizontal scroll bar
69        pub scroll_bar_size: Vec2 = Vec2(24.0, 8.0),
70        /// Minimum size for a horizontal slider
71        pub slider_size: Vec2 = Vec2(24.0, 12.0),
72        /// Minimum size for a horizontal progress bar
73        pub progress_bar: Vec2 = Vec2(24.0, 8.0),
74        /// Shadow size (average)
75        pub shadow_size: Vec2 = Vec2::ZERO,
76        /// Proportional offset of shadow (range: -1..=1)
77        pub shadow_rel_offset: Vec2 = Vec2::ZERO,
78    }
79}
80
81/// Dimensions available within [`Window`]
82#[derive(Clone, Debug)]
83pub struct Dimensions {
84    /// Scale factor
85    pub scale: f32,
86    pub dpem: f32,
87    pub mark_line: f32,
88    pub min_line_length: i32,
89    pub m_inner: u16,
90    pub m_tiny: u16,
91    pub m_small: u16,
92    pub m_large: u16,
93    pub m_text: (u16, u16),
94    pub frame: i32,
95    pub popup_frame: i32,
96    pub menu_frame: i32,
97    pub button_frame: i32,
98    pub button_inner: u16,
99    pub check_box: i32,
100    pub mark: i32,
101    pub handle_len: i32,
102    pub scroll_bar: Size,
103    pub slider: Size,
104    pub progress_bar: Size,
105    pub shadow_a: Vec2,
106    pub shadow_b: Vec2,
107}
108
109impl Dimensions {
110    pub fn new(params: &Parameters, pt_size: f32, scale: f32) -> Self {
111        let dpp = scale * (96.0 / 72.0);
112        let dpem = dpp * pt_size;
113
114        let text_m0 = (params.m_text.0 * scale).cast_nearest();
115        let text_m1 = (params.m_text.1 * scale).cast_nearest();
116
117        let shadow_size = params.shadow_size * scale;
118        let shadow_offset = shadow_size * params.shadow_rel_offset;
119
120        Dimensions {
121            scale,
122            dpem,
123            mark_line: (1.6 * scale).round().max(1.0),
124            min_line_length: (8.0 * dpem).cast_nearest(),
125            m_inner: (params.m_inner * scale).cast_nearest(),
126            m_tiny: (params.m_tiny * scale).cast_nearest(),
127            m_small: (params.m_small * scale).cast_nearest(),
128            m_large: (params.m_large * scale).cast_nearest(),
129            m_text: (text_m0, text_m1),
130            frame: (params.frame * scale).cast_nearest(),
131            popup_frame: (params.popup_frame * scale).cast_nearest(),
132            menu_frame: (params.menu_frame * scale).cast_nearest(),
133            button_frame: (params.button_frame * scale).cast_nearest(),
134            button_inner: (params.button_inner * scale).cast_nearest(),
135            check_box: i32::conv_nearest(params.check_box * scale),
136            mark: i32::conv_nearest(params.mark * scale),
137            handle_len: i32::conv_nearest(params.handle_len * scale),
138            scroll_bar: Size::conv_nearest(params.scroll_bar_size * scale),
139            slider: Size::conv_nearest(params.slider_size * scale),
140            progress_bar: Size::conv_nearest(params.progress_bar * scale),
141            shadow_a: shadow_offset - shadow_size,
142            shadow_b: shadow_offset + shadow_size,
143        }
144    }
145}
146
147/// A convenient implementation of [`crate::Window`]
148pub struct Window<D> {
149    pub dims: Dimensions,
150    pub fonts: Rc<LinearMap<TextClass, FontId>>,
151    pub anim: AnimState<D>,
152}
153
154impl<D> Window<D> {
155    pub fn new(
156        dims: &Parameters,
157        config: &crate::Config,
158        scale: f32,
159        fonts: Rc<LinearMap<TextClass, FontId>>,
160    ) -> Self {
161        Window {
162            dims: Dimensions::new(dims, config.font_size(), scale),
163            fonts,
164            anim: AnimState::new(config),
165        }
166    }
167
168    pub fn update(&mut self, dims: &Parameters, config: &crate::Config, scale: f32) {
169        self.dims = Dimensions::new(dims, config.font_size(), scale);
170    }
171}
172
173impl<D: 'static> crate::Window for Window<D> {
174    fn size(&self) -> &dyn ThemeSize {
175        self
176    }
177
178    fn as_any_mut(&mut self) -> &mut dyn Any {
179        self
180    }
181}
182
183impl<D: 'static> ThemeSize for Window<D> {
184    fn scale_factor(&self) -> f32 {
185        self.dims.scale
186    }
187
188    fn dpem(&self) -> f32 {
189        self.dims.dpem
190    }
191
192    fn min_scroll_size(&self, axis_is_vertical: bool) -> i32 {
193        if axis_is_vertical {
194            (self.dims.dpem * 3.0).cast_ceil()
195        } else {
196            self.dims.min_line_length
197        }
198    }
199
200    fn handle_len(&self) -> i32 {
201        self.dims.handle_len
202    }
203
204    fn scroll_bar_width(&self) -> i32 {
205        self.dims.scroll_bar.1
206    }
207
208    fn margins(&self, style: MarginStyle) -> Margins {
209        match style {
210            MarginStyle::None => Margins::ZERO,
211            MarginStyle::Inner => Margins::splat(self.dims.m_inner),
212            MarginStyle::Tiny => Margins::splat(self.dims.m_tiny),
213            MarginStyle::Small => Margins::splat(self.dims.m_small),
214            MarginStyle::Large => Margins::splat(self.dims.m_large),
215            MarginStyle::Text => Margins::hv_splat(self.dims.m_text),
216            MarginStyle::Px(px) => Margins::splat(u16::conv_nearest(px * self.dims.scale)),
217            MarginStyle::Em(em) => Margins::splat(u16::conv_nearest(em * self.dims.dpem)),
218        }
219    }
220
221    fn feature(&self, feature: Feature, axis_is_vertical: bool) -> SizeRules {
222        let dir_is_vertical;
223        let mut size;
224        let mut ideal_mul = 3;
225        let m;
226
227        match feature {
228            Feature::Separator => {
229                return SizeRules::fixed_splat(self.dims.frame, 0);
230            }
231            Feature::Mark(MarkStyle::Point(dir)) => {
232                let w = match dir.is_vertical() == axis_is_vertical {
233                    true => self.dims.mark / 2 + i32::conv_ceil(self.dims.mark_line),
234                    false => self.dims.mark + i32::conv_ceil(self.dims.mark_line),
235                };
236                return SizeRules::fixed_splat(w, self.dims.m_tiny);
237            }
238            Feature::CheckBox | Feature::RadioBox => {
239                return SizeRules::fixed_splat(self.dims.check_box, self.dims.m_small);
240            }
241            Feature::ScrollBar(dir) => {
242                dir_is_vertical = dir.is_vertical();
243                size = self.dims.scroll_bar;
244                m = 0;
245            }
246            Feature::Slider(dir) => {
247                dir_is_vertical = dir.is_vertical();
248                size = self.dims.slider;
249                ideal_mul = 5;
250                m = self.dims.m_large;
251            }
252            Feature::ProgressBar(dir) => {
253                dir_is_vertical = dir.is_vertical();
254                size = self.dims.progress_bar;
255                m = self.dims.m_large;
256            }
257        }
258
259        let mut stretch = Stretch::High;
260        if dir_is_vertical != axis_is_vertical {
261            size = size.transpose();
262            ideal_mul = 1;
263            stretch = Stretch::None;
264        }
265        SizeRules::new(size.0, ideal_mul * size.0, (m, m), stretch)
266    }
267
268    fn align_feature(&self, feature: Feature, rect: Rect, align: AlignPair) -> Rect {
269        let mut ideal_size = rect.size;
270        match feature {
271            Feature::Separator => (), // has no direction so we cannot align
272            Feature::Mark(_) => (),   // aligned when drawn instead
273            Feature::CheckBox | Feature::RadioBox => {
274                ideal_size = Size::splat(self.dims.check_box);
275            }
276            Feature::ScrollBar(dir) => {
277                ideal_size.set_component(dir.flipped(), self.dims.scroll_bar.1);
278            }
279            Feature::Slider(dir) => {
280                ideal_size.set_component(dir.flipped(), self.dims.slider.1);
281            }
282            Feature::ProgressBar(dir) => {
283                ideal_size.set_component(dir.flipped(), self.dims.progress_bar.1);
284            }
285        }
286        align.aligned_rect(ideal_size, rect)
287    }
288
289    fn frame(&self, style: FrameStyle, _is_vert: bool) -> FrameRules {
290        let outer = self.dims.m_large;
291        match style {
292            FrameStyle::Frame => FrameRules::new_sym(self.dims.frame, 0, outer),
293            FrameStyle::Popup => FrameRules::new_sym(self.dims.popup_frame, 0, 0),
294            FrameStyle::MenuEntry => FrameRules::new_sym(self.dims.menu_frame, 0, 0),
295            FrameStyle::NavFocus => FrameRules::new_sym(0, self.dims.m_inner, 0),
296            FrameStyle::Button => {
297                FrameRules::new_sym(self.dims.button_frame, self.dims.button_inner, outer)
298            }
299            FrameStyle::EditBox => FrameRules::new_sym(self.dims.frame, 0, outer),
300        }
301    }
302
303    fn line_height(&self, class: TextClass) -> i32 {
304        let font_id = self.fonts.get(&class).cloned().unwrap_or_default();
305        kas::text::fonts::fonts()
306            .get_first_face(font_id)
307            .expect("invalid font_id")
308            .height(self.dims.dpem)
309            .cast_ceil()
310    }
311
312    fn text_rules(&self, text: &mut dyn TextApi, class: TextClass, axis: AxisInfo) -> SizeRules {
313        let margin = match axis.is_horizontal() {
314            true => self.dims.m_text.0,
315            false => self.dims.m_text.1,
316        };
317        let margins = (margin, margin);
318
319        let mut env = text.env();
320
321        // TODO(opt): maybe font look-up should only happen during configure?
322        if let Some(font_id) = self.fonts.get(&class).cloned() {
323            env.font_id = font_id;
324        }
325        env.dpem = self.dims.dpem;
326        // TODO(opt): setting horizontal alignment now could avoid re-wrapping
327        // text. Unfortunately we don't know the desired alignment here.
328        let wrap = class.multi_line();
329        env.wrap = wrap;
330        let align = axis.align_or_default();
331        if axis.is_horizontal() {
332            env.align.0 = align;
333        } else {
334            env.align.1 = align;
335        }
336        if let Some(size) = axis.size_other_if_fixed(true) {
337            env.bounds.0 = size.cast();
338        }
339
340        text.set_env(env);
341
342        if axis.is_horizontal() {
343            if wrap {
344                let min = self.dims.min_line_length;
345                let limit = 2 * min;
346                let bound: i32 = text
347                    .measure_width(limit.cast())
348                    .expect("invalid font_id")
349                    .cast_ceil();
350
351                // NOTE: using different variable-width stretch policies here can
352                // cause problems (e.g. edit boxes greedily consuming too much
353                // space). This is a hard layout problem; for now don't do this.
354                SizeRules::new(bound.min(min), bound.min(limit), margins, Stretch::Filler)
355            } else {
356                let bound: i32 = text
357                    .measure_width(f32::INFINITY)
358                    .expect("invalid font_id")
359                    .cast_ceil();
360                SizeRules::new(bound, bound, margins, Stretch::Filler)
361            }
362        } else {
363            let bound: i32 = text.measure_height().expect("invalid font_id").cast_ceil();
364
365            let line_height = self.dims.dpem.cast_ceil();
366            let min = bound.max(line_height);
367            SizeRules::new(min, min, margins, Stretch::Filler)
368        }
369    }
370
371    fn text_set_size(
372        &self,
373        text: &mut dyn TextApi,
374        class: TextClass,
375        size: Size,
376        align: Option<AlignPair>,
377    ) {
378        let mut env = text.env();
379        if let Some(font_id) = self.fonts.get(&class).cloned() {
380            env.font_id = font_id;
381        }
382        env.dpem = self.dims.dpem;
383        env.wrap = class.multi_line();
384        if let Some(align) = align {
385            env.align = align.into();
386        }
387        env.bounds = size.cast();
388        text.update_env(env).expect("invalid font_id");
389    }
390}