Skip to main content

leptos_chartistry/layout/
rotated_label.rs

1use super::{UseLayout, UseVerticalLayout};
2use crate::{
3    bounds::Bounds,
4    debug::DebugRect,
5    edge::Edge,
6    state::{PreState, State},
7    Tick,
8};
9use leptos::prelude::*;
10use std::str::FromStr;
11
12/// Label placement on the main-axis of a component. An edge layout's main-axis runs parallel to its given edge. Similar to SVG's [text-anchor](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor) or CSS's [justify-content](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content).
13#[derive(Copy, Clone, Debug, PartialEq)]
14#[non_exhaustive]
15pub enum Anchor {
16    /// Start of the line (main-axis).
17    Start,
18    /// Middle of the line (main-axis).
19    Middle,
20    /// End of the line (main-axis).
21    End,
22}
23
24/// Builds a rotated label to match the orientation of the axis it's placed on.
25///
26/// Warning: does not wrap text. Extra text will not be clipped.
27#[derive(Clone, Debug, PartialEq)]
28#[non_exhaustive]
29pub struct RotatedLabel {
30    /// Text to display.
31    pub text: RwSignal<String>,
32    /// Anchor of the label.
33    pub anchor: RwSignal<Anchor>,
34}
35
36impl RotatedLabel {
37    fn new(anchor: Anchor, text: String) -> Self {
38        Self {
39            text: RwSignal::new(text),
40            anchor: RwSignal::new(anchor),
41        }
42    }
43
44    /// Creates a new rotated label anchored at the start of the line (main-axis).
45    pub fn start(text: impl Into<String>) -> Self {
46        Self::new(Anchor::Start, text.into())
47    }
48    /// Creates a new rotated label anchored at the middle of the line (main-axis).
49    pub fn middle(text: impl Into<String>) -> Self {
50        Self::new(Anchor::Middle, text.into())
51    }
52    /// Creates a new rotated label anchored at the end of the line (main-axis).
53    pub fn end(text: impl Into<String>) -> Self {
54        Self::new(Anchor::End, text.into())
55    }
56
57    fn size<X: Tick, Y: Tick>(&self, state: &PreState<X, Y>) -> Signal<f64> {
58        let text = self.text;
59        let font_height = state.font_height;
60        let padding = state.padding;
61        Signal::derive(move || {
62            if text.with(|t| t.is_empty()) {
63                0.0
64            } else {
65                font_height.get() + padding.get().height()
66            }
67        })
68    }
69
70    pub(super) fn fixed_height<X: Tick, Y: Tick>(&self, state: &PreState<X, Y>) -> Signal<f64> {
71        self.size(state)
72    }
73
74    pub(super) fn to_horizontal_use(&self) -> UseLayout {
75        UseLayout::RotatedLabel(self.clone())
76    }
77
78    pub(super) fn to_vertical_use<X: Tick, Y: Tick>(
79        &self,
80        state: &PreState<X, Y>,
81    ) -> UseVerticalLayout {
82        // Note: width is height because it's rotated
83        UseVerticalLayout {
84            width: self.size(state),
85            layout: UseLayout::RotatedLabel(self.clone()),
86        }
87    }
88}
89
90impl Anchor {
91    fn to_svg_attr(self) -> String {
92        self.to_string()
93    }
94
95    fn map_points(&self, left: f64, middle: f64, right: f64) -> f64 {
96        match self {
97            Anchor::Start => left,
98            Anchor::Middle => middle,
99            Anchor::End => right,
100        }
101    }
102
103    pub(crate) fn css_justify_content(&self) -> &'static str {
104        match self {
105            Anchor::Start => "flex-start",
106            Anchor::Middle => "center",
107            Anchor::End => "flex-end",
108        }
109    }
110}
111
112impl FromStr for Anchor {
113    type Err = String;
114
115    fn from_str(s: &str) -> Result<Self, Self::Err> {
116        match s.to_lowercase().as_str() {
117            "start" => Ok(Anchor::Start),
118            "middle" => Ok(Anchor::Middle),
119            "end" => Ok(Anchor::End),
120            _ => Err(format!("unknown anchor: `{}`", s)),
121        }
122    }
123}
124
125impl std::fmt::Display for Anchor {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        match self {
128            Anchor::Start => write!(f, "start"),
129            Anchor::Middle => write!(f, "middle"),
130            Anchor::End => write!(f, "end"),
131        }
132    }
133}
134
135#[component]
136pub(super) fn RotatedLabel<X: Tick, Y: Tick>(
137    label: RotatedLabel,
138    edge: Edge,
139    bounds: Memo<Bounds>,
140    state: State<X, Y>,
141) -> impl IntoView {
142    let RotatedLabel { text, anchor } = label;
143    let debug = state.pre.debug;
144    let font_height = state.pre.font_height;
145    let padding = state.pre.padding;
146
147    let content = Signal::derive(move || padding.get().apply(bounds.get()));
148    let position = Memo::new(move |_| {
149        let c = content.get();
150        let (top, right, bottom, left) = (c.top_y(), c.right_x(), c.bottom_y(), c.left_x());
151        let (centre_x, centre_y) = (c.centre_x(), c.centre_y());
152
153        let anchor = anchor.get();
154        match edge {
155            Edge::Top | Edge::Bottom => (0, anchor.map_points(left, centre_x, right), centre_y),
156            Edge::Left => (270, centre_x, anchor.map_points(bottom, centre_y, top)),
157            // Right rotates the opposite way to Left inverting the anchor points
158            Edge::Right => (90, centre_x, anchor.map_points(top, centre_y, bottom)),
159        }
160    });
161
162    view! {
163        <g
164            class="_chartistry_rotated_label"
165            font-family="monospace">
166            <DebugRect label="RotatedLabel" debug=debug bounds=vec![bounds.into(), content] />
167            <text
168                x=move || position.with(|(_, x, _)| x.to_string())
169                y=move || position.with(|(_, _, y)| y.to_string())
170                transform=move || position.with(|(rotate, x, y)| format!("rotate({rotate}, {x}, {y})"))
171                dominant-baseline="middle"
172                text-anchor=move || anchor.get().to_svg_attr()
173                font-size=move || format!("{}px", font_height.get())>
174                {text}
175            </text>
176        </g>
177    }
178}