1use std::sync::Arc;
2
3use emath::GuiRounding as _;
4use epaint::{
5 text::{Fonts, Galley, LayoutJob},
6 CircleShape, ClippedShape, CornerRadius, PathStroke, RectShape, Shape, Stroke, StrokeKind,
7};
8
9use crate::{
10 emath::{Align2, Pos2, Rangef, Rect, Vec2},
11 layers::{LayerId, PaintList, ShapeIdx},
12 Color32, Context, FontId,
13};
14
15#[derive(Clone)]
21pub struct Painter {
22 ctx: Context,
24
25 pixels_per_point: f32,
27
28 layer_id: LayerId,
30
31 clip_rect: Rect,
34
35 fade_to_color: Option<Color32>,
38
39 opacity_factor: f32,
43}
44
45impl Painter {
46 pub fn new(ctx: Context, layer_id: LayerId, clip_rect: Rect) -> Self {
48 let pixels_per_point = ctx.pixels_per_point();
49 Self {
50 ctx,
51 pixels_per_point,
52 layer_id,
53 clip_rect,
54 fade_to_color: None,
55 opacity_factor: 1.0,
56 }
57 }
58
59 #[must_use]
61 #[inline]
62 pub fn with_layer_id(mut self, layer_id: LayerId) -> Self {
63 self.layer_id = layer_id;
64 self
65 }
66
67 pub fn with_clip_rect(&self, rect: Rect) -> Self {
72 let mut new_self = self.clone();
73 new_self.clip_rect = rect.intersect(self.clip_rect);
74 new_self
75 }
76
77 pub fn set_layer_id(&mut self, layer_id: LayerId) {
82 self.layer_id = layer_id;
83 }
84
85 pub fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
87 self.fade_to_color = fade_to_color;
88 }
89
90 pub fn set_opacity(&mut self, opacity: f32) {
97 if opacity.is_finite() {
98 self.opacity_factor = opacity.clamp(0.0, 1.0);
99 }
100 }
101
102 pub fn multiply_opacity(&mut self, opacity: f32) {
106 if opacity.is_finite() {
107 self.opacity_factor *= opacity.clamp(0.0, 1.0);
108 }
109 }
110
111 #[inline]
115 pub fn opacity(&self) -> f32 {
116 self.opacity_factor
117 }
118
119 pub fn is_visible(&self) -> bool {
123 self.fade_to_color != Some(Color32::TRANSPARENT) && !self.ctx.will_discard()
124 }
125
126 pub fn set_invisible(&mut self) {
128 self.fade_to_color = Some(Color32::TRANSPARENT);
129 }
130
131 #[inline]
133 pub fn ctx(&self) -> &Context {
134 &self.ctx
135 }
136
137 #[inline]
139 pub fn pixels_per_point(&self) -> f32 {
140 self.pixels_per_point
141 }
142
143 #[inline]
147 pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
148 self.ctx.fonts(reader)
149 }
150
151 #[inline]
153 pub fn layer_id(&self) -> LayerId {
154 self.layer_id
155 }
156
157 #[inline]
160 pub fn clip_rect(&self) -> Rect {
161 self.clip_rect
162 }
163
164 #[inline]
170 pub fn shrink_clip_rect(&mut self, new_clip_rect: Rect) {
171 self.clip_rect = self.clip_rect.intersect(new_clip_rect);
172 }
173
174 #[inline]
180 pub fn set_clip_rect(&mut self, clip_rect: Rect) {
181 self.clip_rect = clip_rect;
182 }
183
184 #[inline]
186 pub fn round_to_pixel_center(&self, point: f32) -> f32 {
187 point.round_to_pixel_center(self.pixels_per_point())
188 }
189
190 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
192 #[inline]
193 pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 {
194 pos.round_to_pixel_center(self.pixels_per_point())
195 }
196
197 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
199 #[inline]
200 pub fn round_to_pixel(&self, point: f32) -> f32 {
201 point.round_to_pixels(self.pixels_per_point())
202 }
203
204 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
206 #[inline]
207 pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
208 vec.round_to_pixels(self.pixels_per_point())
209 }
210
211 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
213 #[inline]
214 pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
215 pos.round_to_pixels(self.pixels_per_point())
216 }
217
218 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
220 #[inline]
221 pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect {
222 rect.round_to_pixels(self.pixels_per_point())
223 }
224}
225
226impl Painter {
228 #[inline]
229 fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
230 self.ctx.graphics_mut(|g| writer(g.entry(self.layer_id)))
231 }
232
233 fn transform_shape(&self, shape: &mut Shape) {
234 if let Some(fade_to_color) = self.fade_to_color {
235 tint_shape_towards(shape, fade_to_color);
236 }
237 if self.opacity_factor < 1.0 {
238 multiply_opacity(shape, self.opacity_factor);
239 }
240 }
241
242 pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
246 if self.fade_to_color == Some(Color32::TRANSPARENT) || self.opacity_factor == 0.0 {
247 self.paint_list(|l| l.add(self.clip_rect, Shape::Noop))
248 } else {
249 let mut shape = shape.into();
250 self.transform_shape(&mut shape);
251 self.paint_list(|l| l.add(self.clip_rect, shape))
252 }
253 }
254
255 pub fn extend<I: IntoIterator<Item = Shape>>(&self, shapes: I) {
259 if self.fade_to_color == Some(Color32::TRANSPARENT) || self.opacity_factor == 0.0 {
260 return;
261 }
262 if self.fade_to_color.is_some() || self.opacity_factor < 1.0 {
263 let shapes = shapes.into_iter().map(|mut shape| {
264 self.transform_shape(&mut shape);
265 shape
266 });
267 self.paint_list(|l| l.extend(self.clip_rect, shapes));
268 } else {
269 self.paint_list(|l| l.extend(self.clip_rect, shapes));
270 }
271 }
272
273 pub fn set(&self, idx: ShapeIdx, shape: impl Into<Shape>) {
275 if self.fade_to_color == Some(Color32::TRANSPARENT) {
276 return;
277 }
278 let mut shape = shape.into();
279 self.transform_shape(&mut shape);
280 self.paint_list(|l| l.set(idx, self.clip_rect, shape));
281 }
282
283 pub fn for_each_shape(&self, mut reader: impl FnMut(&ClippedShape)) {
285 self.ctx.graphics(|g| {
286 if let Some(list) = g.get(self.layer_id) {
287 for c in list.all_entries() {
288 reader(c);
289 }
290 }
291 });
292 }
293}
294
295impl Painter {
297 #[allow(clippy::needless_pass_by_value)]
298 pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
299 self.rect(
300 rect,
301 0.0,
302 color.additive().linear_multiply(0.015),
303 (1.0, color),
304 StrokeKind::Outside,
305 );
306 self.text(
307 rect.min,
308 Align2::LEFT_TOP,
309 text.to_string(),
310 FontId::monospace(12.0),
311 color,
312 );
313 }
314
315 pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
316 let color = self.ctx.style().visuals.error_fg_color;
317 self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}"))
318 }
319
320 #[allow(clippy::needless_pass_by_value)]
324 pub fn debug_text(
325 &self,
326 pos: Pos2,
327 anchor: Align2,
328 color: Color32,
329 text: impl ToString,
330 ) -> Rect {
331 let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(12.0), color);
332 let rect = anchor.anchor_size(pos, galley.size());
333 let frame_rect = rect.expand(2.0);
334
335 let is_text_bright = color.is_additive() || epaint::Rgba::from(color).intensity() > 0.5;
336 let bg_color = if is_text_bright {
337 Color32::from_black_alpha(150)
338 } else {
339 Color32::from_white_alpha(150)
340 };
341 self.add(Shape::rect_filled(frame_rect, 0.0, bg_color));
342 self.galley(rect.min, galley, color);
343 frame_rect
344 }
345}
346
347impl Painter {
349 pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) -> ShapeIdx {
351 self.add(Shape::LineSegment {
352 points,
353 stroke: stroke.into(),
354 })
355 }
356
357 pub fn line(&self, points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> ShapeIdx {
360 self.add(Shape::line(points, stroke))
361 }
362
363 pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
365 self.add(Shape::hline(x, y, stroke))
366 }
367
368 pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) -> ShapeIdx {
370 self.add(Shape::vline(x, y, stroke))
371 }
372
373 pub fn circle(
374 &self,
375 center: Pos2,
376 radius: f32,
377 fill_color: impl Into<Color32>,
378 stroke: impl Into<Stroke>,
379 ) -> ShapeIdx {
380 self.add(CircleShape {
381 center,
382 radius,
383 fill: fill_color.into(),
384 stroke: stroke.into(),
385 })
386 }
387
388 pub fn circle_filled(
389 &self,
390 center: Pos2,
391 radius: f32,
392 fill_color: impl Into<Color32>,
393 ) -> ShapeIdx {
394 self.add(CircleShape {
395 center,
396 radius,
397 fill: fill_color.into(),
398 stroke: Default::default(),
399 })
400 }
401
402 pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
403 self.add(CircleShape {
404 center,
405 radius,
406 fill: Default::default(),
407 stroke: stroke.into(),
408 })
409 }
410
411 pub fn rect(
413 &self,
414 rect: Rect,
415 corner_radius: impl Into<CornerRadius>,
416 fill_color: impl Into<Color32>,
417 stroke: impl Into<Stroke>,
418 stroke_kind: StrokeKind,
419 ) -> ShapeIdx {
420 self.add(RectShape::new(
421 rect,
422 corner_radius,
423 fill_color,
424 stroke,
425 stroke_kind,
426 ))
427 }
428
429 pub fn rect_filled(
430 &self,
431 rect: Rect,
432 corner_radius: impl Into<CornerRadius>,
433 fill_color: impl Into<Color32>,
434 ) -> ShapeIdx {
435 self.add(RectShape::filled(rect, corner_radius, fill_color))
436 }
437
438 pub fn rect_stroke(
439 &self,
440 rect: Rect,
441 corner_radius: impl Into<CornerRadius>,
442 stroke: impl Into<Stroke>,
443 stroke_kind: StrokeKind,
444 ) -> ShapeIdx {
445 self.add(RectShape::stroke(rect, corner_radius, stroke, stroke_kind))
446 }
447
448 pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: impl Into<Stroke>) {
450 use crate::emath::Rot2;
451 let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
452 let tip_length = vec.length() / 4.0;
453 let tip = origin + vec;
454 let dir = vec.normalized();
455 let stroke = stroke.into();
456 self.line_segment([origin, tip], stroke);
457 self.line_segment([tip, tip - tip_length * (rot * dir)], stroke);
458 self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke);
459 }
460
461 pub fn image(
480 &self,
481 texture_id: epaint::TextureId,
482 rect: Rect,
483 uv: Rect,
484 tint: Color32,
485 ) -> ShapeIdx {
486 self.add(Shape::image(texture_id, rect, uv, tint))
487 }
488}
489
490impl Painter {
492 #[allow(clippy::needless_pass_by_value)]
501 pub fn text(
502 &self,
503 pos: Pos2,
504 anchor: Align2,
505 text: impl ToString,
506 font_id: FontId,
507 text_color: Color32,
508 ) -> Rect {
509 let galley = self.layout_no_wrap(text.to_string(), font_id, text_color);
510 let rect = anchor.anchor_size(pos, galley.size());
511 self.galley(rect.min, galley, text_color);
512 rect
513 }
514
515 #[inline]
519 #[must_use]
520 pub fn layout(
521 &self,
522 text: String,
523 font_id: FontId,
524 color: crate::Color32,
525 wrap_width: f32,
526 ) -> Arc<Galley> {
527 self.fonts(|f| f.layout(text, font_id, color, wrap_width))
528 }
529
530 #[inline]
534 #[must_use]
535 pub fn layout_no_wrap(
536 &self,
537 text: String,
538 font_id: FontId,
539 color: crate::Color32,
540 ) -> Arc<Galley> {
541 self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
542 }
543
544 #[inline]
548 #[must_use]
549 pub fn layout_job(&self, layout_job: LayoutJob) -> Arc<Galley> {
550 self.fonts(|f| f.layout_job(layout_job))
551 }
552
553 #[inline]
561 pub fn galley(&self, pos: Pos2, galley: Arc<Galley>, fallback_color: Color32) {
562 if !galley.is_empty() {
563 self.add(Shape::galley(pos, galley, fallback_color));
564 }
565 }
566
567 #[inline]
573 pub fn galley_with_override_text_color(
574 &self,
575 pos: Pos2,
576 galley: Arc<Galley>,
577 text_color: Color32,
578 ) {
579 if !galley.is_empty() {
580 self.add(Shape::galley_with_override_text_color(
581 pos, galley, text_color,
582 ));
583 }
584 }
585
586 #[deprecated = "Use `Painter::galley` or `Painter::galley_with_override_text_color` instead"]
587 #[inline]
588 pub fn galley_with_color(&self, pos: Pos2, galley: Arc<Galley>, text_color: Color32) {
589 if !galley.is_empty() {
590 self.add(Shape::galley_with_override_text_color(
591 pos, galley, text_color,
592 ));
593 }
594 }
595}
596
597fn tint_shape_towards(shape: &mut Shape, target: Color32) {
598 epaint::shape_transform::adjust_colors(shape, move |color| {
599 if *color != Color32::PLACEHOLDER {
600 *color = crate::ecolor::tint_color_towards(*color, target);
601 }
602 });
603}
604
605fn multiply_opacity(shape: &mut Shape, opacity: f32) {
606 epaint::shape_transform::adjust_colors(shape, move |color| {
607 if *color != Color32::PLACEHOLDER {
608 *color = color.gamma_multiply(opacity);
609 }
610 });
611}