From 11df21e39ee29b05d631523610e4f76216c0bd43 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 30 Jul 2020 14:11:09 +0200 Subject: [PATCH] [refactor] extract paint code from Ui/Context to new struct Painter --- egui/src/containers/collapsing_header.rs | 13 +- egui/src/containers/frame.rs | 8 +- egui/src/containers/resize.rs | 14 +- egui/src/containers/scroll_area.rs | 4 +- egui/src/containers/window.rs | 8 +- egui/src/context.rs | 119 +++------------ egui/src/demos/app.rs | 7 +- egui/src/demos/fractal_clock.rs | 24 +-- egui/src/introspection.rs | 4 +- egui/src/layers.rs | 33 +++- egui/src/lib.rs | 2 + egui/src/paint/command.rs | 2 + egui/src/paint/tessellator.rs | 1 + egui/src/painter.rs | 165 ++++++++++++++++++++ egui/src/ui.rs | 186 ++++++----------------- egui/src/widgets.rs | 33 ++-- egui/src/widgets/slider.rs | 10 +- egui/src/widgets/text_edit.rs | 9 +- 18 files changed, 333 insertions(+), 309 deletions(-) create mode 100644 egui/src/painter.rs diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index 3b56bc8d..7ce666e8 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -87,7 +87,7 @@ impl State { *p = rect.center() + v; } - ui.add_paint_cmd(PaintCmd::Path { + ui.painter().add(PaintCmd::Path { path: Path::from_point_loop(&points), closed: true, fill: None, @@ -219,7 +219,7 @@ impl CollapsingHeader { state.toggle(ui); } - let where_to_put_background = ui.paint_list_len(); + let bg_index = ui.painter().add(PaintCmd::Noop); { let (mut icon_rect, _) = ui.style().icon_rectangles(interact.rect); @@ -234,15 +234,16 @@ impl CollapsingHeader { state.paint_icon(ui, &icon_interact); } - ui.add_galley( + let painter = ui.painter(); + painter.add_galley( text_pos, galley, label.text_style, - Some(ui.style().interact(&interact).stroke_color), + ui.style().interact(&interact).stroke_color, ); - ui.insert_paint_cmd( - where_to_put_background, + painter.set( + bg_index, PaintCmd::Rect { corner_radius: ui.style().interact(&interact).corner_radius, fill: ui.style().interact(&interact).bg_fill, diff --git a/egui/src/containers/frame.rs b/egui/src/containers/frame.rs index 5ca593b9..604844e9 100644 --- a/egui/src/containers/frame.rs +++ b/egui/src/containers/frame.rs @@ -1,6 +1,6 @@ //! Frame container -use crate::{paint::*, *}; +use crate::{layers::PaintCmdIdx, paint::*, *}; #[derive(Clone, Debug, Default)] pub struct Frame { @@ -62,7 +62,7 @@ impl Frame { pub struct Prepared { pub frame: Frame, outer_rect_bounds: Rect, - where_to_put_background: usize, + where_to_put_background: PaintCmdIdx, pub content_ui: Ui, } @@ -70,7 +70,7 @@ impl Frame { pub fn begin(self, ui: &mut Ui) -> Prepared { let outer_rect_bounds = ui.available(); let inner_rect = outer_rect_bounds.shrink2(self.margin); - let where_to_put_background = ui.paint_list_len(); + let where_to_put_background = ui.painter().add(PaintCmd::Noop); let content_ui = ui.child_ui(inner_rect); Prepared { frame: self, @@ -105,7 +105,7 @@ impl Prepared { .. } = self; - ui.insert_paint_cmd( + ui.painter().set( where_to_put_background, PaintCmd::Rect { corner_radius: frame.corner_radius, diff --git a/egui/src/containers/resize.rs b/egui/src/containers/resize.rs index 568cc792..a70fd155 100644 --- a/egui/src/containers/resize.rs +++ b/egui/src/containers/resize.rs @@ -226,7 +226,7 @@ impl Resize { // so we must follow the contents: state.desired_size = state.desired_size.max(state.last_content_size); - state.desired_size = ui.round_vec_to_pixels(state.desired_size); + state.desired_size = ui.painter().round_vec_to_pixels(state.desired_size); // We are as large as we look ui.allocate_space(state.desired_size); @@ -240,7 +240,7 @@ impl Resize { if self.outline && corner_interact.is_some() { let rect = Rect::from_min_size(content_ui.top_left(), state.desired_size); let rect = rect.expand(2.0); // breathing room for content - ui.add_paint_cmd(paint::PaintCmd::Rect { + ui.painter().add(paint::PaintCmd::Rect { rect, corner_radius: 3.0, fill: None, @@ -259,12 +259,12 @@ impl Resize { ui.memory().resize.insert(id, state); if ui.ctx().style().debug_resize { - ui.ctx().debug_rect( + ui.ctx().debug_painter().debug_rect( Rect::from_min_size(content_ui.top_left(), state.desired_size), color::GREEN, "desired_size", ); - ui.ctx().debug_rect( + ui.ctx().debug_painter().debug_rect( Rect::from_min_size(content_ui.top_left(), state.last_content_size), color::LIGHT_BLUE, "last_content_size", @@ -277,11 +277,13 @@ fn paint_resize_corner(ui: &mut Ui, interact: &InteractInfo) { let color = ui.style().interact(interact).stroke_color; let width = ui.style().interact(interact).stroke_width; - let corner = ui.round_pos_to_pixels(interact.rect.right_bottom()); + let painter = ui.painter(); + + let corner = painter.round_pos_to_pixels(interact.rect.right_bottom()); let mut w = 2.0; while w < 12.0 { - ui.add_paint_cmd(paint::PaintCmd::line_segment( + painter.add(paint::PaintCmd::line_segment( [pos2(corner.x - w, corner.y), pos2(corner.x, corner.y - w)], color, width, diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index efbb9f4c..62dda85a 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -268,14 +268,14 @@ impl Prepared { let handle_fill = style.interact(&interact).fill; let handle_outline = style.interact(&interact).rect_outline; - ui.add_paint_cmd(paint::PaintCmd::Rect { + ui.painter().add(paint::PaintCmd::Rect { rect: outer_scroll_rect, corner_radius, fill: Some(ui.style().dark_bg_color), outline: None, }); - ui.add_paint_cmd(paint::PaintCmd::Rect { + ui.painter().add(paint::PaintCmd::Rect { rect: handle_rect.expand(-2.0), corner_radius, fill: Some(handle_fill), diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index dcf3d97c..b76035f1 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -511,7 +511,7 @@ fn paint_frame_interaction( path.add_circle_quadrant(pos2(max.x - cr, min.y + cr), cr, 3.0); path.add_line_segment([pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]); } - ui.add_paint_cmd(PaintCmd::Path { + ui.painter().add(PaintCmd::Path { path, closed: false, fill: None, @@ -614,7 +614,7 @@ impl TitleBar { let left = outer_rect.left(); let right = outer_rect.right(); let y = content_rect.top() + ui.style().item_spacing.y * 0.5; - ui.add_paint_cmd(PaintCmd::LineSegment { + ui.painter().add(PaintCmd::LineSegment { points: [pos2(left, y), pos2(right, y)], style: ui.style().interact.inactive.rect_outline.unwrap(), }); @@ -650,12 +650,12 @@ fn close_button(ui: &mut Ui, rect: Rect) -> InteractInfo { let stroke_color = ui.style().interact(&interact).stroke_color; let stroke_width = ui.style().interact(&interact).stroke_width; - ui.add_paint_cmd(PaintCmd::line_segment( + ui.painter().add(PaintCmd::line_segment( [rect.left_top(), rect.right_bottom()], stroke_color, stroke_width, )); - ui.add_paint_cmd(PaintCmd::line_segment( + ui.painter().add(PaintCmd::line_segment( [rect.right_top(), rect.left_bottom()], stroke_color, stroke_width, diff --git a/egui/src/context.rs b/egui/src/context.rs index 33af0013..c38533ea 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use {ahash::AHashMap, parking_lot::Mutex}; -use crate::{layout::align_rect, paint::*, *}; +use crate::{paint::*, *}; #[derive(Clone, Copy, Default)] struct PaintStats { @@ -234,7 +234,7 @@ impl Context { /// Generate a id from the given source. /// If it is not unique, an error will be printed at the given position. - pub fn make_unique_id(&self, source: IdSource, pos: Pos2) -> Id + pub fn make_unique_id(self: &Arc, source: IdSource, pos: Pos2) -> Id where IdSource: std::hash::Hash + std::fmt::Debug + Copy, { @@ -246,19 +246,25 @@ impl Context { } /// If the given Id is not unique, an error will be printed at the given position. - pub fn register_unique_id(&self, id: Id, source_name: impl std::fmt::Debug, pos: Pos2) -> Id { + pub fn register_unique_id( + self: &Arc, + id: Id, + source_name: impl std::fmt::Debug, + pos: Pos2, + ) -> Id { if let Some(clash_pos) = self.used_ids.lock().insert(id, pos) { + let painter = self.debug_painter(); if clash_pos.distance(pos) < 4.0 { - self.show_error( + painter.error( pos, &format!("use of non-unique ID {:?} (name clash?)", source_name), ); } else { - self.show_error( + painter.error( clash_pos, &format!("first use of non-unique ID {:?} (name clash?)", source_name), ); - self.show_error( + painter.error( pos, &format!( "second use of non-unique ID {:?} (name clash?)", @@ -422,103 +428,12 @@ impl Context { } } } +} - // --------------------------------------------------------------------- - - pub fn show_error(&self, pos: Pos2, text: impl Into) { - let text = text.into(); - let align = (Align::Min, Align::Min); - let layer = Layer::debug(); - let text_style = TextStyle::Monospace; - let font = &self.fonts()[text_style]; - let galley = font.layout_multiline(text, f32::INFINITY); - let rect = align_rect(Rect::from_min_size(pos, galley.size), align); - self.add_paint_cmd( - layer, - PaintCmd::Rect { - corner_radius: 0.0, - fill: Some(color::gray(0, 240)), - outline: Some(LineStyle::new(1.0, color::RED)), - rect: rect.expand(2.0), - }, - ); - self.add_galley(layer, rect.min, galley, text_style, Some(color::RED)); - } - - pub fn debug_text(&self, pos: Pos2, text: impl Into) { - let text = text.into(); - let layer = Layer::debug(); - let align = (Align::Min, Align::Min); - self.floating_text( - layer, - pos, - text, - TextStyle::Monospace, - align, - Some(color::YELLOW), - ); - } - - pub fn debug_rect(&self, rect: Rect, color: Color, name: impl Into) { - let text = format!("{} {:?}", name.into(), rect); - let layer = Layer::debug(); - self.add_paint_cmd( - layer, - PaintCmd::Rect { - corner_radius: 0.0, - fill: None, - outline: Some(LineStyle::new(2.0, color)), - rect, - }, - ); - let align = (Align::Min, Align::Min); - let text_style = TextStyle::Monospace; - self.floating_text(layer, rect.min, text, text_style, align, Some(color)); - } - - /// Show some text anywhere on screen. - /// To center the text at the given position, use `align: (Center, Center)`. - pub fn floating_text( - &self, - layer: Layer, - pos: Pos2, - text: String, - text_style: TextStyle, - align: (Align, Align), - text_color: Option, - ) -> Rect { - let font = &self.fonts()[text_style]; - let galley = font.layout_multiline(text, f32::INFINITY); - let rect = align_rect(Rect::from_min_size(pos, galley.size), align); - self.add_galley(layer, rect.min, galley, text_style, text_color); - rect - } - - /// Already layed out text. - pub fn add_galley( - &self, - layer: Layer, - pos: Pos2, - galley: font::Galley, - text_style: TextStyle, - color: Option, - ) { - let color = color.unwrap_or_else(|| self.style().text_color); - self.add_paint_cmd( - layer, - PaintCmd::Text { - pos, - galley, - text_style, - color, - }, - ); - } - - pub fn add_paint_cmd(&self, layer: Layer, paint_cmd: PaintCmd) { - self.graphics() - .layer(layer) - .push((Rect::everything(), paint_cmd)) +/// ## Painting +impl Context { + pub fn debug_painter(self: &Arc) -> Painter { + Painter::new(self.clone(), Layer::debug(), self.rect()) } } diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index 22fcd83f..3ed52730 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -469,7 +469,7 @@ impl BoxPainting { outline: Some(LineStyle::new(self.stroke_width, gray(255, 255))), }); } - ui.add_paint_cmds(cmds); + ui.painter().extend(cmds); } } @@ -498,7 +498,8 @@ impl Painting { let rect = ui.allocate_space(ui.available_finite().size()); let interact = ui.interact(rect, ui.id(), Sense::drag()); let rect = interact.rect; - ui.set_clip_rect(ui.clip_rect().intersect(rect)); // Make sure we don't paint out of bounds + let clip_rect = ui.clip_rect().intersect(rect); // Make sure we don't paint out of bounds + let painter = Painter::new(ui.ctx().clone(), ui.layer(), clip_rect); if self.lines.is_empty() { self.lines.push(vec![]); @@ -520,7 +521,7 @@ impl Painting { for line in &self.lines { if line.len() >= 2 { let points: Vec = line.iter().map(|p| rect.min + *p).collect(); - ui.add_paint_cmd(PaintCmd::Path { + painter.add(PaintCmd::Path { path: Path::from_open_points(&points), closed: false, outline: Some(LineStyle::new(2.0, LIGHT_GRAY)), diff --git a/egui/src/demos/fractal_clock.rs b/egui/src/demos/fractal_clock.rs index fad845ed..a88a1cae 100644 --- a/egui/src/demos/fractal_clock.rs +++ b/egui/src/demos/fractal_clock.rs @@ -50,18 +50,18 @@ impl FractalClock { ui.ctx().request_repaint(); } - self.fractal_ui(ui, ui.available_finite()); + let painter = Painter::new(ui.ctx().clone(), ui.layer(), ui.available_finite()); + self.fractal_ui(&painter); - let frame = Frame::popup(ui.style()) + Frame::popup(ui.style()) .fill(Some(color::gray(34, 160))) - .outline(None); - - frame.show(&mut ui.left_column(320.0), |ui| { - CollapsingHeader::new("Settings").show(ui, |ui| self.options_ui(ui)); - }); + .outline(None) + .show(&mut ui.left_column(320.0), |ui| { + CollapsingHeader::new("Settings").show(ui, |ui| self.options_ui(ui)); + }); // Make sure we allocate what we used (everything) - ui.allocate_space(ui.available_finite().size()); + ui.allocate_space(painter.clip_rect().size()); } fn options_ui(&mut self, ui: &mut Ui) { @@ -96,7 +96,9 @@ impl FractalClock { ); } - fn fractal_ui(&mut self, ui: &mut Ui, rect: Rect) { + fn fractal_ui(&mut self, painter: &Painter) { + let rect = painter.clip_rect(); + struct Hand { length: f32, angle: f32, @@ -126,13 +128,13 @@ impl FractalClock { ]; let scale = self.zoom * rect.width().min(rect.height()); - let mut paint_line = |points: [Pos2; 2], color: Color, width: f32| { + let paint_line = |points: [Pos2; 2], color: Color, width: f32| { let line = [ rect.center() + scale * points[0].to_vec2(), rect.center() + scale * points[1].to_vec2(), ]; - ui.add_paint_cmd(PaintCmd::line_segment([line[0], line[1]], color, width)); + painter.add(PaintCmd::line_segment([line[0], line[1]], color, width)); }; let hand_rotations = [ diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 5641a372..467292c8 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -30,7 +30,7 @@ impl Texture { }; let mut triangles = Triangles::default(); triangles.add_rect(top_left, bottom_right); - ui.add_paint_cmd(PaintCmd::Triangles(triangles)); + ui.painter().add(PaintCmd::Triangles(triangles)); if ui.hovered(rect) { show_tooltip(ui.ctx(), |ui| { @@ -55,7 +55,7 @@ impl Texture { }; let mut triangles = Triangles::default(); triangles.add_rect(top_left, bottom_right); - ui.add_paint_cmd(PaintCmd::Triangles(triangles)); + ui.painter().add(PaintCmd::Triangles(triangles)); }); } } diff --git a/egui/src/layers.rs b/egui/src/layers.rs index 09de19d9..f31ec72b 100644 --- a/egui/src/layers.rs +++ b/egui/src/layers.rs @@ -16,7 +16,7 @@ pub enum Order { Debug, } -/// An ideintifer for a paint layer. +/// An identifier for a paint layer. /// Also acts as an identifier for `Area`:s. #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] #[cfg_attr(feature = "with_serde", derive(serde::Deserialize, serde::Serialize))] @@ -34,15 +34,38 @@ impl Layer { } } +#[derive(Clone, Copy, PartialEq)] +pub struct PaintCmdIdx(usize); + /// Each `PaintCmd` is paired with a clip rectangle. -type PaintList = Vec<(Rect, PaintCmd)>; +#[derive(Clone, Default)] +pub struct PaintList(Vec<(Rect, PaintCmd)>); + +impl PaintList { + /// Returns the index of the new command that can be used with `PaintList::set`. + pub fn add(&mut self, clip_rect: Rect, cmd: PaintCmd) -> PaintCmdIdx { + let idx = PaintCmdIdx(self.0.len()); + self.0.push((clip_rect, cmd)); + idx + } + + pub fn extend(&mut self, clip_rect: Rect, mut cmds: Vec) { + self.0.extend(cmds.drain(..).map(|cmd| (clip_rect, cmd))) + } + + /// Modify an existing command. + pub fn set(&mut self, idx: PaintCmdIdx, clip_rect: Rect, cmd: PaintCmd) { + assert!(idx.0 < self.0.len()); + self.0[idx.0] = (clip_rect, cmd); + } +} // TODO: improve this #[derive(Clone, Default)] pub struct GraphicLayers(AHashMap); impl GraphicLayers { - pub fn layer(&mut self, layer: Layer) -> &mut PaintList { + pub fn list(&mut self, layer: Layer) -> &mut PaintList { self.0.entry(layer).or_default() } @@ -54,12 +77,12 @@ impl GraphicLayers { for layer in area_order { if let Some(commands) = self.0.get_mut(layer) { - all_commands.extend(commands.drain(..)); + all_commands.extend(commands.0.drain(..)); } } if let Some(commands) = self.0.get_mut(&Layer::debug()) { - all_commands.extend(commands.drain(..)); + all_commands.extend(commands.0.drain(..)); } all_commands.into_iter() diff --git a/egui/src/lib.rs b/egui/src/lib.rs index e3c1af9d..fba6d0db 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -35,6 +35,7 @@ pub mod math; mod memory; mod movement_tracker; pub mod paint; +mod painter; mod style; mod types; mod ui; @@ -52,6 +53,7 @@ pub use { memory::Memory, movement_tracker::MovementTracker, paint::{color, Color, PaintJobs, TextStyle, Texture}, + painter::Painter, style::Style, types::*, ui::Ui, diff --git a/egui/src/paint/command.rs b/egui/src/paint/command.rs index 9a5d5490..9186c95c 100644 --- a/egui/src/paint/command.rs +++ b/egui/src/paint/command.rs @@ -6,6 +6,8 @@ use { // TODO: rename, e.g. `paint::Cmd`? #[derive(Clone, Debug)] pub enum PaintCmd { + /// Paint nothing. This can be useful as a placeholder. + Noop, Circle { center: Pos2, fill: Option, diff --git a/egui/src/paint/tessellator.rs b/egui/src/paint/tessellator.rs index d2248ffd..1eb90d61 100644 --- a/egui/src/paint/tessellator.rs +++ b/egui/src/paint/tessellator.rs @@ -564,6 +564,7 @@ pub fn tessellate_paint_command( path.clear(); match command { + PaintCmd::Noop => {} PaintCmd::Circle { center, fill, diff --git a/egui/src/painter.rs b/egui/src/painter.rs new file mode 100644 index 00000000..33b5aafe --- /dev/null +++ b/egui/src/painter.rs @@ -0,0 +1,165 @@ +use std::sync::Arc; + +use crate::{ + align_rect, color, + layers::PaintCmdIdx, + math::{Pos2, Rect, Vec2}, + paint::{font, Fonts, LineStyle, PaintCmd, TextStyle}, + Align, Color, Context, Layer, +}; + +/// Helper to paint shapes and text to a specific region on a specific layer. +#[derive(Clone)] +pub struct Painter { + /// Source of fonts and destination of paint commands + ctx: Arc, + + /// Where we paint + layer: Layer, + + /// Everything painted in this `Painter` will be clipped against this. + /// This means nothing outside of this rectangle will be visible on screen. + clip_rect: Rect, +} + +impl Painter { + pub fn new(ctx: Arc, layer: Layer, clip_rect: Rect) -> Self { + Self { + ctx, + layer, + clip_rect, + } + } +} + +/// ## Accessors etc +impl Painter { + pub fn ctx(&self) -> &Arc { + &self.ctx + } + + /// Available fonts + pub fn fonts(&self) -> &Fonts { + self.ctx.fonts() + } + + /// Where we paint + pub fn layer(&self) -> Layer { + self.layer + } + + /// Everything painted in this `Painter` will be clipped against this. + /// This means nothing outside of this rectangle will be visible on screen. + pub fn clip_rect(&self) -> Rect { + self.clip_rect + } + + /// Everything painted in this `Painter` will be clipped against this. + /// This means nothing outside of this rectangle will be visible on screen. + pub fn set_clip_rect(&mut self, clip_rect: Rect) { + self.clip_rect = clip_rect; + } + + pub fn round_to_pixel(&self, point: f32) -> f32 { + self.ctx().round_to_pixel(point) + } + + pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 { + self.ctx().round_vec_to_pixels(vec) + } + + pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 { + self.ctx().round_pos_to_pixels(pos) + } +} + +/// ## Low level +impl Painter { + /// It is up to the caller to make sure there is room for this. + /// Can be used for free painting. + /// NOTE: all coordinates are screen coordinates! + pub fn add(&self, paint_cmd: PaintCmd) -> PaintCmdIdx { + self.ctx + .graphics() + .list(self.layer) + .add(self.clip_rect, paint_cmd) + } + + pub fn extend(&self, cmds: Vec) { + self.ctx + .graphics() + .list(self.layer) + .extend(self.clip_rect, cmds); + } + + /// Modify an existing command. + pub fn set(&self, idx: PaintCmdIdx, cmd: PaintCmd) { + self.ctx + .graphics() + .list(self.layer) + .set(idx, self.clip_rect, cmd) + } +} + +/// ## Debug painting +impl Painter { + pub fn debug_rect(&mut self, rect: Rect, color: Color, text: impl Into) { + self.add(PaintCmd::Rect { + corner_radius: 0.0, + fill: None, + outline: Some(LineStyle::new(1.0, color)), + rect, + }); + let align = (Align::Min, Align::Min); + let text_style = TextStyle::Monospace; + self.floating_text(rect.min, text.into(), text_style, align, color); + } + + pub fn error(&self, pos: Pos2, text: impl Into) { + let text = text.into(); + let align = (Align::Min, Align::Min); + let text_style = TextStyle::Monospace; + let font = &self.fonts()[text_style]; + let galley = font.layout_multiline(text, f32::INFINITY); + let rect = align_rect(Rect::from_min_size(pos, galley.size), align); + self.add(PaintCmd::Rect { + corner_radius: 0.0, + fill: Some(color::gray(0, 240)), + outline: Some(LineStyle::new(1.0, color::RED)), + rect: rect.expand(2.0), + }); + self.add_galley(rect.min, galley, text_style, color::RED); + } +} + +/// ## Text +impl Painter { + /// Show some text anywhere in the ui. + /// To center the text at the given position, use `align: (Center, Center)`. + /// If you want to draw text floating on top of everything, + /// consider using `Context.floating_text` instead. + pub fn floating_text( + &self, + pos: Pos2, + text: impl Into, + text_style: TextStyle, + align: (Align, Align), + text_color: Color, + ) -> Rect { + let font = &self.fonts()[text_style]; + let galley = font.layout_multiline(text.into(), f32::INFINITY); + let rect = align_rect(Rect::from_min_size(pos, galley.size), align); + self.add_galley(rect.min, galley, text_style, text_color); + rect + } + + /// Already layed out text. + pub fn add_galley(&self, pos: Pos2, galley: font::Galley, text_style: TextStyle, color: Color) { + self.add(PaintCmd::Text { + pos, + galley, + text_style, + color, + }); + } +} diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 5ee0cb8f..8f97d3c5 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -5,9 +5,6 @@ use crate::{color::*, containers::*, layout::*, paint::*, widgets::*, *}; /// Represents a region of the screen /// with a type of layout (horizontal or vertical). pub struct Ui { - /// How we access input, output and memory - ctx: Arc, - /// ID of this ui. /// Generated based on id of parent ui together with /// another source of child identity (e.g. window title). @@ -15,12 +12,7 @@ pub struct Ui { /// Hopefully unique. id: Id, - /// Where to put the graphics output of this Ui - layer: Layer, - - /// Everything painted in this ui will be clipped against this. - /// This means nothing outside of this rectangle will be visible on screen. - clip_rect: Rect, + painter: Painter, /// The `rect` represents where in screen-space the ui is /// and its max size (original available_space). @@ -62,11 +54,10 @@ impl Ui { pub fn new(ctx: Arc, layer: Layer, id: Id, rect: Rect) -> Self { let style = ctx.style(); + let clip_rect = rect.expand(style.clip_rect_margin); Ui { - ctx, id, - layer, - clip_rect: rect.expand(style.clip_rect_margin), + painter: Painter::new(ctx, layer, clip_rect), desired_rect: rect, child_bounds: Rect::from_min_size(rect.min, Vec2::zero()), // TODO: Rect::nothing() ? style, @@ -77,14 +68,11 @@ impl Ui { } pub fn child_ui(&mut self, child_rect: Rect) -> Self { - let clip_rect = self.clip_rect(); // Keep it unless the child excplicitly desires differently let id = self.make_position_id(); // TODO: is this a good idea? self.child_count += 1; Ui { - ctx: self.ctx.clone(), id, - layer: self.layer, - clip_rect, + painter: self.painter.clone(), desired_rect: child_rect, child_bounds: Rect::from_min_size(child_rect.min, Vec2::zero()), // TODO: Rect::nothing() ? style: self.style.clone(), @@ -96,18 +84,6 @@ impl Ui { // ------------------------------------------------- - pub fn round_to_pixel(&self, point: f32) -> f32 { - self.ctx.round_to_pixel(point) - } - - pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 { - self.ctx.round_vec_to_pixels(vec) - } - - pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 { - self.ctx.round_pos_to_pixels(pos) - } - pub fn id(&self) -> Id { self.id } @@ -122,34 +98,45 @@ impl Ui { } pub fn ctx(&self) -> &Arc { - &self.ctx + self.painter.ctx() + } + + /// Use this to paint stuff within this `Ui`. + pub fn painter(&self) -> &Painter { + &self.painter + } + + /// Use this to paint stuff within this `Ui`. + pub fn layer(&self) -> Layer { + self.painter().layer() } pub fn input(&self) -> &InputState { - self.ctx.input() + self.ctx().input() } pub fn memory(&self) -> parking_lot::MutexGuard<'_, Memory> { - self.ctx.memory() + self.ctx().memory() } pub fn output(&self) -> parking_lot::MutexGuard<'_, Output> { - self.ctx.output() + self.ctx().output() } pub fn fonts(&self) -> &Fonts { - self.ctx.fonts() + self.ctx().fonts() } /// Screen-space rectangle for clipping what we paint in this ui. - /// This is used, for instance, to avoid painting outside a window that is smaller - /// than its contents. + /// This is used, for instance, to avoid painting outside a window that is smaller than its contents. pub fn clip_rect(&self) -> Rect { - self.clip_rect + self.painter.clip_rect() } + /// Screen-space rectangle for clipping what we paint in this ui. + /// This is used, for instance, to avoid painting outside a window that is smaller than its contents. pub fn set_clip_rect(&mut self, clip_rect: Rect) { - self.clip_rect = clip_rect; + self.painter.set_clip_rect(clip_rect); } // ------------------------------------------------------------------------ @@ -269,7 +256,8 @@ impl Ui { // ------------------------------------------------------------------------ pub fn contains_mouse(&self, rect: Rect) -> bool { - self.ctx.contains_mouse(self.layer, self.clip_rect, rect) + self.ctx() + .contains_mouse(self.layer(), self.clip_rect(), rect) } pub fn has_kb_focus(&self, id: Id) -> bool { @@ -293,7 +281,7 @@ impl Ui { { let id = self.id.with(&id_source); // TODO: clip name clash error messages to clip rect - self.ctx.register_unique_id(id, id_source, self.cursor) + self.ctx().register_unique_id(id, id_source, self.cursor) } /// Ideally, all widgets should use this. TODO @@ -309,13 +297,13 @@ impl Ui { self.id.with(&explicit_id_source) } else { let id = self.id.with(default_id_source); - if self.ctx.is_unique_id(id) { + if self.ctx().is_unique_id(id) { id } else { self.make_position_id() } }; - self.ctx + self.ctx() .register_unique_id(id, default_id_source.unwrap_or_default(), self.cursor) } @@ -334,13 +322,13 @@ impl Ui { /// # Interaction impl Ui { pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> InteractInfo { - self.ctx - .interact(self.layer, self.clip_rect, rect, Some(id), sense) + self.ctx() + .interact(self.layer(), self.clip_rect(), rect, Some(id), sense) } pub fn interact_hover(&self, rect: Rect) -> InteractInfo { - self.ctx - .interact(self.layer, self.clip_rect, rect, None, Sense::nothing()) + self.ctx() + .interact(self.layer(), self.clip_rect(), rect, None, Sense::nothing()) } pub fn hovered(&self, rect: Rect) -> bool { @@ -365,7 +353,7 @@ impl Ui { double_clicked, active, rect, - ctx: self.ctx.clone(), + ctx: self.ctx().clone(), } } @@ -385,8 +373,8 @@ impl Ui { /// /// You may get LESS space than you asked for if the current layout won't fit what you asked for. pub fn allocate_space(&mut self, child_size: Vec2) -> Rect { - let child_size = self.round_vec_to_pixels(child_size); - self.cursor = self.round_pos_to_pixels(self.cursor); + let child_size = self.painter().round_vec_to_pixels(child_size); + self.cursor = self.painter().round_pos_to_pixels(self.cursor); // For debug rendering let too_wide = child_size.x > self.available().width(); @@ -395,7 +383,7 @@ impl Ui { let rect = self.reserve_space_impl(child_size); if self.style().debug_widget_rects { - self.add_paint_cmd(PaintCmd::Rect { + self.painter.add(PaintCmd::Rect { rect, corner_radius: 0.0, outline: Some(LineStyle::new(1.0, LIGHT_BLUE)), @@ -405,8 +393,10 @@ impl Ui { let color = color::srgba(200, 0, 0, 255); let width = 2.5; - let mut paint_line_seg = - |a, b| self.add_paint_cmd(PaintCmd::line_segment([a, b], color, width)); + let paint_line_seg = |a, b| { + self.painter + .add(PaintCmd::line_segment([a, b], color, width)) + }; if too_wide { paint_line_seg(rect.left_top(), rect.left_bottom()); @@ -437,96 +427,6 @@ impl Ui { } } -/// # Painting related stuff -impl Ui { - /// It is up to the caller to make sure there is room for this. - /// Can be used for free painting. - /// NOTE: all coordinates are screen coordinates! - pub fn add_paint_cmd(&mut self, paint_cmd: PaintCmd) { - self.ctx - .graphics() - .layer(self.layer) - .push((self.clip_rect(), paint_cmd)) - } - - pub fn add_paint_cmds(&mut self, mut cmds: Vec) { - let clip_rect = self.clip_rect(); - self.ctx - .graphics() - .layer(self.layer) - .extend(cmds.drain(..).map(|cmd| (clip_rect, cmd))); - } - - /// Insert a paint cmd before existing ones - pub fn insert_paint_cmd(&mut self, pos: usize, paint_cmd: PaintCmd) { - self.ctx - .graphics() - .layer(self.layer) - .insert(pos, (self.clip_rect(), paint_cmd)); - } - - pub fn paint_list_len(&self) -> usize { - self.ctx.graphics().layer(self.layer).len() - } - - /// Paint some debug text at current cursor - pub fn debug_text(&self, text: impl Into) { - self.debug_text_at(self.cursor, text); - } - - pub fn debug_text_at(&self, pos: Pos2, text: impl Into) { - self.ctx.debug_text(pos, text); - } - - pub fn debug_rect(&mut self, rect: Rect, text: impl Into) { - self.add_paint_cmd(PaintCmd::Rect { - corner_radius: 0.0, - fill: None, - outline: Some(LineStyle::new(1.0, color::RED)), - rect, - }); - let align = (Align::Min, Align::Min); - let text_style = TextStyle::Monospace; - self.floating_text(rect.min, text.into(), text_style, align, Some(color::RED)); - } - - /// Show some text anywhere in the ui. - /// To center the text at the given position, use `align: (Center, Center)`. - /// If you want to draw text floating on top of everything, - /// consider using `Context.floating_text` instead. - pub fn floating_text( - &mut self, - pos: Pos2, - text: impl Into, - text_style: TextStyle, - align: (Align, Align), - text_color: Option, - ) -> Rect { - let font = &self.fonts()[text_style]; - let galley = font.layout_multiline(text.into(), f32::INFINITY); - let rect = align_rect(Rect::from_min_size(pos, galley.size), align); - self.add_galley(rect.min, galley, text_style, text_color); - rect - } - - /// Already layed out text. - pub fn add_galley( - &mut self, - pos: Pos2, - galley: font::Galley, - text_style: TextStyle, - color: Option, - ) { - let color = color.unwrap_or_else(|| self.style().text_color); - self.add_paint_cmd(PaintCmd::Text { - pos, - galley, - text_style, - color, - }); - } -} - /// # Adding widgets impl Ui { pub fn add(&mut self, widget: impl Widget) -> GuiResponse { @@ -637,9 +537,9 @@ impl Ui { // draw a grey line on the left to mark the indented section let line_start = child_rect.min - indent * 0.5; - let line_start = self.round_pos_to_pixels(line_start); + let line_start = self.painter().round_pos_to_pixels(line_start); let line_end = pos2(line_start.x, line_start.y + size.y - 2.0); - self.add_paint_cmd(PaintCmd::line_segment( + self.painter.add(PaintCmd::line_segment( [line_start, line_end], gray(150, 255), self.style.line_width, diff --git a/egui/src/widgets.rs b/egui/src/widgets.rs index 15ed09a2..b673bdff 100644 --- a/egui/src/widgets.rs +++ b/egui/src/widgets.rs @@ -102,7 +102,9 @@ impl Label { // This should be the easiest method of putting text anywhere. pub fn paint_galley(&self, ui: &mut Ui, pos: Pos2, galley: font::Galley) { - ui.add_galley(pos, galley, self.text_style, self.text_color); + let text_color = self.text_color.unwrap_or_else(|| ui.style().text_color); + ui.painter() + .add_galley(pos, galley, self.text_style, text_color); } } @@ -188,10 +190,10 @@ impl Widget for Hyperlink { for line in &galley.lines { let pos = interact.rect.min; let y = pos.y + line.y_max; - let y = ui.round_to_pixel(y); + let y = ui.painter().round_to_pixel(y); let min_x = pos.x + line.min_x(); let max_x = pos.x + line.max_x(); - ui.add_paint_cmd(PaintCmd::line_segment( + ui.painter().add(PaintCmd::line_segment( [pos2(min_x, y), pos2(max_x, y)], color, ui.style().line_width, @@ -199,7 +201,8 @@ impl Widget for Hyperlink { } } - ui.add_galley(interact.rect.min, galley, text_style, Some(color)); + ui.painter() + .add_galley(interact.rect.min, galley, text_style, color); interact } @@ -279,7 +282,7 @@ impl Widget for Button { let interact = ui.interact(rect, id, sense); let text_cursor = interact.rect.left_center() + vec2(padding.x, -0.5 * galley.size.y); let bg_fill = fill.or(ui.style().interact(&interact).bg_fill); - ui.add_paint_cmd(PaintCmd::Rect { + ui.painter().add(PaintCmd::Rect { corner_radius: ui.style().interact(&interact).corner_radius, fill: bg_fill, outline: ui.style().interact(&interact).rect_outline, @@ -287,7 +290,8 @@ impl Widget for Button { }); let stroke_color = ui.style().interact(&interact).stroke_color; let text_color = text_color.unwrap_or(stroke_color); - ui.add_galley(text_cursor, galley, text_style, Some(text_color)); + ui.painter() + .add_galley(text_cursor, galley, text_style, text_color); interact } } @@ -340,7 +344,7 @@ impl<'a> Widget for Checkbox<'a> { *checked = !*checked; } let (small_icon_rect, big_icon_rect) = ui.style().icon_rectangles(interact.rect); - ui.add_paint_cmd(PaintCmd::Rect { + ui.painter().add(PaintCmd::Rect { corner_radius: ui.style().interact(&interact).corner_radius, fill: ui.style().interact(&interact).bg_fill, outline: ui.style().interact(&interact).rect_outline, @@ -350,7 +354,7 @@ impl<'a> Widget for Checkbox<'a> { let stroke_color = ui.style().interact(&interact).stroke_color; if *checked { - ui.add_paint_cmd(PaintCmd::Path { + ui.painter().add(PaintCmd::Path { path: Path::from_open_points(&[ pos2(small_icon_rect.left(), small_icon_rect.center().y), pos2(small_icon_rect.center().x, small_icon_rect.bottom()), @@ -363,7 +367,8 @@ impl<'a> Widget for Checkbox<'a> { } let text_color = text_color.unwrap_or(stroke_color); - ui.add_galley(text_cursor, galley, text_style, Some(text_color)); + ui.painter() + .add_galley(text_cursor, galley, text_style, text_color); interact } } @@ -417,7 +422,9 @@ impl Widget for RadioButton { let (small_icon_rect, big_icon_rect) = ui.style().icon_rectangles(interact.rect); - ui.add_paint_cmd(PaintCmd::Circle { + let painter = ui.painter(); + + painter.add(PaintCmd::Circle { center: big_icon_rect.center(), fill: bg_fill, outline: ui.style().interact(&interact).rect_outline, // TODO @@ -425,7 +432,7 @@ impl Widget for RadioButton { }); if checked { - ui.add_paint_cmd(PaintCmd::Circle { + painter.add(PaintCmd::Circle { center: small_icon_rect.center(), fill: Some(stroke_color), outline: None, @@ -434,7 +441,7 @@ impl Widget for RadioButton { } let text_color = text_color.unwrap_or(stroke_color); - ui.add_galley(text_cursor, galley, text_style, Some(text_color)); + painter.add_galley(text_cursor, galley, text_style, text_color); interact } } @@ -520,7 +527,7 @@ impl Widget for Separator { ) } }; - ui.add_paint_cmd(PaintCmd::LineSegment { + ui.painter().add(PaintCmd::LineSegment { points, style: LineStyle::new(line_width, color), }); diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index d848086d..84fb07da 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -107,7 +107,7 @@ impl<'a> Widget for Slider<'a> { } let text_on_top = self.text_on_top.unwrap_or_default(); - let text_color = self.text_color; + let text_color = self.text_color.unwrap_or_else(|| ui.style().text_color); let value = (self.get_set_value)(None); let full_text = format!("{}: {:.*}", text, self.precision, value); @@ -116,7 +116,7 @@ impl<'a> Widget for Slider<'a> { if text_on_top { let galley = font.layout_single_line(full_text); let pos = ui.allocate_space(galley.size).min; - ui.add_galley(pos, galley, text_style, text_color); + ui.painter().add_galley(pos, galley, text_style, text_color); slider_sans_text.ui(ui) } else { ui.columns(2, |columns| { @@ -162,21 +162,21 @@ impl<'a> Widget for Slider<'a> { let value = self.get_value_f32(); let rect = interact.rect; - let rail_radius = ui.round_to_pixel((height / 8.0).max(2.0)); + let rail_radius = ui.painter().round_to_pixel((height / 8.0).max(2.0)); let rail_rect = Rect::from_min_max( pos2(interact.rect.left(), rect.center().y - rail_radius), pos2(interact.rect.right(), rect.center().y + rail_radius), ); let marker_center_x = remap_clamp(value, range, left..=right); - ui.add_paint_cmd(PaintCmd::Rect { + ui.painter().add(PaintCmd::Rect { rect: rail_rect, corner_radius: rail_radius, fill: Some(ui.style().background_fill), outline: Some(LineStyle::new(1.0, color::gray(200, 255))), // TODO }); - ui.add_paint_cmd(PaintCmd::Circle { + ui.painter().add(PaintCmd::Circle { center: pos2(marker_center_x, rail_rect.center().y), radius: handle_radius, fill: Some(ui.style().interact(&interact).fill), diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 39eadb52..686fa62b 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -129,9 +129,11 @@ impl<'t> Widget for TextEdit<'t> { // dbg!(&galley); } + let painter = ui.painter(); + { let bg_rect = interact.rect.expand(2.0); // breathing room for content - ui.add_paint_cmd(PaintCmd::Rect { + painter.add(PaintCmd::Rect { rect: bg_rect, corner_radius: ui.style().interact.style(&interact).corner_radius, fill: Some(ui.style().dark_bg_color), @@ -146,7 +148,7 @@ impl<'t> Widget for TextEdit<'t> { if show_cursor { if let Some(cursor) = state.cursor { let cursor_pos = interact.rect.min + galley.char_start_pos(cursor); - ui.add_paint_cmd(PaintCmd::line_segment( + painter.add(PaintCmd::line_segment( [cursor_pos, cursor_pos + vec2(0.0, line_spacing)], color::WHITE, ui.style().text_cursor_width, @@ -156,7 +158,8 @@ impl<'t> Widget for TextEdit<'t> { ui.ctx().request_repaint(); // TODO: only when cursor blinks on or off } - ui.add_galley(interact.rect.min, galley, text_style, text_color); + let text_color = text_color.unwrap_or_else(|| ui.style().text_color); + painter.add_galley(interact.rect.min, galley, text_style, text_color); ui.memory().text_edit.insert(id, state); interact }