Skip to main content

fc_dioxus/core/
measure.rs

1//! The only inline styles this crate writes: custom properties upstream reads from markup.
2
3use dioxus::prelude::*;
4use std::fmt;
5
6/// A percentage, clamped to `0..=100` when constructed so the drawn value and the ARIA
7/// value can never disagree or leave the range.
8///
9/// ```
10/// use fc_dioxus::prelude::Percent;
11/// assert_eq!(Percent::new(140.0).get(), 100.0);
12/// assert_eq!(Percent::new(f64::NAN).get(), 0.0);
13/// ```
14#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
15pub struct Percent(f64);
16impl Percent {
17    /// Clamp a value to `0..=100`; NaN becomes zero rather than poisoning the style.
18    #[must_use]
19    pub fn new(value: f64) -> Self {
20        if value.is_nan() {
21            Self(0.0)
22        } else {
23            Self(value.clamp(0.0, 100.0))
24        }
25    }
26    /// Express `value` out of `max` as a percentage; a non-positive `max` is empty.
27    #[must_use]
28    pub fn of(value: f64, max: f64) -> Self {
29        if max > 0.0 {
30            Self::new(value / max * 100.0)
31        } else {
32            Self(0.0)
33        }
34    }
35    /// The clamped value.
36    #[must_use]
37    pub const fn get(self) -> f64 {
38        self.0
39    }
40}
41impl fmt::Display for Percent {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        // Whole numbers print without a fraction, so markup matches upstream's fixtures.
44        if self.0.fract() == 0.0 {
45            write!(f, "{:.0}", self.0)
46        } else {
47            write!(f, "{}", self.0)
48        }
49    }
50}
51
52/// The custom properties upstream's CSS reads from markup, and nothing else.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub(crate) enum CustomProperty {
55    /// Progress ring fill, 0 to 100.
56    ProgressValue,
57    /// A tool call's determinate progress, as a percentage length.
58    ToolProgress,
59    /// The filled part of a slider's track, as a percentage length.
60    SliderPct,
61    /// A split pane's or resizable element's size.
62    SplitSize,
63    /// A stacked split's block size.
64    SplitBlockSize,
65}
66impl CustomProperty {
67    const fn name(self) -> &'static str {
68        match self {
69            Self::ProgressValue => "--fc-progress-value",
70            Self::ToolProgress => "--fc-tool-progress",
71            Self::SliderPct => "--fc-slider-pct",
72            Self::SplitSize => "--fc-split-size",
73            Self::SplitBlockSize => "--fc-split-block-size",
74        }
75    }
76}
77
78/// A `style` attribute holding exactly one upstream custom property.
79pub(crate) fn custom_property(property: CustomProperty, value: impl fmt::Display) -> Attribute {
80    Attribute::new(
81        "style",
82        format!("{}: {value}", property.name()),
83        None,
84        false,
85    )
86}
87
88/// The data grid's column template: an `auto` track for row headers when present, then
89/// `columns` equal tracks.
90///
91/// The one inline style that is not a custom property. `grid.css` sizes implicit tracks
92/// with `grid-auto-columns`, and rows are `display: contents`, so nothing creates the
93/// tracks and every cell stacks in one column. Upstream's binding and its fixture both
94/// write this declaration inline for that reason; it is layout the markup's shape
95/// decides, not a colour or a theme value.
96pub(crate) fn grid_columns(row_headers: bool, columns: usize) -> Attribute {
97    let lead = if row_headers { "auto " } else { "" };
98    Attribute::new(
99        "style",
100        format!("grid-template-columns: {lead}repeat({columns}, minmax(0, 1fr))"),
101        None,
102        false,
103    )
104}