egui_charts/chart/rendering/
watermark.rs1use crate::config::FontWeight;
4use egui::{Align2, Color32, FontId, Painter, Rect, Shape, Ui, Vec2};
5
6#[derive(Clone, Debug, PartialEq)]
8pub struct WatermarkLine {
9 pub text: String,
11 pub size: f32,
13 pub weight: FontWeight,
15 pub color: Color32,
17}
18
19impl WatermarkLine {
20 pub fn new(text: impl Into<String>, size: f32, color: Color32) -> Self {
22 Self {
23 text: text.into(),
24 size,
25 weight: FontWeight::Normal,
26 color,
27 }
28 }
29
30 #[must_use]
32 pub fn with_weight(mut self, weight: FontWeight) -> Self {
33 self.weight = weight;
34 self
35 }
36}
37
38#[derive(Clone, Debug)]
41pub struct Watermark {
42 pub lines: Vec<WatermarkLine>,
44 pub image: Option<egui::Image<'static>>,
48 pub align: Align2,
51 pub padding: f32,
53}
54
55impl Default for Watermark {
56 fn default() -> Self {
57 Self {
58 lines: Vec::new(),
59 image: None,
60 align: Align2::CENTER_CENTER,
61 padding: 0.0,
62 }
63 }
64}
65
66impl Watermark {
67 #[must_use]
69 pub fn with_line(mut self, line: WatermarkLine) -> Self {
70 self.lines.push(line);
71 self
72 }
73
74 #[must_use]
76 pub fn with_image(mut self, image: egui::Image<'static>) -> Self {
77 self.image = Some(image);
78 self
79 }
80
81 #[must_use]
83 pub fn with_align(mut self, align: Align2) -> Self {
84 self.align = align;
85 self
86 }
87
88 #[must_use]
90 pub fn with_padding(mut self, padding: f32) -> Self {
91 self.padding = padding;
92 self
93 }
94}
95
96pub(crate) fn render_watermark(ui: &Ui, painter: &Painter, rect: Rect, watermark: &Watermark) {
98 let inner = rect.shrink(watermark.padding.max(0.0));
99 if !inner.is_positive() {
100 return;
101 }
102 let image = watermark.image.as_ref().and_then(|image| {
103 let size = image.load_and_calc_size(ui, inner.size())?;
104 Some((image, size))
105 });
106 let galleys: Vec<_> = watermark
107 .lines
108 .iter()
109 .filter(|line| line.size.is_finite() && line.size > 0.0)
110 .map(|line| {
111 let galley = painter.layout_no_wrap(
112 line.text.clone(),
113 FontId::proportional(line.size),
114 line.color,
115 );
116 (line, galley)
117 })
118 .collect();
119 let block = galleys.iter().fold(
120 image.map_or(Vec2::ZERO, |(_, size)| size),
121 |block, (line, galley)| Vec2::new(block.x.max(galley.size().x), block.y + 1.2 * line.size),
122 );
123 let block = watermark.align.align_size_within_rect(block, inner);
124 let row = |top: f32, size: Vec2| {
125 let column = Rect::from_min_max(egui::pos2(block.left(), top), block.right_bottom());
126 Align2([watermark.align.x(), egui::Align::Min]).align_size_within_rect(size, column)
127 };
128 let mut top = block.top();
129 if let Some((image, size)) = image {
130 image.paint_at(ui, row(top, size));
131 top += size.y;
132 }
133 for (line, galley) in galleys {
134 let height = 1.2 * line.size;
135 let at = row(top + (height - galley.size().y) / 2.0, galley.size()).min;
136 let text = Shape::galley(at, galley.clone(), line.color);
137 if line.weight == FontWeight::Bold {
138 painter.add(Shape::galley(at + Vec2::new(0.6, 0.0), galley, line.color));
140 }
141 painter.add(text);
142 top += height;
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::widget::test_frames::{chart, shapes};
150 use egui::load::SizedTexture;
151
152 #[test]
158 fn watermark_stacks_its_image_and_lines_at_the_alignment() {
159 let (bold, plain) = (Color32::from_rgb(1, 2, 3), Color32::from_rgb(4, 5, 6));
160 let texture = egui::TextureId::User(7);
161 let ctx = egui::Context::default();
162 let mut chart = chart(&[100.0], &[110.0]);
163 for align in [
164 Align2::LEFT_TOP,
165 Align2::CENTER_CENTER,
166 Align2::RIGHT_BOTTOM,
167 ] {
168 chart.set_watermark(Some(
169 Watermark::default()
170 .with_image(
171 egui::Image::from_texture(SizedTexture::new(
172 texture,
173 Vec2::new(200.0, 100.0),
174 ))
175 .max_size(Vec2::new(100.0, 50.0)),
176 )
177 .with_line(WatermarkLine::new("AAPL", 40.0, bold).with_weight(FontWeight::Bold))
178 .with_line(WatermarkLine::new("Apple Inc", 20.0, plain))
179 .with_align(align)
180 .with_padding(10.0),
181 ));
182 let painted = shapes(&ctx, &mut chart, 0.0, Vec::new());
183 let pane = chart.last_rendered_price_rect.shrink(10.0);
184 let top = match align.y() {
185 egui::Align::Min => pane.top(),
186 egui::Align::Center => pane.center().y - 61.0,
187 egui::Align::Max => pane.bottom() - 122.0,
188 };
189 let edge = |part: Rect| match align.x() {
190 egui::Align::Min => part.left() - pane.left(),
191 egui::Align::Center => part.center().x - pane.center().x,
192 egui::Align::Max => part.right() - pane.right(),
193 };
194 let image = painted
195 .iter()
196 .find(|shape| shape.texture_id() == texture)
197 .expect("the image")
198 .visual_bounding_rect();
199 assert_eq!(image.size(), Vec2::new(100.0, 50.0), "{align:?}");
200 assert!((image.top() - top).abs() <= 0.5, "{align:?}");
202 assert!(edge(image).abs() < 0.01, "{align:?}");
203 for (color, centre, copies) in
204 [(bold, top + 50.0 + 24.0, 2), (plain, top + 122.0 - 12.0, 1)]
205 {
206 let texts: Vec<_> = painted
208 .iter()
209 .filter_map(|shape| match shape {
210 Shape::Text(t) if t.fallback_color == color => {
211 Some(t.galley.rect.translate(t.pos.to_vec2()))
212 }
213 _ => None,
214 })
215 .collect();
216 assert_eq!(texts.len(), copies, "{align:?}");
217 assert!((texts[0].center().y - centre).abs() < 0.01, "{align:?}");
218 assert!(edge(texts[copies - 1]).abs() < 0.01, "{align:?}");
219 }
220 }
221 }
222}