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