Skip to main content

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