egui/
painter.rs

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/// 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    pub fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
87        self.fade_to_color = fade_to_color;
88    }
89
90    /// Set the opacity (alpha multiplier) of everything painted by this painter from this point forward.
91    ///
92    /// `opacity` must be between 0.0 and 1.0, where 0.0 means fully transparent (i.e., invisible)
93    /// and 1.0 means fully opaque.
94    ///
95    /// See also: [`Self::opacity`] and [`Self::multiply_opacity`].
96    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    /// Like [`Self::set_opacity`], but multiplies the given value with the current opacity.
103    ///
104    /// See also: [`Self::set_opacity`] and [`Self::opacity`].
105    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    /// Read the current opacity of the underlying painter.
112    ///
113    /// See also: [`Self::set_opacity`] and [`Self::multiply_opacity`].
114    #[inline]
115    pub fn opacity(&self) -> f32 {
116        self.opacity_factor
117    }
118
119    /// If `false`, nothing you paint will show up.
120    ///
121    /// Also checks [`Context::will_discard`].
122    pub fn is_visible(&self) -> bool {
123        self.fade_to_color != Some(Color32::TRANSPARENT) && !self.ctx.will_discard()
124    }
125
126    /// If `false`, nothing added to the painter will be visible
127    pub fn set_invisible(&mut self) {
128        self.fade_to_color = Some(Color32::TRANSPARENT);
129    }
130
131    /// Get a reference to the parent [`Context`].
132    #[inline]
133    pub fn ctx(&self) -> &Context {
134        &self.ctx
135    }
136
137    /// Number of physical pixels for each logical UI point.
138    #[inline]
139    pub fn pixels_per_point(&self) -> f32 {
140        self.pixels_per_point
141    }
142
143    /// Read-only access to the shared [`Fonts`].
144    ///
145    /// See [`Context`] documentation for how locks work.
146    #[inline]
147    pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
148        self.ctx.fonts(reader)
149    }
150
151    /// Where we paint
152    #[inline]
153    pub fn layer_id(&self) -> LayerId {
154        self.layer_id
155    }
156
157    /// Everything painted in this [`Painter`] will be clipped against this.
158    /// This means nothing outside of this rectangle will be visible on screen.
159    #[inline]
160    pub fn clip_rect(&self) -> Rect {
161        self.clip_rect
162    }
163
164    /// Constrain the rectangle in which we can paint.
165    ///
166    /// Short for `painter.set_clip_rect(painter.clip_rect().intersect(new_clip_rect))`.
167    ///
168    /// See also: [`Self::clip_rect`] and [`Self::set_clip_rect`].
169    #[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    /// Everything painted in this [`Painter`] will be clipped against this.
175    /// This means nothing outside of this rectangle will be visible on screen.
176    ///
177    /// Warning: growing the clip rect might cause unexpected results!
178    /// When in doubt, use [`Self::shrink_clip_rect`] instead.
179    #[inline]
180    pub fn set_clip_rect(&mut self, clip_rect: Rect) {
181        self.clip_rect = clip_rect;
182    }
183
184    /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels).
185    #[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    /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels).
191    #[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    /// Useful for pixel-perfect rendering of filled shapes.
198    #[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    /// Useful for pixel-perfect rendering.
205    #[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    /// Useful for pixel-perfect rendering.
212    #[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    /// Useful for pixel-perfect rendering.
219    #[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
226/// ## Low level
227impl 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    /// It is up to the caller to make sure there is room for this.
243    /// Can be used for free painting.
244    /// NOTE: all coordinates are screen coordinates!
245    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    /// Add many shapes at once.
256    ///
257    /// Calling this once is generally faster than calling [`Self::add`] multiple times.
258    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    /// Modify an existing [`Shape`].
274    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    /// Access all shapes added this frame.
284    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
295/// ## Debug painting
296impl 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    /// Text with a background.
321    ///
322    /// See also [`Context::debug_text`].
323    #[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
347/// # Paint different primitives
348impl Painter {
349    /// Paints a line from the first point to the second.
350    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    /// Paints a line connecting the points.
358    /// NOTE: all coordinates are screen coordinates!
359    pub fn line(&self, points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> ShapeIdx {
360        self.add(Shape::line(points, stroke))
361    }
362
363    /// Paints a horizontal line.
364    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    /// Paints a vertical line.
369    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    /// See also [`Self::rect_filled`] and [`Self::rect_stroke`].
412    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    /// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`.
449    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    /// An image at the given position.
462    ///
463    /// `uv` should normally be `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`
464    /// unless you want to crop or flip the image.
465    ///
466    /// `tint` is a color multiplier. Use [`Color32::WHITE`] if you don't want to tint the image.
467    ///
468    /// Usually it is easier to use [`crate::Image::paint_at`] instead:
469    ///
470    /// ```
471    /// # egui::__run_test_ui(|ui| {
472    /// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0));
473    /// egui::Image::new(egui::include_image!("../assets/ferris.png"))
474    ///     .corner_radius(5)
475    ///     .tint(egui::Color32::LIGHT_BLUE)
476    ///     .paint_at(ui, rect);
477    /// # });
478    /// ```
479    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
490/// ## Text
491impl Painter {
492    /// Lay out and paint some text.
493    ///
494    /// To center the text at the given position, use `Align2::CENTER_CENTER`.
495    ///
496    /// To find out the size of text before painting it, use
497    /// [`Self::layout`] or [`Self::layout_no_wrap`].
498    ///
499    /// Returns where the text ended up.
500    #[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    /// Will wrap text at the given width and line break at `\n`.
516    ///
517    /// Paint the results with [`Self::galley`].
518    #[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    /// Will line break at `\n`.
531    ///
532    /// Paint the results with [`Self::galley`].
533    #[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    /// Lay out this text layut job in a galley.
545    ///
546    /// Paint the results with [`Self::galley`].
547    #[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    /// Paint text that has already been laid out in a [`Galley`].
554    ///
555    /// You can create the [`Galley`] with [`Self::layout`] or [`Self::layout_job`].
556    ///
557    /// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color.
558    ///
559    /// Any non-placeholder color in the galley takes precedence over this fallback color.
560    #[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    /// Paint text that has already been laid out in a [`Galley`].
568    ///
569    /// You can create the [`Galley`] with [`Self::layout`].
570    ///
571    /// All text color in the [`Galley`] will be replaced with the given color.
572    #[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}