egui/
painter.rs

1use std::sync::Arc;
2
3use crate::{
4    emath::{Align2, Pos2, Rangef, Rect, Vec2},
5    layers::{LayerId, PaintList, ShapeIdx},
6    Color32, Context, FontId,
7};
8use epaint::{
9    text::{Fonts, Galley, LayoutJob},
10    CircleShape, ClippedShape, PathStroke, RectShape, Rounding, Shape, Stroke,
11};
12
13/// Helper to paint shapes and text to a specific region on a specific layer.
14///
15/// All coordinates are screen coordinates in the unit points (one point can consist of many physical pixels).
16#[derive(Clone)]
17pub struct Painter {
18    /// Source of fonts and destination of shapes
19    ctx: Context,
20
21    /// Where we paint
22    layer_id: LayerId,
23
24    /// Everything painted in this [`Painter`] will be clipped against this.
25    /// This means nothing outside of this rectangle will be visible on screen.
26    clip_rect: Rect,
27
28    /// If set, all shapes will have their colors modified to be closer to this.
29    /// This is used to implement grayed out interfaces.
30    fade_to_color: Option<Color32>,
31
32    /// If set, all shapes will have their colors modified with [`Color32::gamma_multiply`] with
33    /// this value as the factor.
34    /// This is used to make interfaces semi-transparent.
35    opacity_factor: f32,
36}
37
38impl Painter {
39    /// Create a painter to a specific layer within a certain clip rectangle.
40    pub fn new(ctx: Context, layer_id: LayerId, clip_rect: Rect) -> Self {
41        Self {
42            ctx,
43            layer_id,
44            clip_rect,
45            fade_to_color: None,
46            opacity_factor: 1.0,
47        }
48    }
49
50    /// Redirect where you are painting.
51    #[must_use]
52    pub fn with_layer_id(self, layer_id: LayerId) -> Self {
53        Self {
54            ctx: self.ctx,
55            layer_id,
56            clip_rect: self.clip_rect,
57            fade_to_color: None,
58            opacity_factor: 1.0,
59        }
60    }
61
62    /// Create a painter for a sub-region of this [`Painter`].
63    ///
64    /// The clip-rect of the returned [`Painter`] will be the intersection
65    /// of the given rectangle and the `clip_rect()` of the parent [`Painter`].
66    pub fn with_clip_rect(&self, rect: Rect) -> Self {
67        Self {
68            ctx: self.ctx.clone(),
69            layer_id: self.layer_id,
70            clip_rect: rect.intersect(self.clip_rect),
71            fade_to_color: self.fade_to_color,
72            opacity_factor: self.opacity_factor,
73        }
74    }
75
76    /// Redirect where you are painting.
77    ///
78    /// It is undefined behavior to change the [`LayerId`]
79    /// of [`crate::Ui::painter`].
80    pub fn set_layer_id(&mut self, layer_id: LayerId) {
81        self.layer_id = layer_id;
82    }
83
84    /// If set, colors will be modified to look like this
85    pub(crate) fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
86        self.fade_to_color = fade_to_color;
87    }
88
89    /// Set the opacity (alpha multiplier) of everything painted by this painter from this point forward.
90    ///
91    /// `opacity` must be between 0.0 and 1.0, where 0.0 means fully transparent (i.e., invisible)
92    /// and 1.0 means fully opaque.
93    ///
94    /// See also: [`Self::opacity`] and [`Self::multiply_opacity`].
95    pub fn set_opacity(&mut self, opacity: f32) {
96        if opacity.is_finite() {
97            self.opacity_factor = opacity.clamp(0.0, 1.0);
98        }
99    }
100
101    /// Like [`Self::set_opacity`], but multiplies the given value with the current opacity.
102    ///
103    /// See also: [`Self::set_opacity`] and [`Self::opacity`].
104    pub fn multiply_opacity(&mut self, opacity: f32) {
105        if opacity.is_finite() {
106            self.opacity_factor *= opacity.clamp(0.0, 1.0);
107        }
108    }
109
110    /// Read the current opacity of the underlying painter.
111    ///
112    /// See also: [`Self::set_opacity`] and [`Self::multiply_opacity`].
113    #[inline]
114    pub fn opacity(&self) -> f32 {
115        self.opacity_factor
116    }
117
118    /// If `false`, nothing you paint will show up.
119    ///
120    /// Also checks [`Context::will_discard`].
121    pub(crate) fn is_visible(&self) -> bool {
122        self.fade_to_color != Some(Color32::TRANSPARENT) && !self.ctx.will_discard()
123    }
124
125    /// If `false`, nothing added to the painter will be visible
126    pub(crate) fn set_invisible(&mut self) {
127        self.fade_to_color = Some(Color32::TRANSPARENT);
128    }
129}
130
131/// ## Accessors etc
132impl Painter {
133    /// Get a reference to the parent [`Context`].
134    #[inline]
135    pub fn ctx(&self) -> &Context {
136        &self.ctx
137    }
138
139    /// Read-only access to the shared [`Fonts`].
140    ///
141    /// See [`Context`] documentation for how locks work.
142    #[inline]
143    pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
144        self.ctx.fonts(reader)
145    }
146
147    /// Where we paint
148    #[inline]
149    pub fn layer_id(&self) -> LayerId {
150        self.layer_id
151    }
152
153    /// Everything painted in this [`Painter`] will be clipped against this.
154    /// This means nothing outside of this rectangle will be visible on screen.
155    #[inline]
156    pub fn clip_rect(&self) -> Rect {
157        self.clip_rect
158    }
159
160    /// Constrain the rectangle in which we can paint.
161    ///
162    /// Short for `painter.set_clip_rect(painter.clip_rect().intersect(new_clip_rect))`.
163    ///
164    /// See also: [`Self::clip_rect`] and [`Self::set_clip_rect`].
165    #[inline]
166    pub fn shrink_clip_rect(&mut self, new_clip_rect: Rect) {
167        self.clip_rect = self.clip_rect.intersect(new_clip_rect);
168    }
169
170    /// Everything painted in this [`Painter`] will be clipped against this.
171    /// This means nothing outside of this rectangle will be visible on screen.
172    ///
173    /// Warning: growing the clip rect might cause unexpected results!
174    /// When in doubt, use [`Self::shrink_clip_rect`] instead.
175    #[inline]
176    pub fn set_clip_rect(&mut self, clip_rect: Rect) {
177        self.clip_rect = clip_rect;
178    }
179
180    /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels).
181    #[inline]
182    pub fn round_to_pixel_center(&self, point: f32) -> f32 {
183        self.ctx().round_to_pixel_center(point)
184    }
185
186    /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels).
187    #[inline]
188    pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 {
189        self.ctx().round_pos_to_pixel_center(pos)
190    }
191
192    /// Useful for pixel-perfect rendering of filled shapes.
193    #[inline]
194    pub fn round_to_pixel(&self, point: f32) -> f32 {
195        self.ctx().round_to_pixel(point)
196    }
197
198    /// Useful for pixel-perfect rendering.
199    #[inline]
200    pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
201        self.ctx().round_vec_to_pixels(vec)
202    }
203
204    /// Useful for pixel-perfect rendering.
205    #[inline]
206    pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
207        self.ctx().round_pos_to_pixels(pos)
208    }
209
210    /// Useful for pixel-perfect rendering.
211    #[inline]
212    pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect {
213        self.ctx().round_rect_to_pixels(rect)
214    }
215}
216
217/// ## Low level
218impl Painter {
219    #[inline]
220    fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
221        self.ctx.graphics_mut(|g| writer(g.entry(self.layer_id)))
222    }
223
224    fn transform_shape(&self, shape: &mut Shape) {
225        if let Some(fade_to_color) = self.fade_to_color {
226            tint_shape_towards(shape, fade_to_color);
227        }
228        if self.opacity_factor < 1.0 {
229            multiply_opacity(shape, self.opacity_factor);
230        }
231    }
232
233    /// It is up to the caller to make sure there is room for this.
234    /// Can be used for free painting.
235    /// NOTE: all coordinates are screen coordinates!
236    pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
237        if self.fade_to_color == Some(Color32::TRANSPARENT) || self.opacity_factor == 0.0 {
238            self.paint_list(|l| l.add(self.clip_rect, Shape::Noop))
239        } else {
240            let mut shape = shape.into();
241            self.transform_shape(&mut shape);
242            self.paint_list(|l| l.add(self.clip_rect, shape))
243        }
244    }
245
246    /// Add many shapes at once.
247    ///
248    /// Calling this once is generally faster than calling [`Self::add`] multiple times.
249    pub fn extend<I: IntoIterator<Item = Shape>>(&self, shapes: I) {
250        if self.fade_to_color == Some(Color32::TRANSPARENT) || self.opacity_factor == 0.0 {
251            return;
252        }
253        if self.fade_to_color.is_some() || self.opacity_factor < 1.0 {
254            let shapes = shapes.into_iter().map(|mut shape| {
255                self.transform_shape(&mut shape);
256                shape
257            });
258            self.paint_list(|l| l.extend(self.clip_rect, shapes));
259        } else {
260            self.paint_list(|l| l.extend(self.clip_rect, shapes));
261        }
262    }
263
264    /// Modify an existing [`Shape`].
265    pub fn set(&self, idx: ShapeIdx, shape: impl Into<Shape>) {
266        if self.fade_to_color == Some(Color32::TRANSPARENT) {
267            return;
268        }
269        let mut shape = shape.into();
270        self.transform_shape(&mut shape);
271        self.paint_list(|l| l.set(idx, self.clip_rect, shape));
272    }
273
274    /// Access all shapes added this frame.
275    pub fn for_each_shape(&self, mut reader: impl FnMut(&ClippedShape)) {
276        self.ctx.graphics(|g| {
277            if let Some(list) = g.get(self.layer_id) {
278                for c in list.all_entries() {
279                    reader(c);
280                }
281            }
282        });
283    }
284}
285
286/// ## Debug painting
287impl Painter {
288    #[allow(clippy::needless_pass_by_value)]
289    pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
290        self.rect(
291            rect,
292            0.0,
293            color.additive().linear_multiply(0.015),
294            (1.0, color),
295        );
296        self.text(
297            rect.min,
298            Align2::LEFT_TOP,
299            text.to_string(),
300            FontId::monospace(12.0),
301            color,
302        );
303    }
304
305    pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
306        let color = self.ctx.style().visuals.error_fg_color;
307        self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}"))
308    }
309
310    /// Text with a background.
311    ///
312    /// See also [`Context::debug_text`].
313    #[allow(clippy::needless_pass_by_value)]
314    pub fn debug_text(
315        &self,
316        pos: Pos2,
317        anchor: Align2,
318        color: Color32,
319        text: impl ToString,
320    ) -> Rect {
321        let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(12.0), color);
322        let rect = anchor.anchor_size(pos, galley.size());
323        let frame_rect = rect.expand(2.0);
324
325        let is_text_bright = color.is_additive() || epaint::Rgba::from(color).intensity() > 0.5;
326        let bg_color = if is_text_bright {
327            Color32::from_black_alpha(150)
328        } else {
329            Color32::from_white_alpha(150)
330        };
331        self.add(Shape::rect_filled(frame_rect, 0.0, bg_color));
332        self.galley(rect.min, galley, color);
333        frame_rect
334    }
335}
336
337/// # Paint different primitives
338impl Painter {
339    /// Paints a line from the first point to the second.
340    pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<PathStroke>) -> ShapeIdx {
341        self.add(Shape::LineSegment {
342            points,
343            stroke: stroke.into(),
344        })
345    }
346
347    /// Paints a line connecting the points.
348    /// NOTE: all coordinates are screen coordinates!
349    pub fn line(&self, points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> ShapeIdx {
350        self.add(Shape::line(points, stroke))
351    }
352
353    /// Paints a horizontal line.
354    pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<PathStroke>) -> ShapeIdx {
355        self.add(Shape::hline(x, y, stroke.into()))
356    }
357
358    /// Paints a vertical line.
359    pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<PathStroke>) -> ShapeIdx {
360        self.add(Shape::vline(x, y, stroke.into()))
361    }
362
363    pub fn circle(
364        &self,
365        center: Pos2,
366        radius: f32,
367        fill_color: impl Into<Color32>,
368        stroke: impl Into<Stroke>,
369    ) -> ShapeIdx {
370        self.add(CircleShape {
371            center,
372            radius,
373            fill: fill_color.into(),
374            stroke: stroke.into(),
375        })
376    }
377
378    pub fn circle_filled(
379        &self,
380        center: Pos2,
381        radius: f32,
382        fill_color: impl Into<Color32>,
383    ) -> ShapeIdx {
384        self.add(CircleShape {
385            center,
386            radius,
387            fill: fill_color.into(),
388            stroke: Default::default(),
389        })
390    }
391
392    pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
393        self.add(CircleShape {
394            center,
395            radius,
396            fill: Default::default(),
397            stroke: stroke.into(),
398        })
399    }
400
401    pub fn rect(
402        &self,
403        rect: Rect,
404        rounding: impl Into<Rounding>,
405        fill_color: impl Into<Color32>,
406        stroke: impl Into<Stroke>,
407    ) -> ShapeIdx {
408        self.add(RectShape::new(rect, rounding, fill_color, stroke))
409    }
410
411    pub fn rect_filled(
412        &self,
413        rect: Rect,
414        rounding: impl Into<Rounding>,
415        fill_color: impl Into<Color32>,
416    ) -> ShapeIdx {
417        self.add(RectShape::filled(rect, rounding, fill_color))
418    }
419
420    pub fn rect_stroke(
421        &self,
422        rect: Rect,
423        rounding: impl Into<Rounding>,
424        stroke: impl Into<Stroke>,
425    ) -> ShapeIdx {
426        self.add(RectShape::stroke(rect, rounding, stroke))
427    }
428
429    /// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`.
430    pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: impl Into<Stroke>) {
431        use crate::emath::Rot2;
432        let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
433        let tip_length = vec.length() / 4.0;
434        let tip = origin + vec;
435        let dir = vec.normalized();
436        let stroke = stroke.into();
437        self.line_segment([origin, tip], stroke);
438        self.line_segment([tip, tip - tip_length * (rot * dir)], stroke);
439        self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke);
440    }
441
442    /// An image at the given position.
443    ///
444    /// `uv` should normally be `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`
445    /// unless you want to crop or flip the image.
446    ///
447    /// `tint` is a color multiplier. Use [`Color32::WHITE`] if you don't want to tint the image.
448    ///
449    /// Usually it is easier to use [`crate::Image::paint_at`] instead:
450    ///
451    /// ```
452    /// # egui::__run_test_ui(|ui| {
453    /// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0));
454    /// egui::Image::new(egui::include_image!("../assets/ferris.png"))
455    ///     .rounding(5.0)
456    ///     .tint(egui::Color32::LIGHT_BLUE)
457    ///     .paint_at(ui, rect);
458    /// # });
459    /// ```
460    pub fn image(
461        &self,
462        texture_id: epaint::TextureId,
463        rect: Rect,
464        uv: Rect,
465        tint: Color32,
466    ) -> ShapeIdx {
467        self.add(Shape::image(texture_id, rect, uv, tint))
468    }
469}
470
471/// ## Text
472impl Painter {
473    /// Lay out and paint some text.
474    ///
475    /// To center the text at the given position, use `Align2::CENTER_CENTER`.
476    ///
477    /// To find out the size of text before painting it, use
478    /// [`Self::layout`] or [`Self::layout_no_wrap`].
479    ///
480    /// Returns where the text ended up.
481    #[allow(clippy::needless_pass_by_value)]
482    pub fn text(
483        &self,
484        pos: Pos2,
485        anchor: Align2,
486        text: impl ToString,
487        font_id: FontId,
488        text_color: Color32,
489    ) -> Rect {
490        let galley = self.layout_no_wrap(text.to_string(), font_id, text_color);
491        let rect = anchor.anchor_size(pos, galley.size());
492        self.galley(rect.min, galley, text_color);
493        rect
494    }
495
496    /// Will wrap text at the given width and line break at `\n`.
497    ///
498    /// Paint the results with [`Self::galley`].
499    #[inline]
500    #[must_use]
501    pub fn layout(
502        &self,
503        text: String,
504        font_id: FontId,
505        color: crate::Color32,
506        wrap_width: f32,
507    ) -> Arc<Galley> {
508        self.fonts(|f| f.layout(text, font_id, color, wrap_width))
509    }
510
511    /// Will line break at `\n`.
512    ///
513    /// Paint the results with [`Self::galley`].
514    #[inline]
515    #[must_use]
516    pub fn layout_no_wrap(
517        &self,
518        text: String,
519        font_id: FontId,
520        color: crate::Color32,
521    ) -> Arc<Galley> {
522        self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
523    }
524
525    /// Lay out this text layut job in a galley.
526    ///
527    /// Paint the results with [`Self::galley`].
528    #[inline]
529    #[must_use]
530    pub fn layout_job(&self, layout_job: LayoutJob) -> Arc<Galley> {
531        self.fonts(|f| f.layout_job(layout_job))
532    }
533
534    /// Paint text that has already been laid out in a [`Galley`].
535    ///
536    /// You can create the [`Galley`] with [`Self::layout`] or [`Self::layout_job`].
537    ///
538    /// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color.
539    ///
540    /// Any non-placeholder color in the galley takes precedence over this fallback color.
541    #[inline]
542    pub fn galley(&self, pos: Pos2, galley: Arc<Galley>, fallback_color: Color32) {
543        if !galley.is_empty() {
544            self.add(Shape::galley(pos, galley, fallback_color));
545        }
546    }
547
548    /// Paint text that has already been laid out in a [`Galley`].
549    ///
550    /// You can create the [`Galley`] with [`Self::layout`].
551    ///
552    /// All text color in the [`Galley`] will be replaced with the given color.
553    #[inline]
554    pub fn galley_with_override_text_color(
555        &self,
556        pos: Pos2,
557        galley: Arc<Galley>,
558        text_color: Color32,
559    ) {
560        if !galley.is_empty() {
561            self.add(Shape::galley_with_override_text_color(
562                pos, galley, text_color,
563            ));
564        }
565    }
566
567    #[deprecated = "Use `Painter::galley` or `Painter::galley_with_override_text_color` instead"]
568    #[inline]
569    pub fn galley_with_color(&self, pos: Pos2, galley: Arc<Galley>, text_color: Color32) {
570        if !galley.is_empty() {
571            self.add(Shape::galley_with_override_text_color(
572                pos, galley, text_color,
573            ));
574        }
575    }
576}
577
578fn tint_shape_towards(shape: &mut Shape, target: Color32) {
579    epaint::shape_transform::adjust_colors(shape, move |color| {
580        if *color != Color32::PLACEHOLDER {
581            *color = crate::ecolor::tint_color_towards(*color, target);
582        }
583    });
584}
585
586fn multiply_opacity(shape: &mut Shape, opacity: f32) {
587    epaint::shape_transform::adjust_colors(shape, move |color| {
588        if *color != Color32::PLACEHOLDER {
589            *color = color.gamma_multiply(opacity);
590        }
591    });
592}