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