Skip to main content

ferroui_controls/
tick_bar.rs

1use crate::{Control, ControlImpl};
2use ferroui_base::collections::FerroList;
3use ferroui_base::input::InputElementImpl;
4use ferroui_base::interactivity::InteractiveImpl;
5use ferroui_base::layout::{LayoutableImpl, Orientation};
6use ferroui_base::media::immutable::ImmutablePen;
7use ferroui_base::media::{BrushExtensions, DrawingContext, IBrush, IPen};
8use ferroui_base::utilities::MathUtilities;
9use ferroui_base::{
10    ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObjectImpl, FerroProperty,
11    Point, Rect, Ref, Size, StyledElementImpl, StyledProperty, Visual, VisualImpl,
12};
13use std::ops::Deref;
14use std::rc::Rc;
15
16/// Enum which describes how to position the [`TickBar`].
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18#[repr(i32)]
19pub enum TickBarPlacement {
20    /// Position this tick at the left of target element.
21    Left = 0,
22    /// Position this tick at the top of target element.
23    Top = 1,
24    /// Position this tick at the right of target element.
25    Right = 2,
26    /// Position this tick at the bottom of target element.
27    Bottom = 3,
28}
29
30/// The value of the `Ticks` property: a shared, observable list of tick
31/// values, compared by identity.
32#[derive(Clone, Default)]
33pub struct TickList(Rc<FerroList<f64>>);
34
35impl TickList {
36    /// Creates an empty list.
37    pub fn new() -> Self {
38        Self(Rc::new(FerroList::new()))
39    }
40
41    /// Creates a list holding `ticks`.
42    pub fn from_items(ticks: impl IntoIterator<Item = f64>) -> Self {
43        let result = Self::new();
44        result.add_range(ticks);
45        result
46    }
47
48    /// The underlying list.
49    #[inline]
50    pub fn list(&self) -> &Rc<FerroList<f64>> {
51        &self.0
52    }
53}
54
55impl Deref for TickList {
56    type Target = FerroList<f64>;
57
58    fn deref(&self) -> &FerroList<f64> {
59        &self.0
60    }
61}
62
63impl PartialEq for TickList {
64    fn eq(&self, other: &Self) -> bool {
65        Rc::ptr_eq(&self.0, &other.0)
66    }
67}
68
69/// An element that is used for drawing the ticks of a [`Slider`](crate::Slider).
70#[repr(C)]
71pub struct TickBar {
72    base: Control,
73}
74
75ferro_class!(TickBar: Control);
76ferroui_base::ferro_class_info!(TickBar { new: TickBar::new });
77ferro_impl_classes!(TickBar: StyledElementImpl, LayoutableImpl, InteractiveImpl, InputElementImpl, ControlImpl);
78
79ferroui_base::ferro_impl_classes!(TickBar: FerroObjectImpl);
80
81impl VisualImpl for TickBar {
82    /// Draws the ticks.
83    ///
84    /// Ticks can be drawn in 8 different ways depending on the `Placement`
85    /// and the `IsDirectionReversed` properties.
86    ///
87    /// The primary ticks (for the `Minimum` and `Maximum` values) have a
88    /// height of 100% of the tick bar's render size (its width or height
89    /// depending on the `Placement` property).
90    ///
91    /// The secondary ticks (all other ticks) have a height of 75% of the
92    /// tick bar's render size.
93    ///
94    /// The brush used to fill the ticks is specified by the `Fill` property.
95    fn render(this: &Self, dc: &mut DrawingContext) {
96        let bounds = this.bounds();
97        let mut size = Size::new(bounds.width, bounds.height);
98        let minimum = this.minimum();
99        let maximum = this.maximum();
100        let range = maximum - minimum;
101        // Height for the primary ticks (for the minimum and maximum value).
102        let tick_len;
103        let mut logical_to_physical;
104        let mut start_point;
105        let mut end_point;
106        let reserved_space = this.reserved_space();
107        let r_space =
108            if this.orientation() == Orientation::Horizontal { reserved_space.width } else { reserved_space.height };
109
110        // Take the thumb size into account.
111        let half_reserved_space = r_space * 0.5;
112
113        let placement = this.placement();
114
115        match placement {
116            TickBarPlacement::Top => {
117                if MathUtilities::greater_than_or_close(r_space, size.width) {
118                    return;
119                }
120                size = Size::new(size.width - r_space, size.height);
121                tick_len = -size.height;
122                start_point = Point::new(half_reserved_space, size.height);
123                end_point = Point::new(half_reserved_space + size.width, size.height);
124                logical_to_physical = size.width / range;
125            }
126            TickBarPlacement::Bottom => {
127                if MathUtilities::greater_than_or_close(r_space, size.width) {
128                    return;
129                }
130                size = Size::new(size.width - r_space, size.height);
131                tick_len = size.height;
132                start_point = Point::new(half_reserved_space, 0.0);
133                end_point = Point::new(half_reserved_space + size.width, 0.0);
134                logical_to_physical = size.width / range;
135            }
136            TickBarPlacement::Left => {
137                if MathUtilities::greater_than_or_close(r_space, size.height) {
138                    return;
139                }
140                size = Size::new(size.width, size.height - r_space);
141
142                tick_len = -size.width;
143                start_point = Point::new(size.width, size.height + half_reserved_space);
144                end_point = Point::new(size.width, half_reserved_space);
145                logical_to_physical = size.height / range * -1.0;
146            }
147            TickBarPlacement::Right => {
148                if MathUtilities::greater_than_or_close(r_space, size.height) {
149                    return;
150                }
151                size = Size::new(size.width, size.height - r_space);
152                tick_len = size.width;
153                start_point = Point::new(0.0, size.height + half_reserved_space);
154                end_point = Point::new(0.0, half_reserved_space);
155                logical_to_physical = size.height / range * -1.0;
156            }
157        }
158
159        // Height for the secondary ticks.
160        let tick_len2 = tick_len * 0.75;
161
162        // Invert the direction of the ticks.
163        if this.is_direction_reversed() {
164            logical_to_physical *= -1.0;
165
166            // Swap the start point and the end point.
167            std::mem::swap(&mut start_point, &mut end_point);
168        }
169
170        let pen: Rc<dyn IPen> = Rc::new(ImmutablePen::with_brush(
171            this.fill().map(|fill| BrushExtensions::to_immutable(&fill)),
172            1.0,
173        ));
174
175        // Is it vertical?
176        if placement == TickBarPlacement::Left || placement == TickBarPlacement::Right {
177            // Reduce the tick interval if it is more than would be visible on
178            // the screen.
179            let mut interval = this.tick_frequency();
180            if interval > 0.0 {
181                let min_interval = (maximum - minimum) / size.height;
182                if interval < min_interval {
183                    interval = min_interval;
184                }
185            }
186
187            // Draw the minimum and maximum ticks.
188            dc.draw_line(&pen, start_point, Point::new(start_point.x + tick_len, start_point.y));
189            dc.draw_line(
190                &pen,
191                Point::new(start_point.x, end_point.y),
192                Point::new(start_point.x + tick_len, end_point.y),
193            );
194
195            let ticks = this.ticks().filter(|ticks| ticks.count() > 0);
196
197            // Draw the ticks using the specified ticks collection.
198            if let Some(ticks) = ticks {
199                for &tick in ticks.snapshot().iter() {
200                    if MathUtilities::less_than_or_close(tick, minimum)
201                        || MathUtilities::greater_than_or_close(tick, maximum)
202                    {
203                        continue;
204                    }
205
206                    let adjusted_tick = tick - minimum;
207
208                    let y = adjusted_tick * logical_to_physical + start_point.y;
209                    dc.draw_line(&pen, Point::new(start_point.x, y), Point::new(start_point.x + tick_len2, y));
210                }
211            }
212            // Draw the ticks using the specified tick frequency.
213            else if interval > 0.0 {
214                let mut i = interval;
215                while i < range {
216                    let y = i * logical_to_physical + start_point.y;
217
218                    dc.draw_line(&pen, Point::new(start_point.x, y), Point::new(start_point.x + tick_len2, y));
219                    i += interval;
220                }
221            }
222        } else {
223            // The placement is top or bottom.
224
225            // Reduce the tick interval if it is more than would be visible on
226            // the screen.
227            let mut interval = this.tick_frequency();
228            if interval > 0.0 {
229                let min_interval = (maximum - minimum) / size.width;
230                if interval < min_interval {
231                    interval = min_interval;
232                }
233            }
234
235            // Draw the minimum and maximum ticks.
236            dc.draw_line(&pen, start_point, Point::new(start_point.x, start_point.y + tick_len));
237            dc.draw_line(
238                &pen,
239                Point::new(end_point.x, start_point.y),
240                Point::new(end_point.x, start_point.y + tick_len),
241            );
242
243            let ticks = this.ticks().filter(|ticks| ticks.count() > 0);
244
245            // Draw the ticks using the specified ticks collection.
246            if let Some(ticks) = ticks {
247                for &tick in ticks.snapshot().iter() {
248                    if MathUtilities::less_than_or_close(tick, minimum)
249                        || MathUtilities::greater_than_or_close(tick, maximum)
250                    {
251                        continue;
252                    }
253                    let adjusted_tick = tick - minimum;
254
255                    let x = adjusted_tick * logical_to_physical + start_point.x;
256                    dc.draw_line(&pen, Point::new(x, start_point.y), Point::new(x, start_point.y + tick_len2));
257                }
258            }
259            // Draw the ticks using the specified tick frequency.
260            else if interval > 0.0 {
261                let mut i = interval;
262                while i < range {
263                    let x = i * logical_to_physical + start_point.x;
264                    dc.draw_line(&pen, Point::new(x, start_point.y), Point::new(x, start_point.y + tick_len2));
265                    i += interval;
266                }
267            }
268        }
269    }
270}
271
272ferroui_base::ferro_properties! { impl TickBar {
273    ferro_property!(
274        /// Defines the `Fill` property.
275        pub fn fill_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
276            FerroProperty::register::<TickBar, _>("Fill", None)
277        }
278    );
279
280    ferro_property!(
281        /// Defines the `Minimum` property.
282        pub fn minimum_property() -> StyledProperty<f64> {
283            FerroProperty::register::<TickBar, _>("Minimum", 0.0)
284        }
285    );
286
287    ferro_property!(
288        /// Defines the `Maximum` property.
289        pub fn maximum_property() -> StyledProperty<f64> {
290            FerroProperty::register::<TickBar, _>("Maximum", 0.0)
291        }
292    );
293
294    ferro_property!(
295        /// Defines the `TickFrequency` property.
296        pub fn tick_frequency_property() -> StyledProperty<f64> {
297            FerroProperty::register::<TickBar, _>("TickFrequency", 0.0)
298        }
299    );
300
301    ferro_property!(
302        /// Defines the `Orientation` property.
303        pub fn orientation_property() -> StyledProperty<Orientation> {
304            FerroProperty::register::<TickBar, _>("Orientation", Orientation::Horizontal)
305        }
306    );
307
308    ferro_property!(
309        /// Defines the `Ticks` property.
310        pub fn ticks_property() -> StyledProperty<Option<TickList>> {
311            FerroProperty::register::<TickBar, _>("Ticks", None)
312        }
313    );
314
315    ferro_property!(
316        /// Defines the `IsDirectionReversed` property.
317        pub fn is_direction_reversed_property() -> StyledProperty<bool> {
318            FerroProperty::register::<TickBar, _>("IsDirectionReversed", false)
319        }
320    );
321
322    ferro_property!(
323        /// Defines the `Placement` property.
324        pub fn placement_property() -> StyledProperty<TickBarPlacement> {
325            FerroProperty::register::<TickBar, _>("Placement", TickBarPlacement::Left)
326        }
327    );
328
329    ferro_property!(
330        /// Defines the `ReservedSpace` property.
331        pub fn reserved_space_property() -> StyledProperty<Rect> {
332            FerroProperty::register::<TickBar, _>("ReservedSpace", Rect::default())
333        }
334    );
335} }
336
337impl TickBar {
338    fn static_constructor() {
339        Visual::affects_render::<TickBar>(&[
340            Self::fill_property().as_property(),
341            Self::is_direction_reversed_property().as_property(),
342            Self::reserved_space_property().as_property(),
343            Self::maximum_property().as_property(),
344            Self::minimum_property().as_property(),
345            Self::orientation_property().as_property(),
346            Self::placement_property().as_property(),
347            Self::tick_frequency_property().as_property(),
348            Self::ticks_property().as_property(),
349        ]);
350    }
351
352    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
353    pub fn construct() -> Self {
354        Self { base: Control::construct() }
355    }
356
357    pub fn new() -> Ref<Self> {
358        instantiate(Self::construct())
359    }
360
361    /// The brush used to fill the ticks of the tick bar.
362    pub fn fill(&self) -> Option<Rc<dyn IBrush>> {
363        self.get_value(Self::fill_property())
364    }
365
366    pub fn set_fill(&self, value: Option<Rc<dyn IBrush>>) {
367        self.set_value(Self::fill_property(), value)
368    }
369
370    /// The logical position where the minimum tick will be drawn.
371    pub fn minimum(&self) -> f64 {
372        self.get_value(Self::minimum_property())
373    }
374
375    pub fn set_minimum(&self, value: f64) {
376        self.set_value(Self::minimum_property(), value)
377    }
378
379    /// The logical position where the maximum tick will be drawn.
380    pub fn maximum(&self) -> f64 {
381        self.get_value(Self::maximum_property())
382    }
383
384    pub fn set_maximum(&self, value: f64) {
385        self.set_value(Self::maximum_property(), value)
386    }
387
388    /// The tick frequency defines how the ticks will be drawn.
389    pub fn tick_frequency(&self) -> f64 {
390        self.get_value(Self::tick_frequency_property())
391    }
392
393    pub fn set_tick_frequency(&self, value: f64) {
394        self.set_value(Self::tick_frequency_property(), value)
395    }
396
397    /// The orientation of the tick bar's parent.
398    pub fn orientation(&self) -> Orientation {
399        self.get_value(Self::orientation_property())
400    }
401
402    pub fn set_orientation(&self, value: Orientation) {
403        self.set_value(Self::orientation_property(), value)
404    }
405
406    /// The collection of values which are the logical positions used to
407    /// draw the ticks.
408    pub fn ticks(&self) -> Option<TickList> {
409        self.get_value(Self::ticks_property())
410    }
411
412    pub fn set_ticks(&self, value: Option<TickList>) {
413        self.set_value(Self::ticks_property(), value)
414    }
415
416    /// Defines the direction of value incrementation.
417    ///
418    /// By default, if the orientation is horizontal, ticks will be drawn
419    /// from left to right (and from bottom to top for the vertical
420    /// orientation). If this is true the ticks are drawn in the opposite
421    /// direction.
422    pub fn is_direction_reversed(&self) -> bool {
423        self.get_value(Self::is_direction_reversed_property())
424    }
425
426    pub fn set_is_direction_reversed(&self, value: bool) {
427        self.set_value(Self::is_direction_reversed_property(), value)
428    }
429
430    /// Specifies how the ticks will be placed. This property affects the
431    /// way ticks are drawn.
432    pub fn placement(&self) -> TickBarPlacement {
433        self.get_value(Self::placement_property())
434    }
435
436    pub fn set_placement(&self, value: TickBarPlacement) {
437        self.set_value(Self::placement_property(), value)
438    }
439
440    /// The tick bar uses the reserved space for left and right spacing (for
441    /// the horizontal orientation) or top and bottom spacing (for the
442    /// vertical orientation). The space on both sides of the tick bar is
443    /// half of the specified reserved space.
444    pub fn reserved_space(&self) -> Rect {
445        self.get_value(Self::reserved_space_property())
446    }
447
448    pub fn set_reserved_space(&self, value: Rect) {
449        self.set_value(Self::reserved_space_property(), value)
450    }
451}