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 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 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 #[inline]
110 pub fn opacity(&self) -> f32 {
111 self.opacity_factor
112 }
113
114 pub fn is_visible(&self) -> bool {
118 self.fade_to_color != Some(Color32::TRANSPARENT) && !self.ctx.will_discard()
119 }
120
121 pub fn set_invisible(&mut self) {
123 self.fade_to_color = Some(Color32::TRANSPARENT);
124 }
125
126 #[inline]
128 pub fn ctx(&self) -> &Context {
129 &self.ctx
130 }
131
132 #[inline]
134 pub fn pixels_per_point(&self) -> f32 {
135 self.pixels_per_point
136 }
137
138 #[inline]
142 pub fn fonts<R>(&self, reader: impl FnOnce(&FontsView<'_>) -> R) -> R {
143 self.ctx.fonts(reader)
144 }
145
146 #[inline]
150 pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R {
151 self.ctx.fonts_mut(reader)
152 }
153
154 #[inline]
156 pub fn layer_id(&self) -> LayerId {
157 self.layer_id
158 }
159
160 #[inline]
163 pub fn clip_rect(&self) -> Rect {
164 self.clip_rect
165 }
166
167 #[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 #[inline]
183 pub fn set_clip_rect(&mut self, clip_rect: Rect) {
184 self.clip_rect = clip_rect;
185 }
186
187 #[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
194impl 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 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 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 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 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
263impl 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 #[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
315impl Painter {
317 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 pub fn line(&self, points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> ShapeIdx {
328 self.add(Shape::line(points, stroke))
329 }
330
331 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 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 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 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 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
458impl Painter {
460 #[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 #[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 #[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 #[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 #[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 #[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}