use std::ops::RangeInclusive;
use std::sync::Arc;
use crate::{
emath::{Align2, Pos2, Rect, Vec2},
layers::{LayerId, PaintList, ShapeIdx},
Color32, Context, FontId,
};
use epaint::{
mutex::{RwLockReadGuard, RwLockWriteGuard},
text::{Fonts, Galley},
CircleShape, RectShape, Rounding, Shape, Stroke,
};
#[derive(Clone)]
pub struct Painter {
ctx: Context,
layer_id: LayerId,
clip_rect: Rect,
fade_to_color: Option<Color32>,
}
impl Painter {
pub fn new(ctx: Context, layer_id: LayerId, clip_rect: Rect) -> Self {
Self {
ctx,
layer_id,
clip_rect,
fade_to_color: None,
}
}
#[must_use]
pub fn with_layer_id(self, layer_id: LayerId) -> Self {
Self {
ctx: self.ctx,
layer_id,
clip_rect: self.clip_rect,
fade_to_color: None,
}
}
pub fn with_clip_rect(&self, rect: Rect) -> Self {
Self {
ctx: self.ctx.clone(),
layer_id: self.layer_id,
clip_rect: rect.intersect(self.clip_rect),
fade_to_color: self.fade_to_color,
}
}
pub fn set_layer_id(&mut self, layer_id: LayerId) {
self.layer_id = layer_id;
}
pub(crate) fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
self.fade_to_color = fade_to_color;
}
pub(crate) fn is_visible(&self) -> bool {
self.fade_to_color != Some(Color32::TRANSPARENT)
}
pub(crate) fn set_invisible(&mut self) {
self.fade_to_color = Some(Color32::TRANSPARENT);
}
#[deprecated = "Use Painter::with_clip_rect"] pub fn sub_region(&self, rect: Rect) -> Self {
Self {
ctx: self.ctx.clone(),
layer_id: self.layer_id,
clip_rect: rect.intersect(self.clip_rect),
fade_to_color: self.fade_to_color,
}
}
}
impl Painter {
#[inline(always)]
pub fn ctx(&self) -> &Context {
&self.ctx
}
#[inline(always)]
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
self.ctx.fonts()
}
#[inline(always)]
pub fn layer_id(&self) -> LayerId {
self.layer_id
}
#[inline(always)]
pub fn clip_rect(&self) -> Rect {
self.clip_rect
}
#[inline(always)]
pub fn set_clip_rect(&mut self, clip_rect: Rect) {
self.clip_rect = clip_rect;
}
#[inline(always)]
pub fn round_to_pixel(&self, point: f32) -> f32 {
self.ctx().round_to_pixel(point)
}
#[inline(always)]
pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
self.ctx().round_vec_to_pixels(vec)
}
#[inline(always)]
pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
self.ctx().round_pos_to_pixels(pos)
}
}
impl Painter {
fn paint_list(&self) -> RwLockWriteGuard<'_, PaintList> {
RwLockWriteGuard::map(self.ctx.graphics(), |g| g.list(self.layer_id))
}
fn transform_shape(&self, shape: &mut Shape) {
if let Some(fade_to_color) = self.fade_to_color {
tint_shape_towards(shape, fade_to_color);
}
}
pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
if self.fade_to_color == Some(Color32::TRANSPARENT) {
self.paint_list().add(self.clip_rect, Shape::Noop)
} else {
let mut shape = shape.into();
self.transform_shape(&mut shape);
self.paint_list().add(self.clip_rect, shape)
}
}
pub fn extend<I: IntoIterator<Item = Shape>>(&self, shapes: I) {
if self.fade_to_color == Some(Color32::TRANSPARENT) {
return;
}
if self.fade_to_color.is_some() {
let shapes = shapes.into_iter().map(|mut shape| {
self.transform_shape(&mut shape);
shape
});
self.paint_list().extend(self.clip_rect, shapes);
} else {
self.paint_list().extend(self.clip_rect, shapes);
};
}
pub fn set(&self, idx: ShapeIdx, shape: impl Into<Shape>) {
if self.fade_to_color == Some(Color32::TRANSPARENT) {
return;
}
let mut shape = shape.into();
self.transform_shape(&mut shape);
self.paint_list().set(idx, self.clip_rect, shape);
}
}
impl Painter {
#[allow(clippy::needless_pass_by_value)]
pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
self.rect(
rect,
0.0,
color.additive().linear_multiply(0.015),
(1.0, color),
);
self.text(
rect.min,
Align2::LEFT_TOP,
text.to_string(),
FontId::monospace(12.0),
color,
);
}
pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
let color = self.ctx.style().visuals.error_fg_color;
self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {}", text))
}
#[allow(clippy::needless_pass_by_value)]
pub fn debug_text(
&self,
pos: Pos2,
anchor: Align2,
color: Color32,
text: impl ToString,
) -> Rect {
let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(12.0), color);
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
let frame_rect = rect.expand(2.0);
self.add(Shape::rect_filled(
frame_rect,
0.0,
Color32::from_black_alpha(150),
));
self.galley(rect.min, galley);
frame_rect
}
}
impl Painter {
pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) {
self.add(Shape::LineSegment {
points,
stroke: stroke.into(),
});
}
pub fn hline(&self, x: RangeInclusive<f32>, y: f32, stroke: impl Into<Stroke>) {
self.add(Shape::hline(x, y, stroke));
}
pub fn vline(&self, x: f32, y: RangeInclusive<f32>, stroke: impl Into<Stroke>) {
self.add(Shape::vline(x, y, stroke));
}
pub fn circle(
&self,
center: Pos2,
radius: f32,
fill_color: impl Into<Color32>,
stroke: impl Into<Stroke>,
) {
self.add(CircleShape {
center,
radius,
fill: fill_color.into(),
stroke: stroke.into(),
});
}
pub fn circle_filled(&self, center: Pos2, radius: f32, fill_color: impl Into<Color32>) {
self.add(CircleShape {
center,
radius,
fill: fill_color.into(),
stroke: Default::default(),
});
}
pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into<Stroke>) {
self.add(CircleShape {
center,
radius,
fill: Default::default(),
stroke: stroke.into(),
});
}
pub fn rect(
&self,
rect: Rect,
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
stroke: impl Into<Stroke>,
) {
self.add(RectShape {
rect,
rounding: rounding.into(),
fill: fill_color.into(),
stroke: stroke.into(),
});
}
pub fn rect_filled(
&self,
rect: Rect,
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
) {
self.add(RectShape {
rect,
rounding: rounding.into(),
fill: fill_color.into(),
stroke: Default::default(),
});
}
pub fn rect_stroke(
&self,
rect: Rect,
rounding: impl Into<Rounding>,
stroke: impl Into<Stroke>,
) {
self.add(RectShape {
rect,
rounding: rounding.into(),
fill: Default::default(),
stroke: stroke.into(),
});
}
pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: Stroke) {
use crate::emath::*;
let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
let tip_length = vec.length() / 4.0;
let tip = origin + vec;
let dir = vec.normalized();
self.line_segment([origin, tip], stroke);
self.line_segment([tip, tip - tip_length * (rot * dir)], stroke);
self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke);
}
pub fn image(&self, texture_id: epaint::TextureId, rect: Rect, uv: Rect, tint: Color32) {
self.add(Shape::image(texture_id, rect, uv, tint));
}
}
impl Painter {
#[allow(clippy::needless_pass_by_value)]
pub fn text(
&self,
pos: Pos2,
anchor: Align2,
text: impl ToString,
font_id: FontId,
text_color: Color32,
) -> Rect {
let galley = self.layout_no_wrap(text.to_string(), font_id, text_color);
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
self.galley(rect.min, galley);
rect
}
#[inline(always)]
pub fn layout(
&self,
text: String,
font_id: FontId,
color: crate::Color32,
wrap_width: f32,
) -> Arc<Galley> {
self.fonts().layout(text, font_id, color, wrap_width)
}
#[inline(always)]
pub fn layout_no_wrap(
&self,
text: String,
font_id: FontId,
color: crate::Color32,
) -> Arc<Galley> {
self.fonts().layout(text, font_id, color, f32::INFINITY)
}
#[inline(always)]
pub fn galley(&self, pos: Pos2, galley: Arc<Galley>) {
if !galley.is_empty() {
self.add(Shape::galley(pos, galley));
}
}
#[inline(always)]
pub fn galley_with_color(&self, pos: Pos2, galley: Arc<Galley>, text_color: Color32) {
if !galley.is_empty() {
self.add(Shape::galley_with_color(pos, galley, text_color));
}
}
}
fn tint_shape_towards(shape: &mut Shape, target: Color32) {
epaint::shape_transform::adjust_colors(shape, &|color| {
*color = crate::ecolor::tint_color_towards(*color, target);
});
}