1use std::sync::Arc;
2
3use emath::GuiRounding as _;
4use epaint::{
5 CircleShape, ClippedShape, CornerRadius, PathStroke, RectShape, Shape, Stroke, StrokeKind,
6 text::{Fonts, 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(&Fonts) -> R) -> R {
149 self.ctx.fonts(reader)
150 }
151
152 #[inline]
154 pub fn layer_id(&self) -> LayerId {
155 self.layer_id
156 }
157
158 #[inline]
161 pub fn clip_rect(&self) -> Rect {
162 self.clip_rect
163 }
164
165 #[inline]
171 pub fn shrink_clip_rect(&mut self, new_clip_rect: Rect) {
172 self.clip_rect = self.clip_rect.intersect(new_clip_rect);
173 }
174
175 #[inline]
181 pub fn set_clip_rect(&mut self, clip_rect: Rect) {
182 self.clip_rect = clip_rect;
183 }
184
185 #[inline]
187 pub fn round_to_pixel_center(&self, point: f32) -> f32 {
188 point.round_to_pixel_center(self.pixels_per_point())
189 }
190
191 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
193 #[inline]
194 pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 {
195 pos.round_to_pixel_center(self.pixels_per_point())
196 }
197
198 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
200 #[inline]
201 pub fn round_to_pixel(&self, point: f32) -> f32 {
202 point.round_to_pixels(self.pixels_per_point())
203 }
204
205 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
207 #[inline]
208 pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
209 vec.round_to_pixels(self.pixels_per_point())
210 }
211
212 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
214 #[inline]
215 pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
216 pos.round_to_pixels(self.pixels_per_point())
217 }
218
219 #[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
221 #[inline]
222 pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect {
223 rect.round_to_pixels(self.pixels_per_point())
224 }
225}
226
227impl Painter {
229 #[inline]
230 fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
231 self.ctx.graphics_mut(|g| writer(g.entry(self.layer_id)))
232 }
233
234 fn transform_shape(&self, shape: &mut Shape) {
235 if let Some(fade_to_color) = self.fade_to_color {
236 tint_shape_towards(shape, fade_to_color);
237 }
238 if self.opacity_factor < 1.0 {
239 multiply_opacity(shape, self.opacity_factor);
240 }
241 }
242
243 pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
247 if self.fade_to_color == Some(Color32::TRANSPARENT) || self.opacity_factor == 0.0 {
248 self.paint_list(|l| l.add(self.clip_rect, Shape::Noop))
249 } else {
250 let mut shape = shape.into();
251 self.transform_shape(&mut shape);
252 self.paint_list(|l| l.add(self.clip_rect, shape))
253 }
254 }
255
256 pub fn extend<I: IntoIterator<Item = Shape>>(&self, shapes: I) {
260 if self.fade_to_color == Some(Color32::TRANSPARENT) || self.opacity_factor == 0.0 {
261 return;
262 }
263 if self.fade_to_color.is_some() || self.opacity_factor < 1.0 {
264 let shapes = shapes.into_iter().map(|mut shape| {
265 self.transform_shape(&mut shape);
266 shape
267 });
268 self.paint_list(|l| l.extend(self.clip_rect, shapes));
269 } else {
270 self.paint_list(|l| l.extend(self.clip_rect, shapes));
271 }
272 }
273
274 pub fn set(&self, idx: ShapeIdx, shape: impl Into<Shape>) {
276 if self.fade_to_color == Some(Color32::TRANSPARENT) {
277 return;
278 }
279 let mut shape = shape.into();
280 self.transform_shape(&mut shape);
281 self.paint_list(|l| l.set(idx, self.clip_rect, shape));
282 }
283
284 pub fn for_each_shape(&self, mut reader: impl FnMut(&ClippedShape)) {
286 self.ctx.graphics(|g| {
287 if let Some(list) = g.get(self.layer_id) {
288 for c in list.all_entries() {
289 reader(c);
290 }
291 }
292 });
293 }
294}
295
296impl Painter {
298 #[expect(clippy::needless_pass_by_value)]
299 pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
300 self.rect(
301 rect,
302 0.0,
303 color.additive().linear_multiply(0.015),
304 (1.0, color),
305 StrokeKind::Outside,
306 );
307 self.text(
308 rect.min,
309 Align2::LEFT_TOP,
310 text.to_string(),
311 FontId::monospace(12.0),
312 color,
313 );
314 }
315
316 pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
317 let color = self.ctx.style().visuals.error_fg_color;
318 self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}"))
319 }
320
321 #[expect(clippy::needless_pass_by_value)]
325 pub fn debug_text(
326 &self,
327 pos: Pos2,
328 anchor: Align2,
329 color: Color32,
330 text: impl ToString,
331 ) -> Rect {
332 let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(12.0), color);
333 let rect = anchor.anchor_size(pos, galley.size());
334 let frame_rect = rect.expand(2.0);
335
336 let is_text_bright = color.is_additive() || epaint::Rgba::from(color).intensity() > 0.5;
337 let bg_color = if is_text_bright {
338 Color32::from_black_alpha(150)
339 } else {
340 Color32::from_white_alpha(150)
341 };
342 self.add(Shape::rect_filled(frame_rect, 0.0, bg_color));
343 self.galley(rect.min, galley, color);
344 frame_rect
345 }
346}
347
348impl Painter {
350 pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) -> ShapeIdx {
352 self.add(Shape::LineSegment {
353 points,
354 stroke: stroke.into(),
355 })
356 }
357
358 pub fn line(&self, points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> ShapeIdx {
361 self.add(Shape::line(points, stroke))
362 }
363
364 pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
366 self.add(Shape::hline(x, y, stroke))
367 }
368
369 pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) -> ShapeIdx {
371 self.add(Shape::vline(x, y, stroke))
372 }
373
374 pub fn circle(
375 &self,
376 center: Pos2,
377 radius: f32,
378 fill_color: impl Into<Color32>,
379 stroke: impl Into<Stroke>,
380 ) -> ShapeIdx {
381 self.add(CircleShape {
382 center,
383 radius,
384 fill: fill_color.into(),
385 stroke: stroke.into(),
386 })
387 }
388
389 pub fn circle_filled(
390 &self,
391 center: Pos2,
392 radius: f32,
393 fill_color: impl Into<Color32>,
394 ) -> ShapeIdx {
395 self.add(CircleShape {
396 center,
397 radius,
398 fill: fill_color.into(),
399 stroke: Default::default(),
400 })
401 }
402
403 pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
404 self.add(CircleShape {
405 center,
406 radius,
407 fill: Default::default(),
408 stroke: stroke.into(),
409 })
410 }
411
412 pub fn rect(
414 &self,
415 rect: Rect,
416 corner_radius: impl Into<CornerRadius>,
417 fill_color: impl Into<Color32>,
418 stroke: impl Into<Stroke>,
419 stroke_kind: StrokeKind,
420 ) -> ShapeIdx {
421 self.add(RectShape::new(
422 rect,
423 corner_radius,
424 fill_color,
425 stroke,
426 stroke_kind,
427 ))
428 }
429
430 pub fn rect_filled(
431 &self,
432 rect: Rect,
433 corner_radius: impl Into<CornerRadius>,
434 fill_color: impl Into<Color32>,
435 ) -> ShapeIdx {
436 self.add(RectShape::filled(rect, corner_radius, fill_color))
437 }
438
439 pub fn rect_stroke(
440 &self,
441 rect: Rect,
442 corner_radius: impl Into<CornerRadius>,
443 stroke: impl Into<Stroke>,
444 stroke_kind: StrokeKind,
445 ) -> ShapeIdx {
446 self.add(RectShape::stroke(rect, corner_radius, stroke, stroke_kind))
447 }
448
449 pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: impl Into<Stroke>) {
451 use crate::emath::Rot2;
452 let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
453 let tip_length = vec.length() / 4.0;
454 let tip = origin + vec;
455 let dir = vec.normalized();
456 let stroke = stroke.into();
457 self.line_segment([origin, tip], stroke);
458 self.line_segment([tip, tip - tip_length * (rot * dir)], stroke);
459 self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke);
460 }
461
462 pub fn image(
481 &self,
482 texture_id: epaint::TextureId,
483 rect: Rect,
484 uv: Rect,
485 tint: Color32,
486 ) -> ShapeIdx {
487 self.add(Shape::image(texture_id, rect, uv, tint))
488 }
489}
490
491impl Painter {
493 #[expect(clippy::needless_pass_by_value)]
502 pub fn text(
503 &self,
504 pos: Pos2,
505 anchor: Align2,
506 text: impl ToString,
507 font_id: FontId,
508 text_color: Color32,
509 ) -> Rect {
510 let galley = self.layout_no_wrap(text.to_string(), font_id, text_color);
511 let rect = anchor.anchor_size(pos, galley.size());
512 self.galley(rect.min, galley, text_color);
513 rect
514 }
515
516 #[inline]
520 #[must_use]
521 pub fn layout(
522 &self,
523 text: String,
524 font_id: FontId,
525 color: crate::Color32,
526 wrap_width: f32,
527 ) -> Arc<Galley> {
528 self.fonts(|f| f.layout(text, font_id, color, wrap_width))
529 }
530
531 #[inline]
535 #[must_use]
536 pub fn layout_no_wrap(
537 &self,
538 text: String,
539 font_id: FontId,
540 color: crate::Color32,
541 ) -> Arc<Galley> {
542 self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
543 }
544
545 #[inline]
549 #[must_use]
550 pub fn layout_job(&self, layout_job: LayoutJob) -> Arc<Galley> {
551 self.fonts(|f| f.layout_job(layout_job))
552 }
553
554 #[inline]
562 pub fn galley(&self, pos: Pos2, galley: Arc<Galley>, fallback_color: Color32) {
563 if !galley.is_empty() {
564 self.add(Shape::galley(pos, galley, fallback_color));
565 }
566 }
567
568 #[inline]
574 pub fn galley_with_override_text_color(
575 &self,
576 pos: Pos2,
577 galley: Arc<Galley>,
578 text_color: Color32,
579 ) {
580 if !galley.is_empty() {
581 self.add(Shape::galley_with_override_text_color(
582 pos, galley, text_color,
583 ));
584 }
585 }
586}
587
588fn tint_shape_towards(shape: &mut Shape, target: Color32) {
589 epaint::shape_transform::adjust_colors(shape, move |color| {
590 if *color != Color32::PLACEHOLDER {
591 *color = crate::ecolor::tint_color_towards(*color, target);
592 }
593 });
594}
595
596fn multiply_opacity(shape: &mut Shape, opacity: f32) {
597 epaint::shape_transform::adjust_colors(shape, move |color| {
598 if *color != Color32::PLACEHOLDER {
599 *color = color.gamma_multiply(opacity);
600 }
601 });
602}