egui/
painter.rs

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