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#[derive(Clone)]
21pub struct Painter {
22 ctx: Context,
24
25 pixels_per_point: f32,
27
28 layer_id: LayerId,
30
31 clip_rect: Rect,
34
35 fade_to_color: Option<Color32>,
38
39 opacity_factor: f32,
43}
44
45impl Painter {
46 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 #[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 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 pub fn set_layer_id(&mut self, layer_id: LayerId) {
82 self.layer_id = layer_id;
83 }
84
85 #[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 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 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 #[inline]
116 pub fn opacity(&self) -> f32 {
117 self.opacity_factor
118 }
119
120 pub fn is_visible(&self) -> bool {
124 self.fade_to_color != Some(Color32::TRANSPARENT) && !self.ctx.will_discard()
125 }
126
127 pub fn set_invisible(&mut self) {
129 self.fade_to_color = Some(Color32::TRANSPARENT);
130 }
131
132 #[inline]
134 pub fn ctx(&self) -> &Context {
135 &self.ctx
136 }
137
138 #[inline]
140 pub fn pixels_per_point(&self) -> f32 {
141 self.pixels_per_point
142 }
143
144 #[inline]
148 pub fn fonts<R>(&self, reader: impl FnOnce(&FontsView<'_>) -> R) -> R {
149 self.ctx.fonts(reader)
150 }
151
152 #[inline]
156 pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R {
157 self.ctx.fonts_mut(reader)
158 }
159
160 #[inline]
162 pub fn layer_id(&self) -> LayerId {
163 self.layer_id
164 }
165
166 #[inline]
169 pub fn clip_rect(&self) -> Rect {
170 self.clip_rect
171 }
172
173 #[inline]
179 pub fn shrink_clip_rect(&mut self, new_clip_rect: Rect) {
180 self.clip_rect = self.clip_rect.intersect(new_clip_rect);
181 }
182
183 #[inline]
189 pub fn set_clip_rect(&mut self, clip_rect: Rect) {
190 self.clip_rect = clip_rect;
191 }
192
193 #[inline]
195 pub fn round_to_pixel_center(&self, point: f32) -> f32 {
196 point.round_to_pixel_center(self.pixels_per_point())
197 }
198
199 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
201 #[inline]
202 pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 {
203 pos.round_to_pixel_center(self.pixels_per_point())
204 }
205
206 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
208 #[inline]
209 pub fn round_to_pixel(&self, point: f32) -> f32 {
210 point.round_to_pixels(self.pixels_per_point())
211 }
212
213 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
215 #[inline]
216 pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
217 vec.round_to_pixels(self.pixels_per_point())
218 }
219
220 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
222 #[inline]
223 pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
224 pos.round_to_pixels(self.pixels_per_point())
225 }
226
227 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
229 #[inline]
230 pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect {
231 rect.round_to_pixels(self.pixels_per_point())
232 }
233}
234
235impl Painter {
237 #[inline]
238 fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
239 self.ctx.graphics_mut(|g| writer(g.entry(self.layer_id)))
240 }
241
242 fn transform_shape(&self, shape: &mut Shape) {
243 if let Some(fade_to_color) = self.fade_to_color {
244 tint_shape_towards(shape, fade_to_color);
245 }
246 if self.opacity_factor < 1.0 {
247 multiply_opacity(shape, self.opacity_factor);
248 }
249 }
250
251 pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
255 if self.fade_to_color == Some(Color32::TRANSPARENT) || self.opacity_factor == 0.0 {
256 self.paint_list(|l| l.add(self.clip_rect, Shape::Noop))
257 } else {
258 let mut shape = shape.into();
259 self.transform_shape(&mut shape);
260 self.paint_list(|l| l.add(self.clip_rect, shape))
261 }
262 }
263
264 pub fn extend<I: IntoIterator<Item = Shape>>(&self, shapes: I) {
268 if self.fade_to_color == Some(Color32::TRANSPARENT) || self.opacity_factor == 0.0 {
269 return;
270 }
271 if self.fade_to_color.is_some() || self.opacity_factor < 1.0 {
272 let shapes = shapes.into_iter().map(|mut shape| {
273 self.transform_shape(&mut shape);
274 shape
275 });
276 self.paint_list(|l| l.extend(self.clip_rect, shapes));
277 } else {
278 self.paint_list(|l| l.extend(self.clip_rect, shapes));
279 }
280 }
281
282 pub fn set(&self, idx: ShapeIdx, shape: impl Into<Shape>) {
284 if self.fade_to_color == Some(Color32::TRANSPARENT) {
285 return;
286 }
287 let mut shape = shape.into();
288 self.transform_shape(&mut shape);
289 self.paint_list(|l| l.set(idx, self.clip_rect, shape));
290 }
291
292 pub fn for_each_shape(&self, mut reader: impl FnMut(&ClippedShape)) {
294 self.ctx.graphics(|g| {
295 if let Some(list) = g.get(self.layer_id) {
296 for c in list.all_entries() {
297 reader(c);
298 }
299 }
300 });
301 }
302}
303
304impl Painter {
306 #[expect(clippy::needless_pass_by_value)]
307 pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
308 self.rect(
309 rect,
310 0.0,
311 color.additive().linear_multiply(0.015),
312 (1.0, color),
313 StrokeKind::Outside,
314 );
315 self.text(
316 rect.min,
317 Align2::LEFT_TOP,
318 text.to_string(),
319 FontId::monospace(12.0),
320 color,
321 );
322 }
323
324 pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
325 let color = self.ctx.style().visuals.error_fg_color;
326 self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}"))
327 }
328
329 #[expect(clippy::needless_pass_by_value)]
333 pub fn debug_text(
334 &self,
335 pos: Pos2,
336 anchor: Align2,
337 color: Color32,
338 text: impl ToString,
339 ) -> Rect {
340 let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(12.0), color);
341 let rect = anchor.anchor_size(pos, galley.size());
342 let frame_rect = rect.expand(2.0);
343
344 let is_text_bright = color.is_additive() || epaint::Rgba::from(color).intensity() > 0.5;
345 let bg_color = if is_text_bright {
346 Color32::from_black_alpha(150)
347 } else {
348 Color32::from_white_alpha(150)
349 };
350 self.add(Shape::rect_filled(frame_rect, 0.0, bg_color));
351 self.galley(rect.min, galley, color);
352 frame_rect
353 }
354}
355
356impl Painter {
358 pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) -> ShapeIdx {
360 self.add(Shape::LineSegment {
361 points,
362 stroke: stroke.into(),
363 })
364 }
365
366 pub fn line(&self, points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> ShapeIdx {
369 self.add(Shape::line(points, stroke))
370 }
371
372 pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
374 self.add(Shape::hline(x, y, stroke))
375 }
376
377 pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) -> ShapeIdx {
379 self.add(Shape::vline(x, y, stroke))
380 }
381
382 pub fn circle(
383 &self,
384 center: Pos2,
385 radius: f32,
386 fill_color: impl Into<Color32>,
387 stroke: impl Into<Stroke>,
388 ) -> ShapeIdx {
389 self.add(CircleShape {
390 center,
391 radius,
392 fill: fill_color.into(),
393 stroke: stroke.into(),
394 })
395 }
396
397 pub fn circle_filled(
398 &self,
399 center: Pos2,
400 radius: f32,
401 fill_color: impl Into<Color32>,
402 ) -> ShapeIdx {
403 self.add(CircleShape {
404 center,
405 radius,
406 fill: fill_color.into(),
407 stroke: Default::default(),
408 })
409 }
410
411 pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
412 self.add(CircleShape {
413 center,
414 radius,
415 fill: Default::default(),
416 stroke: stroke.into(),
417 })
418 }
419
420 pub fn rect(
422 &self,
423 rect: Rect,
424 corner_radius: impl Into<CornerRadius>,
425 fill_color: impl Into<Color32>,
426 stroke: impl Into<Stroke>,
427 stroke_kind: StrokeKind,
428 ) -> ShapeIdx {
429 self.add(RectShape::new(
430 rect,
431 corner_radius,
432 fill_color,
433 stroke,
434 stroke_kind,
435 ))
436 }
437
438 pub fn rect_filled(
439 &self,
440 rect: Rect,
441 corner_radius: impl Into<CornerRadius>,
442 fill_color: impl Into<Color32>,
443 ) -> ShapeIdx {
444 self.add(RectShape::filled(rect, corner_radius, fill_color))
445 }
446
447 pub fn rect_stroke(
448 &self,
449 rect: Rect,
450 corner_radius: impl Into<CornerRadius>,
451 stroke: impl Into<Stroke>,
452 stroke_kind: StrokeKind,
453 ) -> ShapeIdx {
454 self.add(RectShape::stroke(rect, corner_radius, stroke, stroke_kind))
455 }
456
457 pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: impl Into<Stroke>) {
459 use crate::emath::Rot2;
460 let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
461 let tip_length = vec.length() / 4.0;
462 let tip = origin + vec;
463 let dir = vec.normalized();
464 let stroke = stroke.into();
465 self.line_segment([origin, tip], stroke);
466 self.line_segment([tip, tip - tip_length * (rot * dir)], stroke);
467 self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke);
468 }
469
470 pub fn image(
489 &self,
490 texture_id: epaint::TextureId,
491 rect: Rect,
492 uv: Rect,
493 tint: Color32,
494 ) -> ShapeIdx {
495 self.add(Shape::image(texture_id, rect, uv, tint))
496 }
497}
498
499impl Painter {
501 #[expect(clippy::needless_pass_by_value)]
510 pub fn text(
511 &self,
512 pos: Pos2,
513 anchor: Align2,
514 text: impl ToString,
515 font_id: FontId,
516 text_color: Color32,
517 ) -> Rect {
518 let galley = self.layout_no_wrap(text.to_string(), font_id, text_color);
519 let rect = anchor.anchor_size(pos, galley.size());
520 self.galley(rect.min, galley, text_color);
521 rect
522 }
523
524 #[inline]
528 #[must_use]
529 pub fn layout(
530 &self,
531 text: String,
532 font_id: FontId,
533 color: crate::Color32,
534 wrap_width: f32,
535 ) -> Arc<Galley> {
536 self.fonts_mut(|f| f.layout(text, font_id, color, wrap_width))
537 }
538
539 #[inline]
543 #[must_use]
544 pub fn layout_no_wrap(
545 &self,
546 text: String,
547 font_id: FontId,
548 color: crate::Color32,
549 ) -> Arc<Galley> {
550 self.fonts_mut(|f| f.layout(text, font_id, color, f32::INFINITY))
551 }
552
553 #[inline]
557 #[must_use]
558 pub fn layout_job(&self, layout_job: LayoutJob) -> Arc<Galley> {
559 self.fonts_mut(|f| f.layout_job(layout_job))
560 }
561
562 #[inline]
570 pub fn galley(&self, pos: Pos2, galley: Arc<Galley>, fallback_color: Color32) {
571 if !galley.is_empty() {
572 self.add(Shape::galley(pos, galley, fallback_color));
573 }
574 }
575
576 #[inline]
582 pub fn galley_with_override_text_color(
583 &self,
584 pos: Pos2,
585 galley: Arc<Galley>,
586 text_color: Color32,
587 ) {
588 if !galley.is_empty() {
589 self.add(Shape::galley_with_override_text_color(
590 pos, galley, text_color,
591 ));
592 }
593 }
594}
595
596fn tint_shape_towards(shape: &mut Shape, target: Color32) {
597 epaint::shape_transform::adjust_colors(shape, move |color| {
598 if *color != Color32::PLACEHOLDER {
599 *color = crate::ecolor::tint_color_towards(*color, target);
600 }
601 });
602}
603
604fn multiply_opacity(shape: &mut Shape, opacity: f32) {
605 epaint::shape_transform::adjust_colors(shape, move |color| {
606 if *color != Color32::PLACEHOLDER {
607 *color = color.gamma_multiply(opacity);
608 }
609 });
610}