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#[derive(Clone)]
17pub struct Painter {
18 ctx: Context,
20
21 layer_id: LayerId,
23
24 clip_rect: Rect,
27
28 fade_to_color: Option<Color32>,
31
32 opacity_factor: f32,
36}
37
38impl Painter {
39 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 #[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 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 pub fn set_layer_id(&mut self, layer_id: LayerId) {
81 self.layer_id = layer_id;
82 }
83
84 pub(crate) fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
86 self.fade_to_color = fade_to_color;
87 }
88
89 pub fn set_opacity(&mut self, opacity: f32) {
96 if opacity.is_finite() {
97 self.opacity_factor = opacity.clamp(0.0, 1.0);
98 }
99 }
100
101 pub fn multiply_opacity(&mut self, opacity: f32) {
105 if opacity.is_finite() {
106 self.opacity_factor *= opacity.clamp(0.0, 1.0);
107 }
108 }
109
110 #[inline]
114 pub fn opacity(&self) -> f32 {
115 self.opacity_factor
116 }
117
118 pub(crate) fn is_visible(&self) -> bool {
122 self.fade_to_color != Some(Color32::TRANSPARENT) && !self.ctx.will_discard()
123 }
124
125 pub(crate) fn set_invisible(&mut self) {
127 self.fade_to_color = Some(Color32::TRANSPARENT);
128 }
129}
130
131impl Painter {
133 #[inline]
135 pub fn ctx(&self) -> &Context {
136 &self.ctx
137 }
138
139 #[inline]
143 pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
144 self.ctx.fonts(reader)
145 }
146
147 #[inline]
149 pub fn layer_id(&self) -> LayerId {
150 self.layer_id
151 }
152
153 #[inline]
156 pub fn clip_rect(&self) -> Rect {
157 self.clip_rect
158 }
159
160 #[inline]
166 pub fn shrink_clip_rect(&mut self, new_clip_rect: Rect) {
167 self.clip_rect = self.clip_rect.intersect(new_clip_rect);
168 }
169
170 #[inline]
176 pub fn set_clip_rect(&mut self, clip_rect: Rect) {
177 self.clip_rect = clip_rect;
178 }
179
180 #[inline]
182 pub fn round_to_pixel_center(&self, point: f32) -> f32 {
183 self.ctx().round_to_pixel_center(point)
184 }
185
186 #[inline]
188 pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 {
189 self.ctx().round_pos_to_pixel_center(pos)
190 }
191
192 #[inline]
194 pub fn round_to_pixel(&self, point: f32) -> f32 {
195 self.ctx().round_to_pixel(point)
196 }
197
198 #[inline]
200 pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
201 self.ctx().round_vec_to_pixels(vec)
202 }
203
204 #[inline]
206 pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
207 self.ctx().round_pos_to_pixels(pos)
208 }
209
210 #[inline]
212 pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect {
213 self.ctx().round_rect_to_pixels(rect)
214 }
215}
216
217impl Painter {
219 #[inline]
220 fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
221 self.ctx.graphics_mut(|g| writer(g.entry(self.layer_id)))
222 }
223
224 fn transform_shape(&self, shape: &mut Shape) {
225 if let Some(fade_to_color) = self.fade_to_color {
226 tint_shape_towards(shape, fade_to_color);
227 }
228 if self.opacity_factor < 1.0 {
229 multiply_opacity(shape, self.opacity_factor);
230 }
231 }
232
233 pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
237 if self.fade_to_color == Some(Color32::TRANSPARENT) || self.opacity_factor == 0.0 {
238 self.paint_list(|l| l.add(self.clip_rect, Shape::Noop))
239 } else {
240 let mut shape = shape.into();
241 self.transform_shape(&mut shape);
242 self.paint_list(|l| l.add(self.clip_rect, shape))
243 }
244 }
245
246 pub fn extend<I: IntoIterator<Item = Shape>>(&self, shapes: I) {
250 if self.fade_to_color == Some(Color32::TRANSPARENT) || self.opacity_factor == 0.0 {
251 return;
252 }
253 if self.fade_to_color.is_some() || self.opacity_factor < 1.0 {
254 let shapes = shapes.into_iter().map(|mut shape| {
255 self.transform_shape(&mut shape);
256 shape
257 });
258 self.paint_list(|l| l.extend(self.clip_rect, shapes));
259 } else {
260 self.paint_list(|l| l.extend(self.clip_rect, shapes));
261 }
262 }
263
264 pub fn set(&self, idx: ShapeIdx, shape: impl Into<Shape>) {
266 if self.fade_to_color == Some(Color32::TRANSPARENT) {
267 return;
268 }
269 let mut shape = shape.into();
270 self.transform_shape(&mut shape);
271 self.paint_list(|l| l.set(idx, self.clip_rect, shape));
272 }
273
274 pub fn for_each_shape(&self, mut reader: impl FnMut(&ClippedShape)) {
276 self.ctx.graphics(|g| {
277 if let Some(list) = g.get(self.layer_id) {
278 for c in list.all_entries() {
279 reader(c);
280 }
281 }
282 });
283 }
284}
285
286impl Painter {
288 #[allow(clippy::needless_pass_by_value)]
289 pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
290 self.rect(
291 rect,
292 0.0,
293 color.additive().linear_multiply(0.015),
294 (1.0, color),
295 );
296 self.text(
297 rect.min,
298 Align2::LEFT_TOP,
299 text.to_string(),
300 FontId::monospace(12.0),
301 color,
302 );
303 }
304
305 pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
306 let color = self.ctx.style().visuals.error_fg_color;
307 self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}"))
308 }
309
310 #[allow(clippy::needless_pass_by_value)]
314 pub fn debug_text(
315 &self,
316 pos: Pos2,
317 anchor: Align2,
318 color: Color32,
319 text: impl ToString,
320 ) -> Rect {
321 let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(12.0), color);
322 let rect = anchor.anchor_size(pos, galley.size());
323 let frame_rect = rect.expand(2.0);
324
325 let is_text_bright = color.is_additive() || epaint::Rgba::from(color).intensity() > 0.5;
326 let bg_color = if is_text_bright {
327 Color32::from_black_alpha(150)
328 } else {
329 Color32::from_white_alpha(150)
330 };
331 self.add(Shape::rect_filled(frame_rect, 0.0, bg_color));
332 self.galley(rect.min, galley, color);
333 frame_rect
334 }
335}
336
337impl Painter {
339 pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<PathStroke>) -> ShapeIdx {
341 self.add(Shape::LineSegment {
342 points,
343 stroke: stroke.into(),
344 })
345 }
346
347 pub fn line(&self, points: Vec<Pos2>, stroke: impl Into<PathStroke>) -> ShapeIdx {
350 self.add(Shape::line(points, stroke))
351 }
352
353 pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<PathStroke>) -> ShapeIdx {
355 self.add(Shape::hline(x, y, stroke.into()))
356 }
357
358 pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<PathStroke>) -> ShapeIdx {
360 self.add(Shape::vline(x, y, stroke.into()))
361 }
362
363 pub fn circle(
364 &self,
365 center: Pos2,
366 radius: f32,
367 fill_color: impl Into<Color32>,
368 stroke: impl Into<Stroke>,
369 ) -> ShapeIdx {
370 self.add(CircleShape {
371 center,
372 radius,
373 fill: fill_color.into(),
374 stroke: stroke.into(),
375 })
376 }
377
378 pub fn circle_filled(
379 &self,
380 center: Pos2,
381 radius: f32,
382 fill_color: impl Into<Color32>,
383 ) -> ShapeIdx {
384 self.add(CircleShape {
385 center,
386 radius,
387 fill: fill_color.into(),
388 stroke: Default::default(),
389 })
390 }
391
392 pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
393 self.add(CircleShape {
394 center,
395 radius,
396 fill: Default::default(),
397 stroke: stroke.into(),
398 })
399 }
400
401 pub fn rect(
402 &self,
403 rect: Rect,
404 rounding: impl Into<Rounding>,
405 fill_color: impl Into<Color32>,
406 stroke: impl Into<Stroke>,
407 ) -> ShapeIdx {
408 self.add(RectShape::new(rect, rounding, fill_color, stroke))
409 }
410
411 pub fn rect_filled(
412 &self,
413 rect: Rect,
414 rounding: impl Into<Rounding>,
415 fill_color: impl Into<Color32>,
416 ) -> ShapeIdx {
417 self.add(RectShape::filled(rect, rounding, fill_color))
418 }
419
420 pub fn rect_stroke(
421 &self,
422 rect: Rect,
423 rounding: impl Into<Rounding>,
424 stroke: impl Into<Stroke>,
425 ) -> ShapeIdx {
426 self.add(RectShape::stroke(rect, rounding, stroke))
427 }
428
429 pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: impl Into<Stroke>) {
431 use crate::emath::Rot2;
432 let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
433 let tip_length = vec.length() / 4.0;
434 let tip = origin + vec;
435 let dir = vec.normalized();
436 let stroke = stroke.into();
437 self.line_segment([origin, tip], stroke);
438 self.line_segment([tip, tip - tip_length * (rot * dir)], stroke);
439 self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke);
440 }
441
442 pub fn image(
461 &self,
462 texture_id: epaint::TextureId,
463 rect: Rect,
464 uv: Rect,
465 tint: Color32,
466 ) -> ShapeIdx {
467 self.add(Shape::image(texture_id, rect, uv, tint))
468 }
469}
470
471impl Painter {
473 #[allow(clippy::needless_pass_by_value)]
482 pub fn text(
483 &self,
484 pos: Pos2,
485 anchor: Align2,
486 text: impl ToString,
487 font_id: FontId,
488 text_color: Color32,
489 ) -> Rect {
490 let galley = self.layout_no_wrap(text.to_string(), font_id, text_color);
491 let rect = anchor.anchor_size(pos, galley.size());
492 self.galley(rect.min, galley, text_color);
493 rect
494 }
495
496 #[inline]
500 #[must_use]
501 pub fn layout(
502 &self,
503 text: String,
504 font_id: FontId,
505 color: crate::Color32,
506 wrap_width: f32,
507 ) -> Arc<Galley> {
508 self.fonts(|f| f.layout(text, font_id, color, wrap_width))
509 }
510
511 #[inline]
515 #[must_use]
516 pub fn layout_no_wrap(
517 &self,
518 text: String,
519 font_id: FontId,
520 color: crate::Color32,
521 ) -> Arc<Galley> {
522 self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
523 }
524
525 #[inline]
529 #[must_use]
530 pub fn layout_job(&self, layout_job: LayoutJob) -> Arc<Galley> {
531 self.fonts(|f| f.layout_job(layout_job))
532 }
533
534 #[inline]
542 pub fn galley(&self, pos: Pos2, galley: Arc<Galley>, fallback_color: Color32) {
543 if !galley.is_empty() {
544 self.add(Shape::galley(pos, galley, fallback_color));
545 }
546 }
547
548 #[inline]
554 pub fn galley_with_override_text_color(
555 &self,
556 pos: Pos2,
557 galley: Arc<Galley>,
558 text_color: Color32,
559 ) {
560 if !galley.is_empty() {
561 self.add(Shape::galley_with_override_text_color(
562 pos, galley, text_color,
563 ));
564 }
565 }
566
567 #[deprecated = "Use `Painter::galley` or `Painter::galley_with_override_text_color` instead"]
568 #[inline]
569 pub fn galley_with_color(&self, pos: Pos2, galley: Arc<Galley>, text_color: Color32) {
570 if !galley.is_empty() {
571 self.add(Shape::galley_with_override_text_color(
572 pos, galley, text_color,
573 ));
574 }
575 }
576}
577
578fn tint_shape_towards(shape: &mut Shape, target: Color32) {
579 epaint::shape_transform::adjust_colors(shape, move |color| {
580 if *color != Color32::PLACEHOLDER {
581 *color = crate::ecolor::tint_color_towards(*color, target);
582 }
583 });
584}
585
586fn multiply_opacity(shape: &mut Shape, opacity: f32) {
587 epaint::shape_transform::adjust_colors(shape, move |color| {
588 if *color != Color32::PLACEHOLDER {
589 *color = color.gamma_multiply(opacity);
590 }
591 });
592}