From e4e1638fc007aaa54ea4ab30ea1743c8dc3df85f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 19 Dec 2022 11:31:27 +0100 Subject: [PATCH 01/28] Fix newly introduced rendering bug for thin rectangles --- crates/epaint/src/tessellator.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index fd4b2edd..062d18ae 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1316,16 +1316,18 @@ impl Tessellator { self.tessellate_line(line, Stroke::new(rect.width(), fill), out); } if !stroke.is_empty() { - self.tessellate_line(line, stroke, out); + self.tessellate_line(line, stroke, out); // back… + self.tessellate_line(line, stroke, out); // …and forth } } else if rect.height() < self.feathering { // Very thin - approximate by a horizontal line-segment: let line = [rect.left_center(), rect.right_center()]; if fill != Color32::TRANSPARENT { - self.tessellate_line(line, Stroke::new(rect.width(), fill), out); + self.tessellate_line(line, Stroke::new(rect.height(), fill), out); } if !stroke.is_empty() { - self.tessellate_line(line, stroke, out); + self.tessellate_line(line, stroke, out); // back… + self.tessellate_line(line, stroke, out); // …and forth } } else { let path = &mut self.scratchpad_path; From 99af63fad27b2983dd3636a18fe3725e1f845464 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 19 Dec 2022 11:39:22 +0100 Subject: [PATCH 02/28] Add `Plot::clamp_grid` (#2480) * Add Plot::clamp_grid * Update changelog --- CHANGELOG.md | 5 +- crates/egui/src/widgets/plot/items/mod.rs | 1 + crates/egui/src/widgets/plot/mod.rs | 59 ++++++++++++++++++++-- crates/egui/src/widgets/plot/transform.rs | 26 +++++++--- crates/egui_demo_lib/src/demo/plot_demo.rs | 1 + 5 files changed, 80 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d513762..84b76961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ### Added ⭐ * `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)). * Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider. -* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). * Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)). +* Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)). + +### Changed 🔧 +* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). ### Fixed 🐛 * Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)). diff --git a/crates/egui/src/widgets/plot/items/mod.rs b/crates/egui/src/widgets/plot/items/mod.rs index 138aed6e..dfbcbd44 100644 --- a/crates/egui/src/widgets/plot/items/mod.rs +++ b/crates/egui/src/widgets/plot/items/mod.rs @@ -34,6 +34,7 @@ pub(super) struct PlotConfig<'a> { pub(super) trait PlotItem { fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec); + /// For plot-items which are generated based on x values (plotting functions). fn initialize(&mut self, x_range: RangeInclusive); fn name(&self) -> &str; diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 2978f0dd..d46f2056 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -291,8 +291,10 @@ pub struct Plot { legend_config: Option, show_background: bool, show_axes: [bool; 2], + grid_spacers: [GridSpacer; 2], sharp_grid_lines: bool, + clamp_grid: bool, } impl Plot { @@ -331,8 +333,10 @@ impl Plot { legend_config: None, show_background: true, show_axes: [true; 2], + grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], sharp_grid_lines: true, + clamp_grid: false, } } @@ -557,6 +561,14 @@ impl Plot { self } + /// Clamp the grid to only be visible at the range of data where we have values. + /// + /// Default: `false`. + pub fn clamp_grid(mut self, clamp_grid: bool) -> Self { + self.clamp_grid = clamp_grid; + self + } + /// Expand bounds to include the given x value. /// For instance, to always show the y axis, call `plot.include_x(0.0)`. pub fn include_x(mut self, x: impl Into) -> Self { @@ -671,6 +683,8 @@ impl Plot { show_axes, linked_axes, linked_cursors, + + clamp_grid, grid_spacers, sharp_grid_lines, } = self; @@ -971,11 +985,12 @@ impl Plot { axis_formatters, show_axes, transform: transform.clone(), - grid_spacers, draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.link_x), draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.link_y), draw_cursors, + grid_spacers, sharp_grid_lines, + clamp_grid, }; let plot_cursors = prepared.ui(ui, &response); @@ -1297,11 +1312,13 @@ struct PreparedPlot { axis_formatters: [AxisFormatter; 2], show_axes: [bool; 2], transform: ScreenTransform, - grid_spacers: [GridSpacer; 2], draw_cursor_x: bool, draw_cursor_y: bool, draw_cursors: Vec, + + grid_spacers: [GridSpacer; 2], sharp_grid_lines: bool, + clamp_grid: bool, } impl PreparedPlot { @@ -1400,10 +1417,13 @@ impl PreparedPlot { shapes: &mut Vec<(Shape, f32)>, sharp_grid_lines: bool, ) { + #![allow(clippy::collapsible_else_if)] + let Self { transform, axis_formatters, grid_spacers, + clamp_grid, .. } = self; @@ -1417,7 +1437,6 @@ impl PreparedPlot { let font_id = TextStyle::Body.resolve(ui.style()); // Where on the cross-dimension to show the label values - let bounds = transform.bounds(); let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]); let input = GridInput { @@ -1426,9 +1445,31 @@ impl PreparedPlot { }; let steps = (grid_spacers[axis])(input); + let clamp_range = clamp_grid.then(|| { + let mut tight_bounds = PlotBounds::NOTHING; + for item in &self.items { + let item_bounds = item.bounds(); + tight_bounds.merge_x(&item_bounds); + tight_bounds.merge_y(&item_bounds); + } + tight_bounds + }); + for step in steps { let value_main = step.value; + if let Some(clamp_range) = clamp_range { + if axis == 0 { + if !clamp_range.range_x().contains(&value_main) { + continue; + }; + } else { + if !clamp_range.range_y().contains(&value_main) { + continue; + }; + } + } + let value = if axis == 0 { PlotPoint::new(value_main, value_cross) } else { @@ -1451,11 +1492,23 @@ impl PreparedPlot { let mut p1 = pos_in_gui; p0[1 - axis] = transform.frame().min[1 - axis]; p1[1 - axis] = transform.frame().max[1 - axis]; + + if let Some(clamp_range) = clamp_range { + if axis == 0 { + p0.y = transform.position_from_point_y(clamp_range.min[1]); + p1.y = transform.position_from_point_y(clamp_range.max[1]); + } else { + p0.x = transform.position_from_point_x(clamp_range.min[0]); + p1.x = transform.position_from_point_x(clamp_range.max[0]); + } + } + if sharp_grid_lines { // Round to avoid aliasing p0 = ui.ctx().round_pos_to_pixels(p0); p1 = ui.ctx().round_pos_to_pixels(p1); } + shapes.push(( Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)), line_strength, diff --git a/crates/egui/src/widgets/plot/transform.rs b/crates/egui/src/widgets/plot/transform.rs index 51c63e89..ceff3a9a 100644 --- a/crates/egui/src/widgets/plot/transform.rs +++ b/crates/egui/src/widgets/plot/transform.rs @@ -224,6 +224,7 @@ impl ScreenTransform { } } + /// ui-space rectangle. pub fn frame(&self) -> &Rect { &self.frame } @@ -263,18 +264,27 @@ impl ScreenTransform { } } - pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 { - let x = remap( - value.x, + pub fn position_from_point_x(&self, value: f64) -> f32 { + remap( + value, self.bounds.min[0]..=self.bounds.max[0], (self.frame.left() as f64)..=(self.frame.right() as f64), - ); - let y = remap( - value.y, + ) as f32 + } + + pub fn position_from_point_y(&self, value: f64) -> f32 { + remap( + value, self.bounds.min[1]..=self.bounds.max[1], (self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis! - ); - pos2(x as f32, y as f32) + ) as f32 + } + + pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 { + pos2( + self.position_from_point_x(value.x), + self.position_from_point_y(value.y), + ) } pub fn value_from_position(&self, pos: Pos2) -> PlotPoint { diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs index ce834f54..bb98a114 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -755,6 +755,7 @@ impl ChartsDemo { Plot::new("Normal Distribution Demo") .legend(Legend::default()) .data_aspect(1.0) + .clamp_grid(true) .show(ui, |plot_ui| plot_ui.bar_chart(chart)) .response } From 80ea12877ead72eca8ff63af46229a5ac7396de6 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Mon, 19 Dec 2022 12:35:42 +0100 Subject: [PATCH 03/28] Add egui_skia integration to readme (#2483) * Add egui_skia integration to readme * sort Co-authored-by: Emil Ernerfeldt --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e436772c..55c5feae 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ These are the official egui integrations: * [`nannou_egui`](https://github.com/nannou-org/nannou/tree/master/nannou_egui) for [nannou](https://nannou.cc). * [`notan_egui`](https://github.com/Nazariglez/notan/tree/main/crates/notan_egui) for [notan](https://github.com/Nazariglez/notan). * [`screen-13-egui`](https://github.com/attackgoat/screen-13/tree/master/contrib/screen-13-egui) for [Screen 13](https://github.com/attackgoat/screen-13). +* [`egui_skia`](https://github.com/lucasmerlin/egui_skia) for [skia](https://github.com/rust-skia/rust-skia/tree/master/skia-safe). * [`smithay-egui`](https://github.com/Smithay/smithay-egui) for [smithay](https://github.com/Smithay/smithay/). Missing an integration for the thing you're working on? Create one, it's easy! From 0e2656b77cf5281943d753b7e228129868cb9ce1 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 20 Dec 2022 11:09:53 +0100 Subject: [PATCH 04/28] Add ScrollArea::drag_to_scroll --- CHANGELOG.md | 1 + crates/egui/src/containers/scroll_area.rs | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b76961..6e678135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider. * Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)). * Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)). +* Add `ScrollArea::drag_to_scroll` if you want to turn off that feature. ### Changed 🔧 * Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 345b5972..7af37e40 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -97,8 +97,10 @@ pub struct ScrollArea { id_source: Option, offset_x: Option, offset_y: Option, + /// If false, we ignore scroll events. scrolling_enabled: bool, + drag_to_scroll: bool, /// If true for vertical or horizontal the scroll wheel will stick to the /// end position until user manually changes position. It will become true @@ -141,6 +143,7 @@ impl ScrollArea { offset_x: None, offset_y: None, scrolling_enabled: true, + drag_to_scroll: true, stick_to_end: [false; 2], } } @@ -267,6 +270,18 @@ impl ScrollArea { self } + /// Can the user drag the scroll area to scroll? + /// + /// This is useful for touch screens. + /// + /// If `true`, the [`ScrollArea`] will sense drags. + /// + /// Default: `true`. + pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { + self.drag_to_scroll = drag_to_scroll; + self + } + /// For each axis, should the containing area shrink if the content is small? /// /// * If `true`, egui will add blank space outside the scroll area. @@ -336,6 +351,7 @@ impl ScrollArea { offset_x, offset_y, scrolling_enabled, + drag_to_scroll, stick_to_end, } = self; @@ -422,7 +438,9 @@ impl ScrollArea { let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size); - if scrolling_enabled && (state.content_is_too_large[0] || state.content_is_too_large[1]) { + if (scrolling_enabled && drag_to_scroll) + && (state.content_is_too_large[0] || state.content_is_too_large[1]) + { // Drag contents to scroll (for touch screens mostly). // We must do this BEFORE adding content to the `ScrollArea`, // or we will steal input from the widgets we contain. From a68c8910923d6957a0962d8a1e591681a459f270 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 20 Dec 2022 15:27:01 +0100 Subject: [PATCH 05/28] Improve choice of number of decimals to show when hovering in plot --- CHANGELOG.md | 1 + crates/egui/src/widgets/plot/items/bar.rs | 8 +- .../egui/src/widgets/plot/items/box_elem.rs | 10 +- crates/egui/src/widgets/plot/items/mod.rs | 5 +- crates/egui/src/widgets/plot/mod.rs | 13 + crates/egui_demo_lib/src/demo/plot_demo.rs | 237 +++++++++--------- 6 files changed, 149 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e678135..6d66952a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ### Changed 🔧 * Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). +* Improved the algorithm for picking the number of decimals to show when hovering values in the `Plot`. ### Fixed 🐛 * Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)). diff --git a/crates/egui/src/widgets/plot/items/bar.rs b/crates/egui/src/widgets/plot/items/bar.rs index 74602199..a94a7d44 100644 --- a/crates/egui/src/widgets/plot/items/bar.rs +++ b/crates/egui/src/widgets/plot/items/bar.rs @@ -185,7 +185,11 @@ impl RectElement for Bar { fn default_values_format(&self, transform: &ScreenTransform) -> String { let scale = transform.dvalue_dpos(); - let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6); - format!("\n{:.*}", y_decimals, self.value) + let scale = match self.orientation { + Orientation::Horizontal => scale[0], + Orientation::Vertical => scale[1], + }; + let decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize).at_most(6); + crate::plot::format_number(self.value, decimals) } } diff --git a/crates/egui/src/widgets/plot/items/box_elem.rs b/crates/egui/src/widgets/plot/items/box_elem.rs index a865a9db..81174afc 100644 --- a/crates/egui/src/widgets/plot/items/box_elem.rs +++ b/crates/egui/src/widgets/plot/items/box_elem.rs @@ -269,9 +269,15 @@ impl RectElement for BoxElem { fn default_values_format(&self, transform: &ScreenTransform) -> String { let scale = transform.dvalue_dpos(); - let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6); + let scale = match self.orientation { + Orientation::Horizontal => scale[0], + Orientation::Vertical => scale[1], + }; + let y_decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize) + .at_most(6) + .at_least(1); format!( - "\nMax = {max:.decimals$}\ + "Max = {max:.decimals$}\ \nQuartile 3 = {q3:.decimals$}\ \nMedian = {med:.decimals$}\ \nQuartile 1 = {q1:.decimals$}\ diff --git a/crates/egui/src/widgets/plot/items/mod.rs b/crates/egui/src/widgets/plot/items/mod.rs index dfbcbd44..378f27eb 100644 --- a/crates/egui/src/widgets/plot/items/mod.rs +++ b/crates/egui/src/widgets/plot/items/mod.rs @@ -1648,6 +1648,7 @@ fn add_rulers_and_text( let mut text = elem.name().to_owned(); // could be empty if show_values { + text.push('\n'); text.push_str(&elem.default_values_format(plot.transform)); } @@ -1694,8 +1695,8 @@ pub(super) fn rulers_at_value( let text = { let scale = plot.transform.dvalue_dpos(); - let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6); - let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6); + let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6); + let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6); if let Some(custom_label) = label_formatter { custom_label(name, &value) } else if plot.show_x && plot.show_y { diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index d46f2056..bbefe4b4 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -1647,3 +1647,16 @@ fn fill_marks_between(out: &mut Vec, step_size: f64, (min, max): (f64, }); out.extend(marks_iter); } + +/// Helper for formatting a number so that we always show at least a few decimals, +/// unless it is an integer, in which case we never show any decimals. +pub fn format_number(number: f64, num_decimals: usize) -> String { + let is_integral = number as i64 as f64 == number; + if is_integral { + // perfect integer - show it as such: + format!("{:.0}", number) + } else { + // make sure we tell the user it is not an integer by always showing a decimal or two: + format!("{:.*}", num_decimals.at_least(1), number) + } +} diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs index bb98a114..7e9d8833 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -11,6 +11,124 @@ use plot::{ // ---------------------------------------------------------------------------- +#[derive(PartialEq, Eq)] +enum Panel { + Lines, + Markers, + Legend, + Charts, + Items, + Interaction, + CustomAxes, + LinkedAxes, +} + +impl Default for Panel { + fn default() -> Self { + Self::Lines + } +} + +// ---------------------------------------------------------------------------- + +#[derive(PartialEq, Default)] +pub struct PlotDemo { + line_demo: LineDemo, + marker_demo: MarkerDemo, + legend_demo: LegendDemo, + charts_demo: ChartsDemo, + items_demo: ItemsDemo, + interaction_demo: InteractionDemo, + custom_axes_demo: CustomAxisDemo, + linked_axes_demo: LinkedAxisDemo, + open_panel: Panel, +} + +impl super::Demo for PlotDemo { + fn name(&self) -> &'static str { + "🗠 Plot" + } + + fn show(&mut self, ctx: &Context, open: &mut bool) { + use super::View as _; + Window::new(self.name()) + .open(open) + .default_size(vec2(400.0, 400.0)) + .vscroll(false) + .show(ctx, |ui| self.ui(ui)); + } +} + +impl super::View for PlotDemo { + fn ui(&mut self, ui: &mut Ui) { + ui.horizontal(|ui| { + egui::reset_button(ui, self); + ui.collapsing("Instructions", |ui| { + ui.label("Pan by dragging, or scroll (+ shift = horizontal)."); + ui.label("Box zooming: Right click to zoom in and zoom out using a selection."); + if cfg!(target_arch = "wasm32") { + ui.label("Zoom with ctrl / ⌘ + pointer wheel, or with pinch gesture."); + } else if cfg!(target_os = "macos") { + ui.label("Zoom with ctrl / ⌘ + scroll."); + } else { + ui.label("Zoom with ctrl + scroll."); + } + ui.label("Reset view with double-click."); + ui.add(crate::egui_github_link_file!()); + }); + }); + ui.separator(); + ui.horizontal(|ui| { + ui.selectable_value(&mut self.open_panel, Panel::Lines, "Lines"); + ui.selectable_value(&mut self.open_panel, Panel::Markers, "Markers"); + ui.selectable_value(&mut self.open_panel, Panel::Legend, "Legend"); + ui.selectable_value(&mut self.open_panel, Panel::Charts, "Charts"); + ui.selectable_value(&mut self.open_panel, Panel::Items, "Items"); + ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction"); + ui.selectable_value(&mut self.open_panel, Panel::CustomAxes, "Custom Axes"); + ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes"); + }); + ui.separator(); + + match self.open_panel { + Panel::Lines => { + self.line_demo.ui(ui); + } + Panel::Markers => { + self.marker_demo.ui(ui); + } + Panel::Legend => { + self.legend_demo.ui(ui); + } + Panel::Charts => { + self.charts_demo.ui(ui); + } + Panel::Items => { + self.items_demo.ui(ui); + } + Panel::Interaction => { + self.interaction_demo.ui(ui); + } + Panel::CustomAxes => { + self.custom_axes_demo.ui(ui); + } + Panel::LinkedAxes => { + self.linked_axes_demo.ui(ui); + } + } + } +} + +fn is_approx_zero(val: f64) -> bool { + val.abs() < 1e-6 +} + +fn is_approx_integer(val: f64) -> bool { + val.fract().abs() < 1e-6 +} + +// ---------------------------------------------------------------------------- + #[derive(PartialEq)] struct LineDemo { animate: bool, @@ -754,7 +872,6 @@ impl ChartsDemo { Plot::new("Normal Distribution Demo") .legend(Legend::default()) - .data_aspect(1.0) .clamp_grid(true) .show(ui, |plot_ui| plot_ui.bar_chart(chart)) .response @@ -865,121 +982,3 @@ impl ChartsDemo { .response } } - -// ---------------------------------------------------------------------------- - -#[derive(PartialEq, Eq)] -enum Panel { - Lines, - Markers, - Legend, - Charts, - Items, - Interaction, - CustomAxes, - LinkedAxes, -} - -impl Default for Panel { - fn default() -> Self { - Self::Lines - } -} - -// ---------------------------------------------------------------------------- - -#[derive(PartialEq, Default)] -pub struct PlotDemo { - line_demo: LineDemo, - marker_demo: MarkerDemo, - legend_demo: LegendDemo, - charts_demo: ChartsDemo, - items_demo: ItemsDemo, - interaction_demo: InteractionDemo, - custom_axes_demo: CustomAxisDemo, - linked_axes_demo: LinkedAxisDemo, - open_panel: Panel, -} - -impl super::Demo for PlotDemo { - fn name(&self) -> &'static str { - "🗠 Plot" - } - - fn show(&mut self, ctx: &Context, open: &mut bool) { - use super::View as _; - Window::new(self.name()) - .open(open) - .default_size(vec2(400.0, 400.0)) - .vscroll(false) - .show(ctx, |ui| self.ui(ui)); - } -} - -impl super::View for PlotDemo { - fn ui(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - egui::reset_button(ui, self); - ui.collapsing("Instructions", |ui| { - ui.label("Pan by dragging, or scroll (+ shift = horizontal)."); - ui.label("Box zooming: Right click to zoom in and zoom out using a selection."); - if cfg!(target_arch = "wasm32") { - ui.label("Zoom with ctrl / ⌘ + pointer wheel, or with pinch gesture."); - } else if cfg!(target_os = "macos") { - ui.label("Zoom with ctrl / ⌘ + scroll."); - } else { - ui.label("Zoom with ctrl + scroll."); - } - ui.label("Reset view with double-click."); - ui.add(crate::egui_github_link_file!()); - }); - }); - ui.separator(); - ui.horizontal(|ui| { - ui.selectable_value(&mut self.open_panel, Panel::Lines, "Lines"); - ui.selectable_value(&mut self.open_panel, Panel::Markers, "Markers"); - ui.selectable_value(&mut self.open_panel, Panel::Legend, "Legend"); - ui.selectable_value(&mut self.open_panel, Panel::Charts, "Charts"); - ui.selectable_value(&mut self.open_panel, Panel::Items, "Items"); - ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction"); - ui.selectable_value(&mut self.open_panel, Panel::CustomAxes, "Custom Axes"); - ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes"); - }); - ui.separator(); - - match self.open_panel { - Panel::Lines => { - self.line_demo.ui(ui); - } - Panel::Markers => { - self.marker_demo.ui(ui); - } - Panel::Legend => { - self.legend_demo.ui(ui); - } - Panel::Charts => { - self.charts_demo.ui(ui); - } - Panel::Items => { - self.items_demo.ui(ui); - } - Panel::Interaction => { - self.interaction_demo.ui(ui); - } - Panel::CustomAxes => { - self.custom_axes_demo.ui(ui); - } - Panel::LinkedAxes => { - self.linked_axes_demo.ui(ui); - } - } - } -} - -fn is_approx_zero(val: f64) -> bool { - val.abs() < 1e-6 -} - -fn is_approx_integer(val: f64) -> bool { - val.fract().abs() < 1e-6 -} From fa0d7f7f7f3e6736b7191e6e0573cc58c51663d2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 21 Dec 2022 17:47:52 +0100 Subject: [PATCH 06/28] Add Response::on_hover_and_drag_cursor --- CHANGELOG.md | 1 + crates/egui/src/response.rs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d66952a..d59b8bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)). * Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)). * Add `ScrollArea::drag_to_scroll` if you want to turn off that feature. +* Add `Response::on_hover_and_drag_cursor`. ### Changed 🔧 * Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 7e108f37..072f5280 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -459,6 +459,14 @@ impl Response { self } + /// When hovered or dragged, use this icon for the mouse cursor. + pub fn on_hover_and_drag_cursor(self, cursor: CursorIcon) -> Self { + if self.hovered() || self.dragged() { + self.ctx.output().cursor_icon = cursor; + } + self + } + /// Check for more interactions (e.g. sense clicks on a [`Response`] returned from a label). /// /// Note that this call will not add any hover-effects to the widget, so when possible From 34f587d1e1cc69146f7a02f20903e4f573030ffd Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 22 Dec 2022 12:33:06 +0100 Subject: [PATCH 07/28] Add emath::inverse_lerp --- crates/emath/src/lib.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/emath/src/lib.rs b/crates/emath/src/lib.rs index 7652252f..5a88c9a3 100644 --- a/crates/emath/src/lib.rs +++ b/crates/emath/src/lib.rs @@ -87,6 +87,14 @@ impl Real for f64 {} // ---------------------------------------------------------------------------- /// Linear interpolation. +/// +/// ``` +/// # use emath::lerp; +/// assert_eq!(lerp(1.0..=5.0, 0.0), 1.0); +/// assert_eq!(lerp(1.0..=5.0, 0.5), 3.0); +/// assert_eq!(lerp(1.0..=5.0, 1.0), 5.0); +/// assert_eq!(lerp(1.0..=5.0, 2.0), 9.0); +/// ``` #[inline(always)] pub fn lerp(range: RangeInclusive, t: T) -> R where @@ -96,6 +104,34 @@ where (T::one() - t) * *range.start() + t * *range.end() } +/// Where in the range is this value? Returns 0-1 if within the range. +/// +/// Returns <0 if before and >1 if after. +/// +/// Returns `None` if the input range is zero-width. +/// +/// ``` +/// # use emath::inverse_lerp; +/// assert_eq!(inverse_lerp(1.0..=5.0, 1.0), Some(0.0)); +/// assert_eq!(inverse_lerp(1.0..=5.0, 3.0), Some(0.5)); +/// assert_eq!(inverse_lerp(1.0..=5.0, 5.0), Some(1.0)); +/// assert_eq!(inverse_lerp(1.0..=5.0, 9.0), Some(2.0)); +/// assert_eq!(inverse_lerp(1.0..=1.0, 3.0), None); +/// ``` +#[inline] +pub fn inverse_lerp(range: RangeInclusive, value: R) -> Option +where + R: Copy + PartialEq + Sub + Div, +{ + let min = *range.start(); + let max = *range.end(); + if min == max { + None + } else { + Some((value - min) / (max - min)) + } +} + /// Linearly remap a value from one range to another, /// so that when `x == from.start()` returns `to.start()` /// and when `x == from.end()` returns `to.end()`. From e70204d72605500fe9463f7abbfac824e419ab4e Mon Sep 17 00:00:00 2001 From: Kornel Date: Sat, 7 Jan 2023 19:27:19 +0000 Subject: [PATCH 08/28] Point to caller's location when using log_or_panic (#2552) --- crates/egui_extras/src/strip.rs | 3 +++ crates/egui_extras/src/table.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/crates/egui_extras/src/strip.rs b/crates/egui_extras/src/strip.rs index 9c091859..52a768f3 100644 --- a/crates/egui_extras/src/strip.rs +++ b/crates/egui_extras/src/strip.rs @@ -144,6 +144,7 @@ pub struct Strip<'a, 'b> { } impl<'a, 'b> Strip<'a, 'b> { + #[cfg_attr(debug_assertions, track_caller)] fn next_cell_size(&mut self) -> (CellSize, CellSize) { let size = if let Some(size) = self.sizes.get(self.size_index) { self.size_index += 1; @@ -163,6 +164,7 @@ impl<'a, 'b> Strip<'a, 'b> { } /// Add cell contents. + #[cfg_attr(debug_assertions, track_caller)] pub fn cell(&mut self, add_contents: impl FnOnce(&mut Ui)) { let (width, height) = self.next_cell_size(); let striped = false; @@ -171,6 +173,7 @@ impl<'a, 'b> Strip<'a, 'b> { } /// Add an empty cell. + #[cfg_attr(debug_assertions, track_caller)] pub fn empty(&mut self) { let (width, height) = self.next_cell_size(); self.layout.empty(width, height); diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 875aed1f..5de605a8 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -1026,6 +1026,7 @@ impl<'a, 'b> TableRow<'a, 'b> { /// Add the contents of a column. /// /// Return the used space (`min_rect`) plus the [`Response`] of the whole cell. + #[cfg_attr(debug_assertions, track_caller)] pub fn col(&mut self, add_cell_contents: impl FnOnce(&mut Ui)) -> (Rect, Response) { let col_index = self.col_index; From f7ed13519223b27727003dba9c7e5ad48bad40b2 Mon Sep 17 00:00:00 2001 From: joffreybesos <93067706+joffreybesos@users.noreply.github.com> Date: Sun, 8 Jan 2023 06:48:49 +1100 Subject: [PATCH 09/28] window starting collapsed state (#2539) --- crates/egui/src/containers/window.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 9d7032c5..a16ad860 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -30,6 +30,7 @@ pub struct Window<'open> { resize: Resize, scroll: ScrollArea, collapsible: bool, + default_open: bool, with_title_bar: bool, } @@ -51,6 +52,7 @@ impl<'open> Window<'open> { .default_size([340.0, 420.0]), // Default inner size of a window scroll: ScrollArea::neither(), collapsible: true, + default_open: true, with_title_bar: true, } } @@ -162,6 +164,12 @@ impl<'open> Window<'open> { self } + /// Set initial collapsed state of the window + pub fn default_open(mut self, default_open: bool) -> Self { + self.default_open = default_open; + self + } + /// Set initial size of the window. pub fn default_size(mut self, default_size: impl Into) -> Self { self.resize = self.resize.default_size(default_size); @@ -275,6 +283,7 @@ impl<'open> Window<'open> { resize, scroll, collapsible, + default_open, with_title_bar, } = self; @@ -291,7 +300,7 @@ impl<'open> Window<'open> { let area_layer_id = area.layer(); let resize_id = area_id.with("resize"); let mut collapsing = - CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), true); + CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open); let is_collapsed = with_title_bar && !collapsing.is_open(); let possible = PossibleInteractions::new(&area, &resize, is_collapsed); From 60b4f5e3fe00fe3f840374846dfd241817e6bf1e Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Sat, 7 Jan 2023 20:51:01 +0100 Subject: [PATCH 10/28] changelog & doc fix for #2539 (Window::default_open) --- CHANGELOG.md | 1 + crates/egui/src/containers/window.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d59b8bfa..3adff09a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)). * Add `ScrollArea::drag_to_scroll` if you want to turn off that feature. * Add `Response::on_hover_and_drag_cursor`. +* Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)) ### Changed 🔧 * Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index a16ad860..1b5ffc6f 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -10,7 +10,7 @@ use super::*; /// /// You can customize: /// * title -/// * default, minimum, maximum and/or fixed size +/// * default, minimum, maximum and/or fixed size, collapsed/expanded /// * if the window has a scroll area (off by default) /// * if the window can be collapsed (minimized) to just the title bar (yes, by default) /// * if there should be a close button (none by default) From cd0f66b9ae661be737ce57f26fb305cf124f6b73 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Jan 2023 14:40:19 +0100 Subject: [PATCH 11/28] eframe web: ctrl-P and cmd-P will not open the print dialog (#2598) --- crates/eframe/CHANGELOG.md | 3 +++ crates/eframe/src/web/events.rs | 24 +++++++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index bcabaeb7..0f67c3db 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -9,6 +9,9 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C #### Desktop/Native: * `eframe::run_native` now returns a `Result` ([#2433](https://github.com/emilk/egui/pull/2433)). +#### Web: +* Prevent ctrl-P/cmd-P from opening the print dialog ([#2598](https://github.com/emilk/egui/pull/2598)). + ## 0.20.1 - 2022-12-11 * Fix docs.rs build ([#2420](https://github.com/emilk/egui/pull/2420)). diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 6ac9e1e4..dcca886a 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -1,6 +1,9 @@ -use super::*; use std::sync::atomic::{AtomicBool, Ordering}; +use egui::Key; + +use super::*; + struct IsDestroyed(pub bool); pub fn paint_and_schedule( @@ -64,8 +67,9 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res runner_lock.input.raw.modifiers = modifiers; let key = event.key(); + let egui_key = translate_key(&key); - if let Some(key) = translate_key(&key) { + if let Some(key) = egui_key { runner_lock.input.raw.events.push(egui::Event::Key { key, pressed: true, @@ -85,10 +89,13 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input(); - let prevent_default = if matches!(event.key().as_str(), "Tab") { + #[allow(clippy::if_same_then_else)] + let prevent_default = if egui_key == Some(Key::Tab) { // Always prevent moving cursor to url bar. // egui wants to use tab to move to the next text field. true + } else if egui_key == Some(Key::P) { + true // Prevent ctrl-P opening the print dialog. Users may want to use it for a command palette. } else if egui_wants_keyboard { matches!( event.key().as_str(), @@ -112,6 +119,7 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res if prevent_default { event.prevent_default(); + // event.stop_propagation(); } }, )?; @@ -198,15 +206,21 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> { let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap(); - { + let prevent_default_events = [ // By default, right-clicks open a context menu. // We don't want to do that (right clicks is handled by egui): - let event_name = "contextmenu"; + "contextmenu", + // Allow users to use ctrl-p for e.g. a command palette + "afterprint", + ]; + for event_name in prevent_default_events { let closure = move |event: web_sys::MouseEvent, mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| { event.prevent_default(); + // event.stop_propagation(); + // tracing::debug!("Preventing event {:?}", event_name); }; runner_container.add_event_listener(&canvas, event_name, closure)?; From 0eabd894bd0e4466d53e3098e8cce1f10183f3b0 Mon Sep 17 00:00:00 2001 From: apoorv569 <53279454+apoorv569@users.noreply.github.com> Date: Tue, 17 Jan 2023 15:28:38 +0530 Subject: [PATCH 12/28] Fix typo in cargo run command. (#2582) I think someone by mistake wrote `cargo run -p hello_world` instead of `cargo run -p keyboard_events`. --- examples/keyboard_events/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/keyboard_events/README.md b/examples/keyboard_events/README.md index afe67b25..6f88b6cd 100644 --- a/examples/keyboard_events/README.md +++ b/examples/keyboard_events/README.md @@ -1,5 +1,5 @@ ```sh -cargo run -p hello_world +cargo run -p keyboard_events ``` ![](screenshot.png) From 53b1d0e5e9016d9915467cd83fcbc8b313439df5 Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Mon, 23 Jan 2023 15:22:43 +0800 Subject: [PATCH 13/28] Add menu with an image button (#2488) --- crates/egui/src/menu.rs | 33 +++++++++++++++++++++++++++++++++ crates/egui/src/ui.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 59c15348..089a290c 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -100,6 +100,20 @@ pub fn menu_button( stationary_menu_impl(ui, title, Box::new(add_contents)) } +/// Construct a top level menu with an image in a menu bar. This would be e.g. "File", "Edit" etc. +/// +/// Responds to primary clicks. +/// +/// Returns `None` if the menu is not open. +pub fn menu_image_button( + ui: &mut Ui, + texture_id: TextureId, + image_size: impl Into, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> InnerResponse> { + stationary_menu_image_impl(ui, texture_id, image_size, Box::new(add_contents)) +} + /// Construct a nested sub menu in another menu. /// /// Opens on hover. @@ -177,6 +191,25 @@ fn stationary_menu_impl<'c, R>( InnerResponse::new(inner.map(|r| r.inner), button_response) } +/// Build a top level menu with an image button. +/// +/// Responds to primary clicks. +fn stationary_menu_image_impl<'c, R>( + ui: &mut Ui, + texture_id: TextureId, + image_size: impl Into, + add_contents: Box R + 'c>, +) -> InnerResponse> { + let bar_id = ui.id(); + + let mut bar_state = BarState::load(ui.ctx(), bar_id); + let button_response = ui.add(ImageButton::new(texture_id, image_size)); + let inner = bar_state.bar_menu(&button_response, add_contents); + + bar_state.store(ui.ctx(), bar_id); + InnerResponse::new(inner.map(|r| r.inner), button_response) +} + /// Response to secondary clicks (right-clicks) by showing the given menu. pub(crate) fn context_menu( response: &Response, diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index f8a68563..a8ac5c92 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2157,6 +2157,43 @@ impl Ui { menu::menu_button(self, title, add_contents) } } + + #[inline] + /// Create a menu button with an image that when clicked will show the given menu. + /// + /// If called from within a menu this will instead create a button for a sub-menu. + /// + /// ``` + /// use egui_extras; + /// + /// let img = egui_extrasRetainedImage::from_svg_bytes_with_size( + /// "rss", + /// include_bytes!("rss.svg"), + /// egui_extras::image::FitTo::Size(24, 24), + /// ); + /// + /// ui.menu_image_button(img.texture_id(ctx), img.size_vec2(), |ui| { + /// ui.menu_button("My sub-menu", |ui| { + /// if ui.button("Close the menu").clicked() { + /// ui.close_menu(); + /// } + /// }); + /// }); + /// ``` + /// + /// See also: [`Self::close_menu`] and [`Response::context_menu`]. + pub fn menu_image_button( + &mut self, + texture_id: TextureId, + image_size: impl Into, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse> { + if let Some(menu_state) = self.menu_state.clone() { + menu::submenu_button(self, menu_state, String::new(), add_contents) + } else { + menu::menu_image_button(self, texture_id, image_size, add_contents) + } + } } // ---------------------------------------------------------------------------- From 0ad8aea8116836016d07efb9b5cbab68ce1a00ee Mon Sep 17 00:00:00 2001 From: Robert Walter <26892280+RobWalt@users.noreply.github.com> Date: Mon, 23 Jan 2023 09:23:57 +0100 Subject: [PATCH 14/28] Fix: `button_padding` when using image+text buttons (#2510) * feat(image-button-margin): implement image button margin - add `image_margin` field on `Button` widget - implement setter method called `image_margin` for `Button` widget - use margin from `image_margin` field of `Button` widget in `Widget` trait functions * feat(image-button-margin): update changelog * feat(image-button-margin): implement `map_or` clippy fix * feat(image-button-margin): remove margin field & fix button-padding instead * feat(image-button-margin): fix CI errors * feat(image-button-margin): update changelog to include fix * feat(image-button-margin): re-add changes after creating screenshots for PR --- CHANGELOG.md | 1 + crates/egui/src/widgets/button.rs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3adff09a..e6ad6728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ### Fixed 🐛 * Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)). * Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)). +* The `button_padding` style option works closer as expected with image+text buttons now ([#2510](https://github.com/emilk/egui/pull/2510)). ## 0.20.1 - 2022-12-11 - Fix key-repeat diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 19385470..7f050a06 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -221,7 +221,10 @@ impl Widget for Button { if let Some(image) = image { let image_rect = Rect::from_min_size( - pos2(rect.min.x, rect.center().y - 0.5 - (image.size().y / 2.0)), + pos2( + rect.min.x + button_padding.x, + rect.center().y - 0.5 - (image.size().y / 2.0), + ), image.size(), ); image.paint_at(ui, image_rect); From ce5472633d3920bc41d0b2e4df933f91f764293e Mon Sep 17 00:00:00 2001 From: RadonCoding <86915746+RadonCoding@users.noreply.github.com> Date: Mon, 23 Jan 2023 10:55:57 +0200 Subject: [PATCH 15/28] Fix close button not working (#2533) * Fix close button not working By adding the close button after the title bar drag listener the close button will sense clicks. * Update main.rs --- examples/custom_window_frame/src/main.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/custom_window_frame/src/main.rs b/examples/custom_window_frame/src/main.rs index e4110d01..d6a35416 100644 --- a/examples/custom_window_frame/src/main.rs +++ b/examples/custom_window_frame/src/main.rs @@ -84,15 +84,6 @@ fn custom_window_frame( Stroke::new(1.0, text_color), ); - // Add the close button: - let close_response = ui.put( - Rect::from_min_size(rect.left_top(), Vec2::splat(height)), - Button::new(RichText::new("❌").size(height - 4.0)).frame(false), - ); - if close_response.clicked() { - frame.close(); - } - // Interact with the title bar (drag to move window): let title_bar_rect = { let mut rect = rect; @@ -105,6 +96,15 @@ fn custom_window_frame( frame.drag_window(); } + // Add the close button: + let close_response = ui.put( + Rect::from_min_size(rect.left_top(), Vec2::splat(height)), + Button::new(RichText::new("❌").size(height - 4.0)).frame(false), + ); + if close_response.clicked() { + frame.close(); + } + // Add the contents: let content_rect = { let mut rect = rect; From f87c6cbd7caed01387fbf73b0a91e3f83eb97d1a Mon Sep 17 00:00:00 2001 From: Ales Tsurko Date: Mon, 23 Jan 2023 12:06:54 +0300 Subject: [PATCH 16/28] Derive Hash for KeyboardShortcut and Modifiers (#2563) --- crates/egui/src/data/input.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 4c64b56a..30dab676 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -314,7 +314,7 @@ pub const NUM_POINTER_BUTTONS: usize = 5; /// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers /// as on mac that is how you type special characters, /// so those key presses are usually not reported to egui. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Modifiers { /// Either of the alt keys are down (option ⌥ on Mac). @@ -777,7 +777,7 @@ impl Key { /// /// Can be used with [`crate::InputState::consume_shortcut`] /// and [`crate::Context::format_shortcut`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub struct KeyboardShortcut { pub modifiers: Modifiers, pub key: Key, From 01bbda4544b4a76acf2b00dc9ede95ba1b399794 Mon Sep 17 00:00:00 2001 From: lictex_ Date: Mon, 23 Jan 2023 19:20:05 +0800 Subject: [PATCH 17/28] check point count before tessellating bezier (#2506) --- crates/epaint/src/tessellator.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 062d18ae..fe80e478 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1507,6 +1507,10 @@ impl Tessellator { stroke: Stroke, out: &mut Mesh, ) { + if points.len() < 2 { + return; + } + self.scratchpad_path.clear(); if closed { self.scratchpad_path.add_line_loop(points); From 30e49f1da2cce74afa4e5e39991c5f3bdb9da3e8 Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 23 Jan 2023 12:37:15 +0100 Subject: [PATCH 18/28] Expose area interactable and movable to Window api. (#2610) * Expose area interactable to window. * Add movable function * update dockstring --- crates/egui/src/containers/window.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 1b5ffc6f..1182e6a7 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -79,6 +79,18 @@ impl<'open> Window<'open> { self } + /// If `false` the window will be non-interactive. + pub fn interactable(mut self, interactable: bool) -> Self { + self.area = self.area.interactable(interactable); + self + } + + /// If `false` the window will be immovable. + pub fn movable(mut self, movable: bool) -> Self { + self.area = self.area.movable(movable); + self + } + /// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))` // TODO(emilk): I'm not sure this is a good interface for this. pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self { From 5029575ed038dbcb7f0147bc7996c11295fc9e13 Mon Sep 17 00:00:00 2001 From: LEAVING <52616273+LEAVING-7@users.noreply.github.com> Date: Mon, 23 Jan 2023 19:37:26 +0800 Subject: [PATCH 19/28] Fix typo: 'Viewport width' -> 'Viewport height' (#2615) --- crates/epaint/src/shape.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 3771296d..9304a27b 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -768,7 +768,7 @@ pub struct ViewportInPixels { /// Viewport width in physical pixels. pub width_px: f32, - /// Viewport width in physical pixels. + /// Viewport height in physical pixels. pub height_px: f32, } From 356ebe55da1f57099f422d21cc035fdf48d51125 Mon Sep 17 00:00:00 2001 From: Weasy Date: Mon, 23 Jan 2023 12:37:39 +0100 Subject: [PATCH 20/28] Add `rounding` fn to Button, to enable rounded buttons (#2616) --- CHANGELOG.md | 1 + crates/egui/src/widgets/button.rs | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6ad6728..ca0b4b68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Add `ScrollArea::drag_to_scroll` if you want to turn off that feature. * Add `Response::on_hover_and_drag_cursor`. * Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)) +* Add `Button::rounding` to enable round buttons ([#2539](https://github.com/emilk/egui/pull/2539)) ### Changed 🔧 * Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 7f050a06..e485dbb9 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -30,6 +30,7 @@ pub struct Button { small: bool, frame: Option, min_size: Vec2, + rounding: Option, image: Option, } @@ -45,6 +46,7 @@ impl Button { small: false, frame: None, min_size: Vec2::ZERO, + rounding: None, image: None, } } @@ -117,6 +119,12 @@ impl Button { self } + /// Set the rounding of the button. + pub fn rounding(mut self, rounding: impl Into) -> Self { + self.rounding = Some(rounding.into()); + self + } + /// Show some text on the right side of the button, in weak color. /// /// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`). @@ -140,6 +148,7 @@ impl Widget for Button { small, frame, min_size, + rounding, image, } = self; @@ -186,12 +195,9 @@ impl Widget for Button { if frame { let fill = fill.unwrap_or(visuals.bg_fill); let stroke = stroke.unwrap_or(visuals.bg_stroke); - ui.painter().rect( - rect.expand(visuals.expansion), - visuals.rounding, - fill, - stroke, - ); + let rounding = rounding.unwrap_or(visuals.rounding); + ui.painter() + .rect(rect.expand(visuals.expansion), rounding, fill, stroke); } let text_pos = if let Some(image) = image { From d4f9f6984d704c5eaa52416e8e02687ec80ac7be Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Jan 2023 15:04:35 +0100 Subject: [PATCH 21/28] Fix red doctest --- crates/egui/src/ui.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index a8ac5c92..23ad9092 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2163,10 +2163,10 @@ impl Ui { /// /// If called from within a menu this will instead create a button for a sub-menu. /// - /// ``` + /// ```ignore /// use egui_extras; /// - /// let img = egui_extrasRetainedImage::from_svg_bytes_with_size( + /// let img = egui_extras::RetainedImage::from_svg_bytes_with_size( /// "rss", /// include_bytes!("rss.svg"), /// egui_extras::image::FitTo::Size(24, 24), From 518b4f447e5ac1ce647920cab6b36066d6f1ded5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A2ris=20DOUADY?= Date: Mon, 23 Jan 2023 15:33:02 +0100 Subject: [PATCH 22/28] Allow changing ProgressBar fill color (#2618) --- CHANGELOG.md | 1 + crates/egui/src/widgets/progress_bar.rs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca0b4b68..645f25e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Add `ScrollArea::drag_to_scroll` if you want to turn off that feature. * Add `Response::on_hover_and_drag_cursor`. * Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)) +* Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)) * Add `Button::rounding` to enable round buttons ([#2539](https://github.com/emilk/egui/pull/2539)) ### Changed 🔧 diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index 7a10dd40..6762a2dd 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -13,6 +13,7 @@ pub struct ProgressBar { progress: f32, desired_width: Option, text: Option, + fill: Option, animate: bool, } @@ -23,6 +24,7 @@ impl ProgressBar { progress: progress.clamp(0.0, 1.0), desired_width: None, text: None, + fill: None, animate: false, } } @@ -33,6 +35,12 @@ impl ProgressBar { self } + /// The fill color of the bar. + pub fn fill(mut self, color: Color32) -> Self { + self.fill = Some(color); + self + } + /// A custom text to display on the progress bar. pub fn text(mut self, text: impl Into) -> Self { self.text = Some(ProgressBarText::Custom(text.into())); @@ -60,6 +68,7 @@ impl Widget for ProgressBar { progress, desired_width, text, + fill, animate, } = self; @@ -98,7 +107,9 @@ impl Widget for ProgressBar { ui.painter().rect( inner_rect, rounding, - Color32::from(Rgba::from(visuals.selection.bg_fill) * color_factor as f32), + Color32::from( + Rgba::from(fill.unwrap_or(visuals.selection.bg_fill)) * color_factor as f32, + ), Stroke::NONE, ); From c75e72693c0828f73c5626423f670a3c0c7f7d29 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Jan 2023 20:24:38 +0100 Subject: [PATCH 23/28] =?UTF-8?q?Fix=20rendering=20of=20`=E2=80=A6`=20(ell?= =?UTF-8?q?ipsis)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Broken when we introduced thing space support --- CHANGELOG.md | 1 + crates/epaint/src/text/font.rs | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 645f25e2..c19304ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)). * Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)). * The `button_padding` style option works closer as expected with image+text buttons now ([#2510](https://github.com/emilk/egui/pull/2510)). +* Fixed rendering of `…` (ellipsis). ## 0.20.1 - 2022-12-11 - Fix key-repeat diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 67dfd43e..e97687f0 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -392,7 +392,38 @@ fn invisible_char(c: char) -> bool { // See https://github.com/emilk/egui/issues/336 // From https://www.fileformat.info/info/unicode/category/Cf/list.htm - ('\u{200B}'..='\u{206F}').contains(&c) // TODO(emilk): heed bidi characters + + // TODO(emilk): heed bidi characters + + matches!( + c, + '\u{200B}' // ZERO WIDTH SPACE + | '\u{200C}' // ZERO WIDTH NON-JOINER + | '\u{200D}' // ZERO WIDTH JOINER + | '\u{200E}' // LEFT-TO-RIGHT MARK + | '\u{200F}' // RIGHT-TO-LEFT MARK + | '\u{202A}' // LEFT-TO-RIGHT EMBEDDING + | '\u{202B}' // RIGHT-TO-LEFT EMBEDDING + | '\u{202C}' // POP DIRECTIONAL FORMATTING + | '\u{202D}' // LEFT-TO-RIGHT OVERRIDE + | '\u{202E}' // RIGHT-TO-LEFT OVERRIDE + | '\u{2060}' // WORD JOINER + | '\u{2061}' // FUNCTION APPLICATION + | '\u{2062}' // INVISIBLE TIMES + | '\u{2063}' // INVISIBLE SEPARATOR + | '\u{2064}' // INVISIBLE PLUS + | '\u{2066}' // LEFT-TO-RIGHT ISOLATE + | '\u{2067}' // RIGHT-TO-LEFT ISOLATE + | '\u{2068}' // FIRST STRONG ISOLATE + | '\u{2069}' // POP DIRECTIONAL ISOLATE + | '\u{206A}' // INHIBIT SYMMETRIC SWAPPING + | '\u{206B}' // ACTIVATE SYMMETRIC SWAPPING + | '\u{206C}' // INHIBIT ARABIC FORM SHAPING + | '\u{206D}' // ACTIVATE ARABIC FORM SHAPING + | '\u{206E}' // NATIONAL DIGIT SHAPES + | '\u{206F}' // NOMINAL DIGIT SHAPES + | '\u{FEFF}' // ZERO WIDTH NO-BREAK SPACE + ) } fn allocate_glyph( From 4bd4eca2e4b440ca585d8844740afd8144ba7dbc Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Jan 2023 10:11:05 +0100 Subject: [PATCH 24/28] Add ability to hide button backgrounds (#2621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Spacing::combo_width * Put ComboBox arrow closer to the text * Tweak faint_bg_color * Make it possible to have buttons without background …while still having background for sliders, checkboxes, etc * Rename mandatory_bg_fill -> bg_fill * tweak grid stripe color (again) * Make the animated part of the ProgressBar more visible * Add line in changelog * Add another line in changelog * Menu fix: use the `open` widget style for open menus * Adjust sizes on menu buttons and regular buttons to make sure they match * Update comment Co-authored-by: Andreas Reich * optional_bg_fill -> weak_bg_fill Co-authored-by: Andreas Reich --- CHANGELOG.md | 8 ++- .../egui/src/containers/collapsing_header.rs | 2 +- crates/egui/src/containers/combo_box.rs | 17 +++--- crates/egui/src/containers/popup.rs | 2 + crates/egui/src/menu.rs | 11 ++-- crates/egui/src/style.rs | 58 ++++++++++++++----- crates/egui/src/widgets/button.rs | 6 +- crates/egui/src/widgets/progress_bar.rs | 6 +- crates/egui/src/widgets/selected_label.rs | 8 ++- crates/egui/src/widgets/slider.rs | 4 -- .../egui_demo_lib/src/demo/widget_gallery.rs | 2 + crates/egui_extras/src/datepicker/button.rs | 2 +- 12 files changed, 82 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c19304ba..e30fc98e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,13 +12,15 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)). * Add `ScrollArea::drag_to_scroll` if you want to turn off that feature. * Add `Response::on_hover_and_drag_cursor`. -* Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)) -* Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)) -* Add `Button::rounding` to enable round buttons ([#2539](https://github.com/emilk/egui/pull/2539)) +* Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)). +* Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)). +* Add `Button::rounding` to enable round buttons ([#2616](https://github.com/emilk/egui/pull/2616)). +* Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)). ### Changed 🔧 * Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). * Improved the algorithm for picking the number of decimals to show when hovering values in the `Plot`. +* Default `ComboBox` is now controlled with `Spacing::combo_width` ([#2621](https://github.com/emilk/egui/pull/2621)). ### Fixed 🐛 * Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)). diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 942e0625..ea7d52de 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -555,7 +555,7 @@ impl CollapsingHeader { ui.painter().add(epaint::RectShape { rect: header_response.rect.expand(visuals.expansion), rounding: visuals.rounding, - fill: visuals.bg_fill, + fill: visuals.weak_bg_fill, stroke: visuals.bg_stroke, // stroke: Default::default(), }); diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 9942e899..911f9424 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -162,9 +162,6 @@ impl ComboBox { let button_id = ui.make_persistent_id(id_source); ui.horizontal(|ui| { - if let Some(width) = width { - ui.spacing_mut().slider_width = width; // yes, this is ugly. Will remove later. - } let mut ir = combo_box_dyn( ui, button_id, @@ -172,6 +169,7 @@ impl ComboBox { menu_contents, icon, wrap_enabled, + width, ); if let Some(label) = label { ir.response @@ -240,6 +238,7 @@ fn combo_box_dyn<'c, R>( menu_contents: Box R + 'c>, icon: Option, wrap_enabled: bool, + width: Option, ) -> InnerResponse> { let popup_id = button_id.with("popup"); @@ -263,18 +262,20 @@ fn combo_box_dyn<'c, R>( let margin = ui.spacing().button_padding; let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| { + let icon_spacing = ui.spacing().icon_spacing; // We don't want to change width when user selects something new let full_minimum_width = if wrap_enabled { // Currently selected value's text will be wrapped if needed, so occupy the available width. ui.available_width() } else { - // Occupy at least the minimum width assigned to Slider and ComboBox. - ui.spacing().slider_width - 2.0 * margin.x + // Occupy at least the minimum width assigned to ComboBox. + let width = width.unwrap_or_else(|| ui.spacing().combo_width); + width - 2.0 * margin.x }; let icon_size = Vec2::splat(ui.spacing().icon_width); let wrap_width = if wrap_enabled { // Use the available width, currently selected value's text will be wrapped if exceeds this value. - ui.available_width() - ui.spacing().item_spacing.x - icon_size.x + ui.available_width() - icon_spacing - icon_size.x } else { // Use all the width necessary to display the currently selected value's text. f32::INFINITY @@ -288,7 +289,7 @@ fn combo_box_dyn<'c, R>( full_minimum_width } else { // Occupy at least the minimum width needed to contain the widget with the currently selected value's text. - galley.size().x + ui.spacing().item_spacing.x + icon_size.x + galley.size().x + icon_spacing + icon_size.x }; // Case : wrap_enabled : occupy all the available width. @@ -390,7 +391,7 @@ fn button_frame( epaint::RectShape { rect: outer_rect.expand(visuals.expansion), rounding: visuals.rounding, - fill: visuals.bg_fill, + fill: visuals.weak_bg_fill, stroke: visuals.bg_stroke, }, ); diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 4decca3f..add02b22 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -314,6 +314,8 @@ pub fn popup_below_widget( /// /// Useful for drop-down menus (combo boxes) or suggestion menus under text fields. /// +/// The opened popup will have the same width as the parent. +/// /// You must open the popup with [`Memory::open_popup`] or [`Memory::toggle_popup`]. /// /// Returns `None` if the popup is not open. diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 089a290c..46236d6d 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -68,7 +68,7 @@ fn set_menu_style(style: &mut Style) { style.spacing.button_padding = vec2(2.0, 0.0); style.visuals.widgets.active.bg_stroke = Stroke::NONE; style.visuals.widgets.hovered.bg_stroke = Stroke::NONE; - style.visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT; + style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; style.visuals.widgets.inactive.bg_stroke = Stroke::NONE; } @@ -180,7 +180,7 @@ fn stationary_menu_impl<'c, R>( let mut button = Button::new(title); if bar_state.open_menu.is_menu_open(menu_id) { - button = button.fill(ui.visuals().widgets.open.bg_fill); + button = button.fill(ui.visuals().widgets.open.weak_bg_fill); button = button.stroke(ui.visuals().widgets.open.bg_stroke); } @@ -443,7 +443,7 @@ impl SubMenuButton { sub_id: Id, ) -> &'a WidgetVisuals { if menu_state.is_open(sub_id) { - &ui.style().visuals.widgets.hovered + &ui.style().visuals.widgets.open } else { ui.style().interact(response) } @@ -472,7 +472,8 @@ impl SubMenuButton { text_galley.size().x + icon_galley.size().x, text_galley.size().y.max(icon_galley.size().y), ); - let desired_size = text_and_icon_size + 2.0 * button_padding; + let mut desired_size = text_and_icon_size + 2.0 * button_padding; + desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); let (rect, response) = ui.allocate_at_least(desired_size, sense); response.widget_info(|| { @@ -492,7 +493,7 @@ impl SubMenuButton { ui.painter().rect_filled( rect.expand(visuals.expansion), visuals.rounding, - visuals.bg_fill, + visuals.weak_bg_fill, ); } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 963f1b90..3078595d 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -216,6 +216,7 @@ impl Style { pub fn interact_selectable(&self, response: &Response, selected: bool) -> WidgetVisuals { let mut visuals = *self.visuals.widgets.style(response); if selected { + visuals.weak_bg_fill = self.visuals.selection.bg_fill; visuals.bg_fill = self.visuals.selection.bg_fill; // visuals.bg_stroke = self.visuals.selection.stroke; visuals.fg_stroke = self.visuals.selection.stroke; @@ -264,8 +265,11 @@ pub struct Spacing { /// Anything clickable should be (at least) this size. pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ? - /// Default width of a [`Slider`] and [`ComboBox`](crate::ComboBox). - pub slider_width: f32, // TODO(emilk): rename big_interact_size ? + /// Default width of a [`Slider`]. + pub slider_width: f32, + + /// Default (minimum) width of a [`ComboBox`](crate::ComboBox). + pub combo_width: f32, /// Default width of a [`TextEdit`]. pub text_edit_width: f32, @@ -533,7 +537,7 @@ impl Visuals { // TODO(emilk): replace with an alpha #[inline(always)] pub fn fade_out_to_color(&self) -> Color32 { - self.widgets.noninteractive.bg_fill + self.widgets.noninteractive.weak_bg_fill } /// Returned a "grayed out" version of the given color. @@ -594,9 +598,17 @@ impl Widgets { #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct WidgetVisuals { - /// Background color of widget. + /// Background color of widgets that must have a background fill, + /// such as the slider background, a checkbox background, or a radio button background. + /// + /// Must never be [`Color32::TRANSPARENT`]. pub bg_fill: Color32, + /// Background color of widgets that can _optionally_ have a background fill, such as buttons. + /// + /// May be [`Color32::TRANSPARENT`]. + pub weak_bg_fill: Color32, + /// For surrounding rectangle of things that need it, /// like buttons, the box of the checkbox, etc. /// Should maybe be called `frame_stroke`. @@ -684,6 +696,7 @@ impl Default for Spacing { indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing` interact_size: vec2(40.0, 18.0), slider_width: 100.0, + combo_width: 100.0, text_edit_width: 280.0, icon_width: 14.0, icon_width_inner: 8.0, @@ -717,8 +730,8 @@ impl Visuals { widgets: Widgets::default(), selection: Selection::default(), hyperlink_color: Color32::from_rgb(90, 170, 255), - faint_bg_color: Color32::from_gray(35), - extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background + faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so + extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background code_bg_color: Color32::from_gray(64), warn_fg_color: Color32::from_rgb(255, 143, 0), // orange error_fg_color: Color32::from_rgb(255, 0, 0), // red @@ -751,8 +764,8 @@ impl Visuals { widgets: Widgets::light(), selection: Selection::light(), hyperlink_color: Color32::from_rgb(0, 155, 255), - faint_bg_color: Color32::from_gray(242), - extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background + faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so + extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background code_bg_color: Color32::from_gray(230), warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background. error_fg_color: Color32::from_rgb(255, 0, 0), // red @@ -801,6 +814,7 @@ impl Widgets { pub fn dark() -> Self { Self { noninteractive: WidgetVisuals { + weak_bg_fill: Color32::from_gray(27), bg_fill: Color32::from_gray(27), bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color @@ -808,13 +822,15 @@ impl Widgets { expansion: 0.0, }, inactive: WidgetVisuals { - bg_fill: Color32::from_gray(60), // button background + weak_bg_fill: Color32::from_gray(60), // button background + bg_fill: Color32::from_gray(60), // checkbox background bg_stroke: Default::default(), fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text rounding: Rounding::same(2.0), expansion: 0.0, }, hovered: WidgetVisuals { + weak_bg_fill: Color32::from_gray(70), bg_fill: Color32::from_gray(70), bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button fg_stroke: Stroke::new(1.5, Color32::from_gray(240)), @@ -822,6 +838,7 @@ impl Widgets { expansion: 1.0, }, active: WidgetVisuals { + weak_bg_fill: Color32::from_gray(55), bg_fill: Color32::from_gray(55), bg_stroke: Stroke::new(1.0, Color32::WHITE), fg_stroke: Stroke::new(2.0, Color32::WHITE), @@ -829,6 +846,7 @@ impl Widgets { expansion: 1.0, }, open: WidgetVisuals { + weak_bg_fill: Color32::from_gray(27), bg_fill: Color32::from_gray(27), bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), fg_stroke: Stroke::new(1.0, Color32::from_gray(210)), @@ -841,6 +859,7 @@ impl Widgets { pub fn light() -> Self { Self { noninteractive: WidgetVisuals { + weak_bg_fill: Color32::from_gray(248), bg_fill: Color32::from_gray(248), bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color @@ -848,13 +867,15 @@ impl Widgets { expansion: 0.0, }, inactive: WidgetVisuals { - bg_fill: Color32::from_gray(230), // button background + weak_bg_fill: Color32::from_gray(230), // button background + bg_fill: Color32::from_gray(230), // checkbox background bg_stroke: Default::default(), fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text rounding: Rounding::same(2.0), expansion: 0.0, }, hovered: WidgetVisuals { + weak_bg_fill: Color32::from_gray(220), bg_fill: Color32::from_gray(220), bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button fg_stroke: Stroke::new(1.5, Color32::BLACK), @@ -862,6 +883,7 @@ impl Widgets { expansion: 1.0, }, active: WidgetVisuals { + weak_bg_fill: Color32::from_gray(165), bg_fill: Color32::from_gray(165), bg_stroke: Stroke::new(1.0, Color32::BLACK), fg_stroke: Stroke::new(2.0, Color32::BLACK), @@ -869,6 +891,7 @@ impl Widgets { expansion: 1.0, }, open: WidgetVisuals { + weak_bg_fill: Color32::from_gray(220), bg_fill: Color32::from_gray(220), bg_stroke: Stroke::new(1.0, Color32::from_gray(160)), fg_stroke: Stroke::new(1.0, Color32::BLACK), @@ -984,6 +1007,7 @@ impl Spacing { indent, interact_size, slider_width, + combo_width, text_edit_width, icon_width, icon_width_inner, @@ -1012,6 +1036,10 @@ impl Spacing { ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0)); ui.label("Slider width"); }); + ui.horizontal(|ui| { + ui.add(DragValue::new(combo_width).clamp_range(0.0..=1000.0)); + ui.label("ComboBox width"); + }); ui.horizontal(|ui| { ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0)); ui.label("TextEdit width"); @@ -1185,13 +1213,17 @@ impl Selection { impl WidgetVisuals { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { - bg_fill, + weak_bg_fill, + bg_fill: mandatory_bg_fill, bg_stroke, rounding, fg_stroke, expansion, } = self; - ui_color(ui, bg_fill, "background fill"); + ui_color(ui, weak_bg_fill, "optional background fill") + .on_hover_text("For buttons, combo-boxes, etc"); + ui_color(ui, mandatory_bg_fill, "mandatory background fill") + .on_hover_text("For checkboxes, sliders, etc"); stroke_ui(ui, bg_stroke, "background stroke"); rounding_ui(ui, rounding); @@ -1270,7 +1302,7 @@ impl Visuals { } = self; ui.collapsing("Background Colors", |ui| { - ui_color(ui, &mut widgets.inactive.bg_fill, "Buttons"); + ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons"); ui_color(ui, window_fill, "Windows"); ui_color(ui, panel_fill, "Panels"); ui_color(ui, faint_bg_color, "Faint accent").on_hover_text( diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index e485dbb9..8f4ad6bb 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -180,10 +180,10 @@ impl Widget for Button { desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x; desired_size.y = desired_size.y.max(shortcut_text.size().y); } + desired_size += 2.0 * button_padding; if !small { desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); } - desired_size += 2.0 * button_padding; desired_size = desired_size.at_least(min_size); let (rect, response) = ui.allocate_at_least(desired_size, sense); @@ -193,7 +193,7 @@ impl Widget for Button { let visuals = ui.style().interact(&response); if frame { - let fill = fill.unwrap_or(visuals.bg_fill); + let fill = fill.unwrap_or(visuals.weak_bg_fill); let stroke = stroke.unwrap_or(visuals.bg_stroke); let rounding = rounding.unwrap_or(visuals.rounding); ui.painter() @@ -540,7 +540,7 @@ impl Widget for ImageButton { ( expansion, visuals.rounding, - visuals.bg_fill, + visuals.weak_bg_fill, visuals.bg_stroke, ) } else { diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index 6762a2dd..89f4b0bf 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -127,10 +127,8 @@ impl Widget for ProgressBar { + vec2(-rounding, 0.0) }) .collect(); - ui.painter().add(Shape::line( - points, - Stroke::new(2.0, visuals.faint_bg_color), - )); + ui.painter() + .add(Shape::line(points, Stroke::new(2.0, visuals.text_color()))); } if let Some(text_kind) = text { diff --git a/crates/egui/src/widgets/selected_label.rs b/crates/egui/src/widgets/selected_label.rs index 2f2d49d2..86024ae3 100644 --- a/crates/egui/src/widgets/selected_label.rs +++ b/crates/egui/src/widgets/selected_label.rs @@ -64,8 +64,12 @@ impl Widget for SelectableLabel { if selected || response.hovered() || response.has_focus() { let rect = rect.expand(visuals.expansion); - ui.painter() - .rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke); + ui.painter().rect( + rect, + visuals.rounding, + visuals.weak_bg_fill, + visuals.bg_stroke, + ); } text.paint_with_visuals(ui.painter(), text_pos, &visuals); diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 0142e2e3..9d03fa06 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -624,11 +624,7 @@ impl<'a> Slider<'a> { rect: rail_rect, rounding: ui.visuals().widgets.inactive.rounding, fill: ui.visuals().widgets.inactive.bg_fill, - // fill: visuals.bg_fill, - // fill: ui.visuals().extreme_bg_color, stroke: Default::default(), - // stroke: visuals.bg_stroke, - // stroke: ui.visuals().widgets.inactive.bg_stroke, }); let center = self.marker_center(position_1d, &rail_rect); diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 22d4ac06..d91ae395 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -175,6 +175,8 @@ impl WidgetGallery { egui::ComboBox::from_label("Take your pick") .selected_text(format!("{:?}", radio)) .show_ui(ui, |ui| { + ui.style_mut().wrap = Some(false); + ui.set_min_width(60.0); ui.selectable_value(radio, Enum::First, "First"); ui.selectable_value(radio, Enum::Second, "Second"); ui.selectable_value(radio, Enum::Third, "Third"); diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index 9c5cb111..2a07ba9c 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -76,7 +76,7 @@ impl<'a> Widget for DatePickerButton<'a> { } let mut button = Button::new(text); if button_state.picker_visible { - button = button.fill(visuals.bg_fill).stroke(visuals.bg_stroke); + button = button.fill(visuals.weak_bg_fill).stroke(visuals.bg_stroke); } let mut button_response = ui.add(button); if button_response.clicked() { From eee4cf6a824e21141bb893aae2aea7849c0e9e03 Mon Sep 17 00:00:00 2001 From: lictex_ Date: Tue, 24 Jan 2023 17:11:32 +0800 Subject: [PATCH 25/28] add functions to check which button triggered a drag start & end (#2507) * add button release events for drags * add utility functions * fix CHANGELOG.md * fix CHANGELOG.md Co-authored-by: Emil Ernerfeldt --- CHANGELOG.md | 3 ++ crates/egui/src/context.rs | 8 ++-- crates/egui/src/input_state.rs | 69 ++++++++++++++++++++++------------ crates/egui/src/response.rs | 10 +++++ 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e30fc98e..6e340596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ## Unreleased ### Added ⭐ +* Add `Response::drag_started_by` and `Response::drag_released_by` for convenience, similar to `dragged` and `dragged_by` ([#2507](https://github.com/emilk/egui/pull/2507)). +* Add `PointerState::*_pressed` to check if the given button was pressed in this frame ([#2507](https://github.com/emilk/egui/pull/2507)). * `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)). * Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider. * Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)). @@ -23,6 +25,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Default `ComboBox` is now controlled with `Spacing::combo_width` ([#2621](https://github.com/emilk/egui/pull/2621)). ### Fixed 🐛 +* Trigger `PointerEvent::Released` for drags ([#2507](https://github.com/emilk/egui/pull/2507)). * Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)). * Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)). * The `button_padding` style option works closer as expected with image+text buttons now ([#2510](https://github.com/emilk/egui/pull/2510)). diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index dc4a479b..d94f9b79 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -564,17 +564,17 @@ impl Context { } } } - PointerEvent::Released(click) => { + PointerEvent::Released { click, button } => { response.drag_released = response.dragged; response.dragged = false; if hovered && response.is_pointer_button_down_on { if let Some(click) = click { let clicked = hovered && response.is_pointer_button_down_on; - response.clicked[click.button as usize] = clicked; - response.double_clicked[click.button as usize] = + response.clicked[*button as usize] = clicked; + response.double_clicked[*button as usize] = clicked && click.is_double(); - response.triple_clicked[click.button as usize] = + response.triple_clicked[*button as usize] = clicked && click.is_triple(); } } diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 050bcc3b..92c6a32f 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -447,7 +447,6 @@ impl InputState { #[derive(Clone, Debug, PartialEq)] pub(crate) struct Click { pub pos: Pos2, - pub button: PointerButton, /// 1 or 2 (double-click) or 3 (triple-click) pub count: u32, /// Allows you to check for e.g. shift-click @@ -471,7 +470,10 @@ pub(crate) enum PointerEvent { position: Pos2, button: PointerButton, }, - Released(Option), + Released { + click: Option, + button: PointerButton, + }, } impl PointerEvent { @@ -480,11 +482,11 @@ impl PointerEvent { } pub fn is_release(&self) -> bool { - matches!(self, PointerEvent::Released(_)) + matches!(self, PointerEvent::Released { .. }) } pub fn is_click(&self) -> bool { - matches!(self, PointerEvent::Released(Some(_click))) + matches!(self, PointerEvent::Released { click: Some(_), .. }) } } @@ -639,7 +641,6 @@ impl PointerState { Some(Click { pos, - button, count, modifiers, }) @@ -647,7 +648,8 @@ impl PointerState { None }; - self.pointer_events.push(PointerEvent::Released(click)); + self.pointer_events + .push(PointerEvent::Released { click, button }); self.press_origin = None; self.press_start_time = None; @@ -775,11 +777,28 @@ impl PointerState { self.pointer_events.iter().any(|event| event.is_release()) } + /// Was the button given pressed this frame? + pub fn button_pressed(&self, button: PointerButton) -> bool { + self.pointer_events + .iter() + .any(|event| matches!(event, &PointerEvent::Pressed{button: b, ..} if button == b)) + } + /// Was the button given released this frame? pub fn button_released(&self, button: PointerButton) -> bool { self.pointer_events .iter() - .any(|event| matches!(event, &PointerEvent::Released(Some(Click{button: b, ..})) if button == b)) + .any(|event| matches!(event, &PointerEvent::Released{button: b, ..} if button == b)) + } + + /// Was the primary button pressed this frame? + pub fn primary_pressed(&self) -> bool { + self.button_pressed(PointerButton::Primary) + } + + /// Was the secondary button pressed this frame? + pub fn secondary_pressed(&self) -> bool { + self.button_pressed(PointerButton::Secondary) } /// Was the primary button released this frame? @@ -811,16 +830,28 @@ impl PointerState { /// Was the button given double clicked this frame? pub fn button_double_clicked(&self, button: PointerButton) -> bool { - self.pointer_events - .iter() - .any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_double())) + self.pointer_events.iter().any(|event| { + matches!( + &event, + PointerEvent::Released { + click: Some(click), + button: b, + } if *b == button && click.is_double() + ) + }) } /// Was the button given triple clicked this frame? pub fn button_triple_clicked(&self, button: PointerButton) -> bool { - self.pointer_events - .iter() - .any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_triple())) + self.pointer_events.iter().any(|event| { + matches!( + &event, + PointerEvent::Released { + click: Some(click), + button: b, + } if *b == button && click.is_triple() + ) + }) } /// Was the primary button clicked this frame? @@ -833,18 +864,6 @@ impl PointerState { self.button_clicked(PointerButton::Secondary) } - // /// Was this button pressed (`!down -> down`) this frame? - // /// This can sometimes return `true` even if `any_down() == false` - // /// because a press can be shorted than one frame. - // pub fn button_pressed(&self, button: PointerButton) -> bool { - // self.pointer_events.iter().any(|event| event.is_press()) - // } - - // /// Was this button released (`down -> !down`) this frame? - // pub fn button_released(&self, button: PointerButton) -> bool { - // self.pointer_events.iter().any(|event| event.is_release()) - // } - /// Is this button currently down? #[inline(always)] pub fn button_down(&self, button: PointerButton) -> bool { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 072f5280..28ab3fd5 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -278,11 +278,21 @@ impl Response { self.dragged && self.ctx.input().pointer.any_pressed() } + /// Did a drag on this widgets by the button begin this frame? + pub fn drag_started_by(&self, button: PointerButton) -> bool { + self.drag_started() && self.ctx.input().pointer.button_pressed(button) + } + /// The widget was being dragged, but now it has been released. pub fn drag_released(&self) -> bool { self.drag_released } + /// The widget was being dragged by the button, but now it has been released. + pub fn drag_released_by(&self, button: PointerButton) -> bool { + self.drag_released() && self.ctx.input().pointer.button_released(button) + } + /// If dragged, how many points were we dragged and in what direction? pub fn drag_delta(&self) -> Vec2 { if self.dragged() { From 8ce0e1c5206780e76234842b94ceb0edf5bb8b75 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 25 Jan 2023 10:24:23 +0100 Subject: [PATCH 26/28] Avoid deadlocks by using lambdas for context lock (#2625) ctx.input().key_pressed(Key::A) -> ctx.input(|i| i.key_pressed(Key::A)) --- .gitignore | 3 +- CHANGELOG.md | 5 + Cargo.lock | 7 + README.md | 3 + crates/eframe/Cargo.toml | 2 +- crates/eframe/src/epi.rs | 2 +- crates/eframe/src/native/epi_integration.rs | 13 +- crates/eframe/src/web/backend.rs | 9 +- crates/eframe/src/web/storage.rs | 4 +- crates/egui-winit/src/lib.rs | 2 +- crates/egui/src/containers/area.rs | 32 +- .../egui/src/containers/collapsing_header.rs | 11 +- crates/egui/src/containers/combo_box.rs | 13 +- crates/egui/src/containers/panel.rs | 65 +- crates/egui/src/containers/popup.rs | 31 +- crates/egui/src/containers/resize.rs | 8 +- crates/egui/src/containers/scroll_area.rs | 21 +- crates/egui/src/containers/window.rs | 55 +- crates/egui/src/context.rs | 904 ++++++++++-------- crates/egui/src/data/output.rs | 2 +- crates/egui/src/grid.rs | 4 +- crates/egui/src/gui_zoom.rs | 6 +- crates/egui/src/input_state.rs | 2 +- crates/egui/src/introspection.rs | 4 +- crates/egui/src/lib.rs | 2 +- crates/egui/src/memory.rs | 9 +- crates/egui/src/menu.rs | 64 +- crates/egui/src/painter.rs | 28 +- crates/egui/src/response.rs | 99 +- crates/egui/src/ui.rs | 171 ++-- crates/egui/src/util/id_type_map.rs | 8 +- crates/egui/src/widget_text.rs | 4 +- crates/egui/src/widgets/color_picker.rs | 16 +- crates/egui/src/widgets/drag_value.rs | 61 +- crates/egui/src/widgets/hyperlink.rs | 20 +- crates/egui/src/widgets/label.rs | 6 +- crates/egui/src/widgets/plot/items/mod.rs | 37 +- crates/egui/src/widgets/plot/legend.rs | 4 +- crates/egui/src/widgets/plot/mod.rs | 12 +- crates/egui/src/widgets/progress_bar.rs | 8 +- crates/egui/src/widgets/slider.rs | 45 +- crates/egui/src/widgets/spinner.rs | 5 +- crates/egui/src/widgets/text_edit/builder.rs | 229 ++--- crates/egui/src/widgets/text_edit/state.rs | 4 +- .../egui_demo_app/src/apps/fractal_clock.rs | 2 +- crates/egui_demo_app/src/apps/http_app.rs | 8 +- crates/egui_demo_app/src/backend_panel.rs | 14 +- crates/egui_demo_app/src/frame_history.rs | 20 +- crates/egui_demo_app/src/wrap_app.rs | 51 +- crates/egui_demo_lib/benches/benchmark.rs | 2 +- crates/egui_demo_lib/src/demo/code_editor.rs | 2 +- crates/egui_demo_lib/src/demo/code_example.rs | 2 +- .../egui_demo_lib/src/demo/dancing_strings.rs | 2 +- .../src/demo/demo_app_windows.rs | 18 +- .../egui_demo_lib/src/demo/drag_and_drop.rs | 14 +- crates/egui_demo_lib/src/demo/font_book.rs | 21 +- .../src/demo/misc_demo_window.rs | 2 +- crates/egui_demo_lib/src/demo/multi_touch.rs | 8 +- crates/egui_demo_lib/src/demo/paint_bezier.rs | 7 +- crates/egui_demo_lib/src/demo/password.rs | 4 +- crates/egui_demo_lib/src/demo/plot_demo.rs | 2 +- crates/egui_demo_lib/src/demo/scrolling.rs | 2 +- crates/egui_demo_lib/src/demo/text_edit.rs | 9 +- .../egui_demo_lib/src/demo/window_options.rs | 4 +- .../src/easy_mark/easy_mark_editor.rs | 6 +- .../src/easy_mark/easy_mark_viewer.rs | 2 +- crates/egui_demo_lib/src/lib.rs | 2 +- .../egui_demo_lib/src/syntax_highlighting.rs | 35 +- crates/egui_extras/src/datepicker/button.rs | 10 +- crates/egui_extras/src/datepicker/popup.rs | 73 +- crates/egui_extras/src/table.rs | 12 +- examples/file_dialog/src/main.rs | 33 +- examples/hello_world_par/Cargo.toml | 16 + examples/hello_world_par/README.md | 5 + examples/hello_world_par/src/main.rs | 132 +++ examples/keyboard_events/src/main.rs | 6 +- examples/puffin_profiler/src/main.rs | 2 +- 77 files changed, 1445 insertions(+), 1123 deletions(-) create mode 100644 examples/hello_world_par/Cargo.toml create mode 100644 examples/hello_world_par/README.md create mode 100644 examples/hello_world_par/src/main.rs diff --git a/.gitignore b/.gitignore index f33c23b9..c43b9559 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ +.DS_Store **/target **/target_ra +**/target_wasm /.*.json /.vscode /media/* -.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e340596..dbd28bf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ## Unreleased +* ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)): + * `ctx.input().key_pressed(Key::A)` -> `ctx.input(|i| i.key_pressed(Key::A))` + * `ui.memory().toggle_popup(popup_id)` -> `ui.memory_mut(|mem| mem.toggle_popup(popup_id))` + ### Added ⭐ * Add `Response::drag_started_by` and `Response::drag_released_by` for convenience, similar to `dragged` and `dragged_by` ([#2507](https://github.com/emilk/egui/pull/2507)). * Add `PointerState::*_pressed` to check if the given button was pressed in this frame ([#2507](https://github.com/emilk/egui/pull/2507)). @@ -18,6 +22,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)). * Add `Button::rounding` to enable round buttons ([#2616](https://github.com/emilk/egui/pull/2616)). * Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)). +* Add `Context::screen_rect` and `Context::set_cursor_icon` ([#2625](https://github.com/emilk/egui/pull/2625)). ### Changed 🔧 * Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). diff --git a/Cargo.lock b/Cargo.lock index 36a4872b..34cc098f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2171,6 +2171,13 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "hello_world_par" +version = "0.1.0" +dependencies = [ + "eframe", +] + [[package]] name = "hermit-abi" version = "0.1.19" diff --git a/README.md b/README.md index 55c5feae..e30042a9 100644 --- a/README.md +++ b/README.md @@ -371,6 +371,8 @@ egui uses the builder pattern for construction widgets. For instance: `ui.add(La Instead of using matching `begin/end` style function calls (which can be error prone) egui prefers to use `FnOnce` closures passed to a wrapping function. Lambdas are a bit ugly though, so I'd like to find a nicer solution to this. More discussion of this at . +egui uses a single `RwLock` for short-time locks on each access of `Context` data. This is to leave implementation simple and transactional and allow users to run their UI logic in parallel. Instead of creating mutex guards, egui uses closures passed to a wrapping function, e.g. `ctx.input(|i| i.key_down(Key::A))`. This is to make it less likely that a user would accidentally double-lock the `Context`, which would lead to a deadlock. + ### Inspiration The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immediate Mode GUI for C++ which works with many backends. That library revolutionized how I think about GUI code and turned GUI programming from something I hated to do to something I now enjoy. @@ -396,6 +398,7 @@ Notable contributions by: * [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543). * [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868). * [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050). +* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625). * And [many more](https://github.com/emilk/egui/graphs/contributors?type=a). egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE). diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index ef528e40..e7fb4935 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -55,7 +55,7 @@ persistence = [ ## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you puffin = ["dep:puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"] -## Enable screen reader support (requires `ctx.options().screen_reader = true;`) +## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) screen_reader = ["egui-winit/screen_reader", "tts"] ## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit. diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index e6442736..c4bd5406 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -173,7 +173,7 @@ pub trait App { } /// If `true` a warm-up call to [`Self::update`] will be issued where - /// `ctx.memory().everything_is_visible()` will be set to `true`. + /// `ctx.memory(|mem| mem.everything_is_visible())` will be set to `true`. /// /// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on. /// diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index ff54d12f..ea572271 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -257,7 +257,8 @@ impl EpiIntegration { ) -> Self { let egui_ctx = egui::Context::default(); - *egui_ctx.memory() = load_egui_memory(storage.as_deref()).unwrap_or_default(); + let memory = load_egui_memory(storage.as_deref()).unwrap_or_default(); + egui_ctx.memory_mut(|mem| *mem = memory); let native_pixels_per_point = window.scale_factor() as f32; @@ -315,11 +316,12 @@ impl EpiIntegration { pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { crate::profile_function!(); - let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); - self.egui_ctx.memory().set_everything_is_visible(true); + let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone()); + self.egui_ctx + .memory_mut(|mem| mem.set_everything_is_visible(true)); let full_output = self.update(app, window); self.pending_full_output.append(full_output); // Handle it next frame - *self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge. + self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge. self.egui_ctx.clear_animations(); } @@ -446,7 +448,8 @@ impl EpiIntegration { } if _app.persist_egui_memory() { crate::profile_scope!("egui_memory"); - epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, &*self.egui_ctx.memory()); + self.egui_ctx + .memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem)); } { crate::profile_scope!("App::save"); diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index f139cab7..107a8ce4 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -313,10 +313,11 @@ impl AppRunner { pub fn warm_up(&mut self) -> Result<(), JsValue> { if self.app.warm_up_enabled() { - let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); - self.egui_ctx.memory().set_everything_is_visible(true); + let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone()); + self.egui_ctx + .memory_mut(|m| m.set_everything_is_visible(true)); self.logic()?; - *self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge. + self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge. self.egui_ctx.clear_animations(); } Ok(()) @@ -388,7 +389,7 @@ impl AppRunner { } fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) { - if self.egui_ctx.options().screen_reader { + if self.egui_ctx.options(|o| o.screen_reader) { self.screen_reader .speak(&platform_output.events_description()); } diff --git a/crates/eframe/src/web/storage.rs b/crates/eframe/src/web/storage.rs index 6a3d5279..f0f3c843 100644 --- a/crates/eframe/src/web/storage.rs +++ b/crates/eframe/src/web/storage.rs @@ -15,7 +15,7 @@ pub fn load_memory(ctx: &egui::Context) { if let Some(memory_string) = local_storage_get("egui_memory_ron") { match ron::from_str(&memory_string) { Ok(memory) => { - *ctx.memory() = memory; + ctx.memory_mut(|m| *m = memory); } Err(err) => { tracing::error!("Failed to parse memory RON: {}", err); @@ -29,7 +29,7 @@ pub fn load_memory(_: &egui::Context) {} #[cfg(feature = "persistence")] pub fn save_memory(ctx: &egui::Context) { - match ron::to_string(&*ctx.memory()) { + match ctx.memory(|mem| ron::to_string(mem)) { Ok(ron) => { local_storage_set("egui_memory_ron", &ron); } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 82c2d68c..07c5766e 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -615,7 +615,7 @@ impl State { egui_ctx: &egui::Context, platform_output: egui::PlatformOutput, ) { - if egui_ctx.options().screen_reader { + if egui_ctx.options(|o| o.screen_reader) { self.screen_reader .speak(&platform_output.events_description()); } diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index de348e15..84f0ea44 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -5,7 +5,7 @@ use crate::*; /// State that is persisted between frames. -// TODO(emilk): this is not currently stored in `memory().data`, but maybe it should be? +// TODO(emilk): this is not currently stored in `Memory::data`, but maybe it should be? #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) struct State { @@ -231,7 +231,7 @@ impl Area { let layer_id = LayerId::new(order, id); - let state = ctx.memory().areas.get(id).copied(); + let state = ctx.memory(|mem| mem.areas.get(id).copied()); let is_new = state.is_none(); if is_new { ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place @@ -278,7 +278,7 @@ impl Area { // Important check - don't try to move e.g. a combobox popup! if movable { if move_response.dragged() { - state.pos += ctx.input().pointer.delta(); + state.pos += ctx.input(|i| i.pointer.delta()); } state.pos = ctx @@ -288,9 +288,9 @@ impl Area { if (move_response.dragged() || move_response.clicked()) || pointer_pressed_on_area(ctx, layer_id) - || !ctx.memory().areas.visible_last_frame(&layer_id) + || !ctx.memory(|m| m.areas.visible_last_frame(&layer_id)) { - ctx.memory().areas.move_to_top(layer_id); + ctx.memory_mut(|m| m.areas.move_to_top(layer_id)); ctx.request_repaint(); } @@ -329,7 +329,7 @@ impl Area { } let layer_id = LayerId::new(self.order, self.id); - let area_rect = ctx.memory().areas.get(self.id).map(|area| area.rect()); + let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect())); if let Some(area_rect) = area_rect { let clip_rect = ctx.available_rect(); let painter = Painter::new(ctx.clone(), layer_id, clip_rect); @@ -358,7 +358,7 @@ impl Prepared { } pub(crate) fn content_ui(&self, ctx: &Context) -> Ui { - let screen_rect = ctx.input().screen_rect(); + let screen_rect = ctx.screen_rect(); let bounds = if let Some(bounds) = self.drag_bounds { bounds.intersect(screen_rect) // protect against infinite bounds @@ -410,7 +410,7 @@ impl Prepared { state.size = content_ui.min_rect().size(); - ctx.memory().areas.set_state(layer_id, state); + ctx.memory_mut(|m| m.areas.set_state(layer_id, state)); move_response } @@ -418,7 +418,7 @@ impl Prepared { fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { if let Some(pointer_pos) = ctx.pointer_interact_pos() { - let any_pressed = ctx.input().pointer.any_pressed(); + let any_pressed = ctx.input(|i| i.pointer.any_pressed()); any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id) } else { false @@ -426,13 +426,13 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { } fn automatic_area_position(ctx: &Context) -> Pos2 { - let mut existing: Vec = ctx - .memory() - .areas - .visible_windows() - .into_iter() - .map(State::rect) - .collect(); + let mut existing: Vec = ctx.memory(|mem| { + mem.areas + .visible_windows() + .into_iter() + .map(State::rect) + .collect() + }); existing.sort_by_key(|r| r.left().round() as i32); let available_rect = ctx.available_rect(); diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index ea7d52de..c8803fc7 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -26,13 +26,14 @@ pub struct CollapsingState { impl CollapsingState { pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data() - .get_persisted::(id) - .map(|state| Self { id, state }) + ctx.data_mut(|d| { + d.get_persisted::(id) + .map(|state| Self { id, state }) + }) } pub fn store(&self, ctx: &Context) { - ctx.data().insert_persisted(self.id, self.state); + ctx.data_mut(|d| d.insert_persisted(self.id, self.state)); } pub fn id(&self) -> Id { @@ -64,7 +65,7 @@ impl CollapsingState { /// 0 for closed, 1 for open, with tweening pub fn openness(&self, ctx: &Context) -> f32 { - if ctx.memory().everything_is_visible() { + if ctx.memory(|mem| mem.everything_is_visible()) { 1.0 } else { ctx.animate_bool(self.id, self.state.open) diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 911f9424..8ede4d67 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -242,18 +242,13 @@ fn combo_box_dyn<'c, R>( ) -> InnerResponse> { let popup_id = button_id.with("popup"); - let is_popup_open = ui.memory().is_popup_open(popup_id); + let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id)); - let popup_height = ui - .ctx() - .memory() - .areas - .get(popup_id) - .map_or(100.0, |state| state.size.y); + let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y)); let above_or_below = if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height - < ui.ctx().input().screen_rect().bottom() + < ui.ctx().screen_rect().bottom() { AboveOrBelow::Below } else { @@ -334,7 +329,7 @@ fn combo_box_dyn<'c, R>( }); if button_response.clicked() { - ui.memory().toggle_popup(popup_id); + ui.memory_mut(|mem| mem.toggle_popup(popup_id)); } let inner = crate::popup::popup_above_or_below_widget( ui, diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index e4947db3..959ef595 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -28,7 +28,7 @@ pub struct PanelState { impl PanelState { pub fn load(ctx: &Context, bar_id: Id) -> Option { - ctx.data().get_persisted(bar_id) + ctx.data_mut(|d| d.get_persisted(bar_id)) } /// The size of the panel (from previous frame). @@ -37,7 +37,7 @@ impl PanelState { } fn store(self, ctx: &Context, bar_id: Id) { - ctx.data().insert_persisted(bar_id, self); + ctx.data_mut(|d| d.insert_persisted(bar_id, self)); } } @@ -245,11 +245,12 @@ impl SidePanel { && (resize_x - pointer.x).abs() <= ui.style().interaction.resize_grab_radius_side; - let any_pressed = ui.input().pointer.any_pressed(); // avoid deadlocks - if any_pressed && ui.input().pointer.any_down() && mouse_over_resize_line { - ui.memory().set_dragged_id(resize_id); + if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) + && mouse_over_resize_line + { + ui.memory_mut(|mem| mem.set_dragged_id(resize_id)); } - is_resizing = ui.memory().is_being_dragged(resize_id); + is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id)); if is_resizing { let width = (pointer.x - side.side_x(panel_rect)).abs(); let width = @@ -257,12 +258,12 @@ impl SidePanel { side.set_rect_width(&mut panel_rect, width); } - let any_down = ui.input().pointer.any_down(); // avoid deadlocks - let dragging_something_else = any_down || ui.input().pointer.any_pressed(); + let dragging_something_else = + ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed()); resize_hover = mouse_over_resize_line && !dragging_something_else; if resize_hover || is_resizing { - ui.output().cursor_icon = CursorIcon::ResizeHorizontal; + ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal); } } } @@ -334,19 +335,19 @@ impl SidePanel { let layer_id = LayerId::background(); let side = self.side; let available_rect = ctx.available_rect(); - let clip_rect = ctx.input().screen_rect(); + let clip_rect = ctx.screen_rect(); let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); let rect = inner_response.response.rect; match side { - Side::Left => ctx - .frame_state() - .allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)), - Side::Right => ctx - .frame_state() - .allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)), + Side::Left => ctx.frame_state_mut(|state| { + state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)); + }), + Side::Right => ctx.frame_state_mut(|state| { + state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)); + }), } inner_response } @@ -682,7 +683,7 @@ impl TopBottomPanel { let mut is_resizing = false; if resizable { let resize_id = id.with("__resize"); - let latest_pos = ui.input().pointer.latest_pos(); + let latest_pos = ui.input(|i| i.pointer.latest_pos()); if let Some(pointer) = latest_pos { let we_are_on_top = ui .ctx() @@ -695,13 +696,12 @@ impl TopBottomPanel { && (resize_y - pointer.y).abs() <= ui.style().interaction.resize_grab_radius_side; - if ui.input().pointer.any_pressed() - && ui.input().pointer.any_down() + if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) && mouse_over_resize_line { - ui.memory().interaction.drag_id = Some(resize_id); + ui.memory_mut(|mem| mem.interaction.drag_id = Some(resize_id)); } - is_resizing = ui.memory().interaction.drag_id == Some(resize_id); + is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id)); if is_resizing { let height = (pointer.y - side.side_y(panel_rect)).abs(); let height = clamp_to_range(height, height_range.clone()) @@ -709,12 +709,12 @@ impl TopBottomPanel { side.set_rect_height(&mut panel_rect, height); } - let any_down = ui.input().pointer.any_down(); // avoid deadlocks - let dragging_something_else = any_down || ui.input().pointer.any_pressed(); + let dragging_something_else = + ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed()); resize_hover = mouse_over_resize_line && !dragging_something_else; if resize_hover || is_resizing { - ui.output().cursor_icon = CursorIcon::ResizeVertical; + ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical); } } } @@ -787,7 +787,7 @@ impl TopBottomPanel { let available_rect = ctx.available_rect(); let side = self.side; - let clip_rect = ctx.input().screen_rect(); + let clip_rect = ctx.screen_rect(); let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); @@ -795,12 +795,14 @@ impl TopBottomPanel { match side { TopBottomSide::Top => { - ctx.frame_state() - .allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max)); + ctx.frame_state_mut(|state| { + state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max)); + }); } TopBottomSide::Bottom => { - ctx.frame_state() - .allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max)); + ctx.frame_state_mut(|state| { + state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max)); + }); } } @@ -1042,14 +1044,13 @@ impl CentralPanel { let layer_id = LayerId::background(); let id = Id::new("central_panel"); - let clip_rect = ctx.input().screen_rect(); + let clip_rect = ctx.screen_rect(); let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, available_rect, clip_rect); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); // Only inform ctx about what we actually used, so we can shrink the native window to fit. - ctx.frame_state() - .allocate_central_panel(inner_response.response.rect); + ctx.frame_state_mut(|state| state.allocate_central_panel(inner_response.response.rect)); inner_response } diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index add02b22..adb31ce6 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -13,11 +13,11 @@ pub(crate) struct TooltipState { impl TooltipState { pub fn load(ctx: &Context) -> Option { - ctx.data().get_temp(Id::null()) + ctx.data_mut(|d| d.get_temp(Id::null())) } fn store(self, ctx: &Context) { - ctx.data().insert_temp(Id::null(), self); + ctx.data_mut(|d| d.insert_temp(Id::null(), self)); } fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option { @@ -95,9 +95,7 @@ pub fn show_tooltip_at_pointer( add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { let suggested_pos = ctx - .input() - .pointer - .hover_pos() + .input(|i| i.pointer.hover_pos()) .map(|pointer_pos| pointer_pos + vec2(16.0, 16.0)); show_tooltip_at(ctx, id, suggested_pos, add_contents) } @@ -112,7 +110,7 @@ pub fn show_tooltip_for( add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { let expanded_rect = rect.expand2(vec2(2.0, 4.0)); - let (above, position) = if ctx.input().any_touches() { + let (above, position) = if ctx.input(|i| i.any_touches()) { (true, expanded_rect.left_top()) } else { (false, expanded_rect.left_bottom()) @@ -159,8 +157,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( // if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work. let mut frame_state = - ctx.frame_state() - .tooltip_state + ctx.frame_state(|fs| fs.tooltip_state) .unwrap_or(crate::frame_state::TooltipFrameState { common_id: individual_id, rect: Rect::NOTHING, @@ -176,7 +173,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( } } else if let Some(position) = suggested_position { position - } else if ctx.memory().everything_is_visible() { + } else if ctx.memory(|mem| mem.everything_is_visible()) { Pos2::ZERO } else { return None; // No good place for a tooltip :( @@ -191,7 +188,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( position.y -= expected_size.y; } - position = position.at_most(ctx.input().screen_rect().max - expected_size); + position = position.at_most(ctx.screen_rect().max - expected_size); // check if we intersect the avoid_rect { @@ -209,7 +206,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( } } - let position = position.at_least(ctx.input().screen_rect().min); + let position = position.at_least(ctx.screen_rect().min); let area_id = frame_state.common_id.with(frame_state.count); @@ -226,7 +223,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( frame_state.count += 1; frame_state.rect = frame_state.rect.union(response.rect); - ctx.frame_state().tooltip_state = Some(frame_state); + ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state)); Some(inner) } @@ -283,7 +280,7 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool { if *individual_id == tooltip_id { let area_id = common_id.with(count); let layer_id = LayerId::new(Order::Tooltip, area_id); - if ctx.memory().areas.visible_last_frame(&layer_id) { + if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) { return true; } } @@ -325,7 +322,7 @@ pub fn popup_below_widget( /// let response = ui.button("Open popup"); /// let popup_id = ui.make_persistent_id("my_unique_id"); /// if response.clicked() { -/// ui.memory().toggle_popup(popup_id); +/// ui.memory_mut(|mem| mem.toggle_popup(popup_id)); /// } /// let below = egui::AboveOrBelow::Below; /// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| { @@ -342,7 +339,7 @@ pub fn popup_above_or_below_widget( above_or_below: AboveOrBelow, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - if ui.memory().is_popup_open(popup_id) { + if ui.memory(|mem| mem.is_popup_open(popup_id)) { let (pos, pivot) = match above_or_below { AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM), AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP), @@ -370,8 +367,8 @@ pub fn popup_above_or_below_widget( }) .inner; - if ui.input().key_pressed(Key::Escape) || widget_response.clicked_elsewhere() { - ui.memory().close_popup(); + if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() { + ui.memory_mut(|mem| mem.close_popup()); } Some(inner) } else { diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 844fc758..befb51a1 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -18,11 +18,11 @@ pub(crate) struct State { impl State { pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data().get_persisted(id) + ctx.data_mut(|d| d.get_persisted(id)) } pub fn store(self, ctx: &Context, id: Id) { - ctx.data().insert_persisted(id, self); + ctx.data_mut(|d| d.insert_persisted(id, self)); } } @@ -180,7 +180,7 @@ impl Resize { .at_least(self.min_size) .at_most(self.max_size) .at_most( - ui.input().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows + ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows ); State { @@ -305,7 +305,7 @@ impl Resize { paint_resize_corner(ui, &corner_response); if corner_response.hovered() || corner_response.dragged() { - ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe; + ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe); } } diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 7af37e40..80158f52 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -48,11 +48,11 @@ impl Default for State { impl State { pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data().get_persisted(id) + ctx.data_mut(|d| d.get_persisted(id)) } pub fn store(self, ctx: &Context, id: Id) { - ctx.data().insert_persisted(id, self); + ctx.data_mut(|d| d.insert_persisted(id, self)); } } @@ -449,8 +449,10 @@ impl ScrollArea { if content_response.dragged() { for d in 0..2 { if has_bar[d] { - state.offset[d] -= ui.input().pointer.delta()[d]; - state.vel[d] = ui.input().pointer.velocity()[d]; + ui.input(|input| { + state.offset[d] -= input.pointer.delta()[d]; + state.vel[d] = input.pointer.velocity()[d]; + }); state.scroll_stuck_to_end[d] = false; } else { state.vel[d] = 0.0; @@ -459,7 +461,7 @@ impl ScrollArea { } else { let stop_speed = 20.0; // Pixels per second. let friction_coeff = 1000.0; // Pixels per second squared. - let dt = ui.input().unstable_dt; + let dt = ui.input(|i| i.unstable_dt); let friction = friction_coeff * dt; if friction > state.vel.length() || state.vel.length() < stop_speed { @@ -603,7 +605,9 @@ impl Prepared { for d in 0..2 { if has_bar[d] { // We take the scroll target so only this ScrollArea will use it: - let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take(); + let scroll_target = content_ui + .ctx() + .frame_state_mut(|state| state.scroll_target[d].take()); if let Some((scroll, align)) = scroll_target { let min = content_ui.min_rect().min[d]; let clip_rect = content_ui.clip_rect(); @@ -668,8 +672,7 @@ impl Prepared { if scrolling_enabled && ui.rect_contains_pointer(outer_rect) { for d in 0..2 { if has_bar[d] { - let mut frame_state = ui.ctx().frame_state(); - let scroll_delta = frame_state.scroll_delta; + let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta); let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0; let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0; @@ -677,7 +680,7 @@ impl Prepared { if scrolling_up || scrolling_down { state.offset[d] -= scroll_delta[d]; // Clear scroll delta so no parent scroll will use it. - frame_state.scroll_delta[d] = 0.0; + ui.ctx().frame_state_mut(|fs| fs.scroll_delta[d] = 0.0); state.scroll_stuck_to_end[d] = false; } } diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 1182e6a7..a236a5e3 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -301,7 +301,8 @@ impl<'open> Window<'open> { let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); - let is_open = !matches!(open, Some(false)) || ctx.memory().everything_is_visible(); + let is_explicitly_closed = matches!(open, Some(false)); + let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); area.show_open_close_animation(ctx, &frame, is_open); if !is_open { @@ -339,7 +340,7 @@ impl<'open> Window<'open> { // Calculate roughly how much larger the window size is compared to the inner rect let title_bar_height = if with_title_bar { let style = ctx.style(); - title.font_height(&ctx.fonts(), &style) + title_content_spacing + ctx.fonts(|f| title.font_height(f, &style)) + title_content_spacing } else { 0.0 }; @@ -425,7 +426,7 @@ impl<'open> Window<'open> { ctx.style().visuals.widgets.active, ); } else if let Some(hover_interaction) = hover_interaction { - if ctx.input().pointer.has_pointer() { + if ctx.input(|i| i.pointer.has_pointer()) { paint_frame_interaction( &mut area_content_ui, outer_rect, @@ -520,13 +521,13 @@ pub(crate) struct WindowInteraction { impl WindowInteraction { pub fn set_cursor(&self, ctx: &Context) { if (self.left && self.top) || (self.right && self.bottom) { - ctx.output().cursor_icon = CursorIcon::ResizeNwSe; + ctx.set_cursor_icon(CursorIcon::ResizeNwSe); } else if (self.right && self.top) || (self.left && self.bottom) { - ctx.output().cursor_icon = CursorIcon::ResizeNeSw; + ctx.set_cursor_icon(CursorIcon::ResizeNeSw); } else if self.left || self.right { - ctx.output().cursor_icon = CursorIcon::ResizeHorizontal; + ctx.set_cursor_icon(CursorIcon::ResizeHorizontal); } else if self.bottom || self.top { - ctx.output().cursor_icon = CursorIcon::ResizeVertical; + ctx.set_cursor_icon(CursorIcon::ResizeVertical); } } @@ -558,7 +559,7 @@ fn interact( } } - ctx.memory().areas.move_to_top(area_layer_id); + ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id)); Some(window_interaction) } @@ -566,11 +567,11 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction) window_interaction.set_cursor(ctx); // Only move/resize windows with primary mouse button: - if !ctx.input().pointer.primary_down() { + if !ctx.input(|i| i.pointer.primary_down()) { return None; } - let pointer_pos = ctx.input().pointer.interact_pos()?; + let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?; let mut rect = window_interaction.start_rect; // prevent drift if window_interaction.is_resize() { @@ -592,8 +593,8 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction) // but we want anything interactive in the window (e.g. slider) to steal // the drag from us. It is therefor important not to move the window the first frame, // but instead let other widgets to the steal. HACK. - if !ctx.input().pointer.any_pressed() { - let press_origin = ctx.input().pointer.press_origin()?; + if !ctx.input(|i| i.pointer.any_pressed()) { + let press_origin = ctx.input(|i| i.pointer.press_origin())?; let delta = pointer_pos - press_origin; rect = rect.translate(delta); } @@ -611,30 +612,31 @@ fn window_interaction( rect: Rect, ) -> Option { { - let drag_id = ctx.memory().interaction.drag_id; + let drag_id = ctx.memory(|mem| mem.interaction.drag_id); if drag_id.is_some() && drag_id != Some(id) { return None; } } - let mut window_interaction = { ctx.memory().window_interaction }; + let mut window_interaction = ctx.memory(|mem| mem.window_interaction); if window_interaction.is_none() { if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) { hover_window_interaction.set_cursor(ctx); - let any_pressed = ctx.input().pointer.any_pressed(); // avoid deadlocks - if any_pressed && ctx.input().pointer.primary_down() { - ctx.memory().interaction.drag_id = Some(id); - ctx.memory().interaction.drag_is_window = true; - window_interaction = Some(hover_window_interaction); - ctx.memory().window_interaction = window_interaction; + if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) { + ctx.memory_mut(|mem| { + mem.interaction.drag_id = Some(id); + mem.interaction.drag_is_window = true; + window_interaction = Some(hover_window_interaction); + mem.window_interaction = window_interaction; + }); } } } if let Some(window_interaction) = window_interaction { - let is_active = ctx.memory().interaction.drag_id == Some(id); + let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id)); if is_active && window_interaction.area_layer_id == area_layer_id { return Some(window_interaction); @@ -650,10 +652,9 @@ fn resize_hover( area_layer_id: LayerId, rect: Rect, ) -> Option { - let pointer = ctx.input().pointer.interact_pos()?; + let pointer = ctx.input(|i| i.pointer.interact_pos())?; - let any_down = ctx.input().pointer.any_down(); // avoid deadlocks - if any_down && !ctx.input().pointer.any_pressed() { + if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) { return None; // already dragging (something) } @@ -663,7 +664,7 @@ fn resize_hover( } } - if ctx.memory().interaction.drag_interest { + if ctx.memory(|mem| mem.interaction.drag_interest) { // Another widget will become active if we drag here return None; } @@ -825,8 +826,8 @@ fn show_title_bar( collapsible: bool, ) -> TitleBar { let inner_response = ui.horizontal(|ui| { - let height = title - .font_height(&ui.fonts(), ui.style()) + let height = ui + .fonts(|fonts| title.font_height(fonts, ui.style())) .max(ui.spacing().interact_size.y); ui.set_min_height(height); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index d94f9b79..f42690ee 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use crate::{ animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState, input_state::*, layers::GraphicLayers, memory::Options, os::OperatingSystem, - output::FullOutput, TextureHandle, *, + output::FullOutput, util::IdTypeMap, TextureHandle, *, }; use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *}; @@ -179,12 +179,26 @@ impl ContextImpl { /// [`Context`] is cheap to clone, and any clones refers to the same mutable data /// ([`Context`] uses refcounting internally). /// -/// All methods are marked `&self`; [`Context`] has interior mutability (protected by a mutex). +/// ## Locking +/// All methods are marked `&self`; [`Context`] has interior mutability protected by an [`RwLock`]. /// +/// To access parts of a `Context` you need to use some of the helper functions that take closures: /// -/// You can store +/// ``` +/// # let ctx = egui::Context::default(); +/// if ctx.input(|i| i.key_pressed(egui::Key::A)) { +/// ctx.output_mut(|o| o.copied_text = "Hello!".to_string()); +/// } +/// ``` /// -/// # Example: +/// Within such a closure you may NOT recursively lock the same [`Context`], as that can lead to a deadlock. +/// Therefore it is important that any lock of [`Context`] is short-lived. +/// +/// These are effectively transactional accesses. +/// +/// [`Ui`] has many of the same accessor functions, and the same applies there. +/// +/// ## Example: /// /// ``` no_run /// # fn handle_platform_output(_: egui::PlatformOutput) {} @@ -234,12 +248,14 @@ impl Default for Context { } impl Context { - fn read(&self) -> RwLockReadGuard<'_, ContextImpl> { - self.0.read() + // Do read-only (shared access) transaction on Context + fn read(&self, reader: impl FnOnce(&ContextImpl) -> R) -> R { + reader(&self.0.read()) } - fn write(&self) -> RwLockWriteGuard<'_, ContextImpl> { - self.0.write() + // Do read-write (exclusive access) transaction on Context + fn write(&self, writer: impl FnOnce(&mut ContextImpl) -> R) -> R { + writer(&mut self.0.write()) } /// Run the ui code for one frame. @@ -289,9 +305,150 @@ impl Context { /// // handle full_output /// ``` pub fn begin_frame(&self, new_input: RawInput) { - self.write().begin_frame_mut(new_input); + self.write(|ctx| ctx.begin_frame_mut(new_input)); + } +} + +/// ## Borrows parts of [`Context`] +/// These functions all lock the [`Context`]. +/// Please see the documentation of [`Context`] for how locking works! +impl Context { + /// Read-only access to [`InputState`]. + /// + /// Note that this locks the [`Context`]. + /// + /// ``` + /// # let mut ctx = egui::Context::default(); + /// ctx.input(|i| { + /// // ⚠️ Using `ctx` (even from other `Arc` reference) again here will lead to a dead-lock! + /// }); + /// + /// if let Some(pos) = ctx.input(|i| i.pointer.hover_pos()) { + /// // This is fine! + /// } + /// ``` + #[inline] + pub fn input(&self, reader: impl FnOnce(&InputState) -> R) -> R { + self.read(move |ctx| reader(&ctx.input)) } + /// Read-write access to [`InputState`]. + #[inline] + pub fn input_mut(&self, writer: impl FnOnce(&mut InputState) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.input)) + } + + /// Read-only access to [`Memory`]. + #[inline] + pub fn memory(&self, reader: impl FnOnce(&Memory) -> R) -> R { + self.read(move |ctx| reader(&ctx.memory)) + } + + /// Read-write access to [`Memory`]. + #[inline] + pub fn memory_mut(&self, writer: impl FnOnce(&mut Memory) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.memory)) + } + + /// Read-only access to [`IdTypeMap`], which stores superficial widget state. + #[inline] + pub fn data(&self, reader: impl FnOnce(&IdTypeMap) -> R) -> R { + self.read(move |ctx| reader(&ctx.memory.data)) + } + + /// Read-write access to [`IdTypeMap`], which stores superficial widget state. + #[inline] + pub fn data_mut(&self, writer: impl FnOnce(&mut IdTypeMap) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.memory.data)) + } + + /// Read-write access to [`GraphicLayers`], where painted [`crate::Shape`]s are written to. + #[inline] + pub(crate) fn graphics_mut(&self, writer: impl FnOnce(&mut GraphicLayers) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.graphics)) + } + + /// Read-only access to [`PlatformOutput`]. + /// + /// This is what egui outputs each frame. + /// + /// ``` + /// # let mut ctx = egui::Context::default(); + /// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress); + /// ``` + #[inline] + pub fn output(&self, reader: impl FnOnce(&PlatformOutput) -> R) -> R { + self.read(move |ctx| reader(&ctx.output)) + } + + /// Read-write access to [`PlatformOutput`]. + #[inline] + pub fn output_mut(&self, writer: impl FnOnce(&mut PlatformOutput) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.output)) + } + + /// Read-only access to [`FrameState`]. + #[inline] + pub(crate) fn frame_state(&self, reader: impl FnOnce(&FrameState) -> R) -> R { + self.read(move |ctx| reader(&ctx.frame_state)) + } + + /// Read-write access to [`FrameState`]. + #[inline] + pub(crate) fn frame_state_mut(&self, writer: impl FnOnce(&mut FrameState) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.frame_state)) + } + + /// Read-only access to [`Fonts`]. + /// + /// Not valid until first call to [`Context::run()`]. + /// That's because since we don't know the proper `pixels_per_point` until then. + #[inline] + pub fn fonts(&self, reader: impl FnOnce(&Fonts) -> R) -> R { + self.read(move |ctx| { + reader( + ctx.fonts + .as_ref() + .expect("No fonts available until first call to Context::run()"), + ) + }) + } + + /// Read-write access to [`Fonts`]. + #[inline] + pub fn fonts_mut(&self, writer: impl FnOnce(&mut Option) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.fonts)) + } + + /// Read-only access to [`Options`]. + #[inline] + pub fn options(&self, reader: impl FnOnce(&Options) -> R) -> R { + self.read(move |ctx| reader(&ctx.memory.options)) + } + + /// Read-write access to [`Options`]. + #[inline] + pub fn options_mut(&self, writer: impl FnOnce(&mut Options) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.memory.options)) + } + + /// Read-only access to [`TessellationOptions`]. + #[inline] + pub fn tessellation_options(&self, reader: impl FnOnce(&TessellationOptions) -> R) -> R { + self.read(move |ctx| reader(&ctx.memory.options.tessellation_options)) + } + + /// Read-write access to [`TessellationOptions`]. + #[inline] + pub fn tessellation_options_mut( + &self, + writer: impl FnOnce(&mut TessellationOptions) -> R, + ) -> R { + self.write(move |ctx| writer(&mut ctx.memory.options.tessellation_options)) + } +} + +impl Context { // --------------------------------------------------------------------- /// If the given [`Id`] has been used previously the same frame at at different position, @@ -304,7 +461,7 @@ impl Context { /// The most important thing is that [`Rect::min`] is approximately correct, /// because that's where the warning will be painted. If you don't know what size to pick, just pick [`Vec2::ZERO`]. pub fn check_for_id_clash(&self, id: Id, new_rect: Rect, what: &str) { - let prev_rect = self.frame_state().used_ids.insert(id, new_rect); + let prev_rect = self.frame_state_mut(move |state| state.used_ids.insert(id, new_rect)); if let Some(prev_rect) = prev_rect { // it is ok to reuse the same ID for e.g. a frame around a widget, // or to check for interaction with the same widget twice: @@ -320,7 +477,7 @@ impl Context { let painter = self.debug_painter(); painter.rect_stroke(widget_rect, 0.0, (1.0, color)); - let below = widget_rect.bottom() + 32.0 < self.input().screen_rect.bottom(); + let below = widget_rect.bottom() + 32.0 < self.input(|i| i.screen_rect.bottom()); let text_rect = if below { painter.debug_text( @@ -408,46 +565,45 @@ impl Context { ); } - let mut slf = self.write(); + self.write(|ctx| { + ctx.layer_rects_this_frame + .entry(layer_id) + .or_default() + .push((id, interact_rect)); - slf.layer_rects_this_frame - .entry(layer_id) - .or_default() - .push((id, interact_rect)); - - if hovered { - let pointer_pos = slf.input.pointer.interact_pos(); - if let Some(pointer_pos) = pointer_pos { - if let Some(rects) = slf.layer_rects_prev_frame.get(&layer_id) { - for &(prev_id, prev_rect) in rects.iter().rev() { - if prev_id == id { - break; // there is no other interactive widget covering us at the pointer position. - } - if prev_rect.contains(pointer_pos) { - // Another interactive widget is covering us at the pointer position, - // so we aren't hovered. - - if slf.memory.options.style.debug.show_blocking_widget { - drop(slf); - Self::layer_painter(self, LayerId::debug()).debug_rect( - interact_rect, - Color32::GREEN, - "Covered", - ); - Self::layer_painter(self, LayerId::debug()).debug_rect( - prev_rect, - Color32::LIGHT_BLUE, - "On top", - ); + if hovered { + let pointer_pos = ctx.input.pointer.interact_pos(); + if let Some(pointer_pos) = pointer_pos { + if let Some(rects) = ctx.layer_rects_prev_frame.get(&layer_id) { + for &(prev_id, prev_rect) in rects.iter().rev() { + if prev_id == id { + break; // there is no other interactive widget covering us at the pointer position. } + if prev_rect.contains(pointer_pos) { + // Another interactive widget is covering us at the pointer position, + // so we aren't hovered. - hovered = false; - break; + if ctx.memory.options.style.debug.show_blocking_widget { + Self::layer_painter(self, LayerId::debug()).debug_rect( + interact_rect, + Color32::GREEN, + "Covered", + ); + Self::layer_painter(self, LayerId::debug()).debug_rect( + prev_rect, + Color32::LIGHT_BLUE, + "On top", + ); + } + + hovered = false; + break; + } } } } } - } + }); } self.interact_with_hovered(layer_id, id, rect, sense, enabled, hovered) @@ -485,7 +641,7 @@ impl Context { if !enabled || !sense.focusable || !layer_id.allow_interaction() { // Not interested or allowed input: - self.memory().surrender_focus(id); + self.memory_mut(|mem| mem.surrender_focus(id)); return response; } @@ -496,116 +652,115 @@ impl Context { // Make sure anything that can receive focus has an AccessKit node. // TODO(mwcampbell): For nodes that are filled from widget info, // some information is written to the node twice. - if let Some(mut node) = self.accesskit_node(id) { - response.fill_accesskit_node_common(&mut node); - } + self.accesskit_node(id, |node| response.fill_accesskit_node_common(node)); } let clicked_elsewhere = response.clicked_elsewhere(); - let ctx_impl = &mut *self.write(); - let memory = &mut ctx_impl.memory; - let input = &mut ctx_impl.input; + self.write(|ctx| { + let memory = &mut ctx.memory; + let input = &mut ctx.input; - if sense.focusable { - memory.interested_in_focus(id); - } + if sense.focusable { + memory.interested_in_focus(id); + } - if sense.click - && memory.has_focus(response.id) - && (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter)) - { - // Space/enter works like a primary click for e.g. selected buttons - response.clicked[PointerButton::Primary as usize] = true; - } - - #[cfg(feature = "accesskit")] - { if sense.click - && input.has_accesskit_action_request(response.id, accesskit::Action::Default) + && memory.has_focus(response.id) + && (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter)) { + // Space/enter works like a primary click for e.g. selected buttons response.clicked[PointerButton::Primary as usize] = true; } - } - if sense.click || sense.drag { - memory.interaction.click_interest |= hovered && sense.click; - memory.interaction.drag_interest |= hovered && sense.drag; + #[cfg(feature = "accesskit")] + { + if sense.click + && input.has_accesskit_action_request(response.id, accesskit::Action::Default) + { + response.clicked[PointerButton::Primary as usize] = true; + } + } - response.dragged = memory.interaction.drag_id == Some(id); - response.is_pointer_button_down_on = - memory.interaction.click_id == Some(id) || response.dragged; + if sense.click || sense.drag { + memory.interaction.click_interest |= hovered && sense.click; + memory.interaction.drag_interest |= hovered && sense.drag; - for pointer_event in &input.pointer.pointer_events { - match pointer_event { - PointerEvent::Moved(_) => {} - PointerEvent::Pressed { .. } => { - if hovered { - if sense.click && memory.interaction.click_id.is_none() { - // potential start of a click - memory.interaction.click_id = Some(id); - response.is_pointer_button_down_on = true; - } + response.dragged = memory.interaction.drag_id == Some(id); + response.is_pointer_button_down_on = + memory.interaction.click_id == Some(id) || response.dragged; - // HACK: windows have low priority on dragging. - // This is so that if you drag a slider in a window, - // the slider will steal the drag away from the window. - // This is needed because we do window interaction first (to prevent frame delay), - // and then do content layout. - if sense.drag - && (memory.interaction.drag_id.is_none() - || memory.interaction.drag_is_window) - { - // potential start of a drag - memory.interaction.drag_id = Some(id); - memory.interaction.drag_is_window = false; - memory.window_interaction = None; // HACK: stop moving windows (if any) - response.is_pointer_button_down_on = true; - response.dragged = true; + for pointer_event in &input.pointer.pointer_events { + match pointer_event { + PointerEvent::Moved(_) => {} + PointerEvent::Pressed { .. } => { + if hovered { + if sense.click && memory.interaction.click_id.is_none() { + // potential start of a click + memory.interaction.click_id = Some(id); + response.is_pointer_button_down_on = true; + } + + // HACK: windows have low priority on dragging. + // This is so that if you drag a slider in a window, + // the slider will steal the drag away from the window. + // This is needed because we do window interaction first (to prevent frame delay), + // and then do content layout. + if sense.drag + && (memory.interaction.drag_id.is_none() + || memory.interaction.drag_is_window) + { + // potential start of a drag + memory.interaction.drag_id = Some(id); + memory.interaction.drag_is_window = false; + memory.window_interaction = None; // HACK: stop moving windows (if any) + response.is_pointer_button_down_on = true; + response.dragged = true; + } } } - } - PointerEvent::Released { click, button } => { - response.drag_released = response.dragged; - response.dragged = false; + PointerEvent::Released { click, button } => { + response.drag_released = response.dragged; + response.dragged = false; - if hovered && response.is_pointer_button_down_on { - if let Some(click) = click { - let clicked = hovered && response.is_pointer_button_down_on; - response.clicked[*button as usize] = clicked; - response.double_clicked[*button as usize] = - clicked && click.is_double(); - response.triple_clicked[*button as usize] = - clicked && click.is_triple(); + if hovered && response.is_pointer_button_down_on { + if let Some(click) = click { + let clicked = hovered && response.is_pointer_button_down_on; + response.clicked[*button as usize] = clicked; + response.double_clicked[*button as usize] = + clicked && click.is_double(); + response.triple_clicked[*button as usize] = + clicked && click.is_triple(); + } } } } } } - } - if response.is_pointer_button_down_on { - response.interact_pointer_pos = input.pointer.interact_pos(); - } + if response.is_pointer_button_down_on { + response.interact_pointer_pos = input.pointer.interact_pos(); + } - if input.pointer.any_down() { - response.hovered &= response.is_pointer_button_down_on; // we don't hover widgets while interacting with *other* widgets - } + if input.pointer.any_down() { + response.hovered &= response.is_pointer_button_down_on; // we don't hover widgets while interacting with *other* widgets + } - if memory.has_focus(response.id) && clicked_elsewhere { - memory.surrender_focus(id); - } + if memory.has_focus(response.id) && clicked_elsewhere { + memory.surrender_focus(id); + } - if response.dragged() && !memory.has_focus(response.id) { - // e.g.: remove focus from a widget when you drag something else - memory.stop_text_input(); - } + if response.dragged() && !memory.has_focus(response.id) { + // e.g.: remove focus from a widget when you drag something else + memory.stop_text_input(); + } + }); response } /// Get a full-screen painter for a new or existing layer pub fn layer_painter(&self, layer_id: LayerId) -> Painter { - let screen_rect = self.input().screen_rect(); + let screen_rect = self.screen_rect(); Painter::new(self.clone(), layer_id, screen_rect) } @@ -614,107 +769,6 @@ impl Context { Self::layer_painter(self, LayerId::debug()) } - /// How much space is still available after panels has been added. - /// This is the "background" area, what egui doesn't cover with panels (but may cover with windows). - /// This is also the area to which windows are constrained. - pub fn available_rect(&self) -> Rect { - self.frame_state().available_rect() - } -} - -/// ## Borrows parts of [`Context`] -impl Context { - /// Stores all the egui state. - /// - /// If you want to store/restore egui, serialize this. - #[inline] - pub fn memory(&self) -> RwLockWriteGuard<'_, Memory> { - RwLockWriteGuard::map(self.write(), |c| &mut c.memory) - } - - /// Stores superficial widget state. - #[inline] - pub fn data(&self) -> RwLockWriteGuard<'_, crate::util::IdTypeMap> { - RwLockWriteGuard::map(self.write(), |c| &mut c.memory.data) - } - - #[inline] - pub(crate) fn graphics(&self) -> RwLockWriteGuard<'_, GraphicLayers> { - RwLockWriteGuard::map(self.write(), |c| &mut c.graphics) - } - - /// What egui outputs each frame. - /// - /// ``` - /// # let mut ctx = egui::Context::default(); - /// ctx.output().cursor_icon = egui::CursorIcon::Progress; - /// ``` - #[inline] - pub fn output(&self) -> RwLockWriteGuard<'_, PlatformOutput> { - RwLockWriteGuard::map(self.write(), |c| &mut c.output) - } - - #[inline] - pub(crate) fn frame_state(&self) -> RwLockWriteGuard<'_, FrameState> { - RwLockWriteGuard::map(self.write(), |c| &mut c.frame_state) - } - - /// Access the [`InputState`]. - /// - /// Note that this locks the [`Context`], so be careful with if-let bindings: - /// - /// ``` - /// # let mut ctx = egui::Context::default(); - /// if let Some(pos) = ctx.input().pointer.hover_pos() { - /// // ⚠️ Using `ctx` again here will lead to a dead-lock! - /// } - /// - /// if let Some(pos) = { ctx.input().pointer.hover_pos() } { - /// // This is fine! - /// } - /// - /// let pos = ctx.input().pointer.hover_pos(); - /// if let Some(pos) = pos { - /// // This is fine! - /// } - /// ``` - #[inline] - pub fn input(&self) -> RwLockReadGuard<'_, InputState> { - RwLockReadGuard::map(self.read(), |c| &c.input) - } - - #[inline] - pub fn input_mut(&self) -> RwLockWriteGuard<'_, InputState> { - RwLockWriteGuard::map(self.write(), |c| &mut c.input) - } - - /// Not valid until first call to [`Context::run()`]. - /// That's because since we don't know the proper `pixels_per_point` until then. - #[inline] - pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> { - RwLockReadGuard::map(self.read(), |c| { - c.fonts - .as_ref() - .expect("No fonts available until first call to Context::run()") - }) - } - - #[inline] - fn fonts_mut(&self) -> RwLockWriteGuard<'_, Option> { - RwLockWriteGuard::map(self.write(), |c| &mut c.fonts) - } - - #[inline] - pub fn options(&self) -> RwLockWriteGuard<'_, Options> { - RwLockWriteGuard::map(self.write(), |c| &mut c.memory.options) - } - - /// Change the options used by the tessellator. - #[inline] - pub fn tessellation_options(&self) -> RwLockWriteGuard<'_, TessellationOptions> { - RwLockWriteGuard::map(self.write(), |c| &mut c.memory.options.tessellation_options) - } - /// What operating system are we running on? /// /// When compiling natively, this is @@ -723,7 +777,7 @@ impl Context { /// For web, this can be figured out from the user-agent, /// and is done so by [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe). pub fn os(&self) -> OperatingSystem { - self.read().os + self.read(|ctx| ctx.os) } /// Set the operating system we are running on. @@ -731,7 +785,18 @@ impl Context { /// If you are writing wasm-based integration for egui you /// may want to set this based on e.g. the user-agent. pub fn set_os(&self, os: OperatingSystem) { - self.write().os = os; + self.write(|ctx| ctx.os = os); + } + + /// Set the cursor icon. + /// + /// Equivalent to: + /// ``` + /// # let ctx = egui::Context::default(); + /// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::PointingHand); + /// ``` + pub fn set_cursor_icon(&self, cursor_icon: CursorIcon) { + self.output_mut(|o| o.cursor_icon = cursor_icon); } /// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`). @@ -752,13 +817,14 @@ impl Context { } = ModifierNames::SYMBOLS; let font_id = TextStyle::Body.resolve(&self.style()); - let fonts = self.fonts(); - let mut fonts = fonts.lock(); - let font = fonts.fonts.font(&font_id); - font.has_glyphs(alt) - && font.has_glyphs(ctrl) - && font.has_glyphs(shift) - && font.has_glyphs(mac_cmd) + self.fonts(|f| { + let mut lock = f.lock(); + let font = lock.fonts.font(&font_id); + font.has_glyphs(alt) + && font.has_glyphs(ctrl) + && font.has_glyphs(shift) + && font.has_glyphs(mac_cmd) + }) }; if is_mac && can_show_symbols() { @@ -767,9 +833,7 @@ impl Context { shortcut.format(&ModifierNames::NAMES, is_mac) } } -} -impl Context { /// Call this if there is need to repaint the UI, i.e. if you are showing an animation. /// /// If this is called at least once in a frame, then there will be another frame right after this. @@ -780,14 +844,15 @@ impl Context { /// (this will work on `eframe`). pub fn request_repaint(&self) { // request two frames of repaint, just to cover some corner cases (frame delays): - let mut ctx = self.write(); - ctx.repaint_requests = 2; - if let Some(callback) = &ctx.request_repaint_callback { - if !ctx.has_requested_repaint_this_frame { - (callback)(); - ctx.has_requested_repaint_this_frame = true; + self.write(|ctx| { + ctx.repaint_requests = 2; + if let Some(callback) = &ctx.request_repaint_callback { + if !ctx.has_requested_repaint_this_frame { + (callback)(); + ctx.has_requested_repaint_this_frame = true; + } } - } + }); } /// Request repaint after the specified duration elapses in the case of no new input @@ -819,8 +884,7 @@ impl Context { /// during app idle time where we are not receiving any new input events. pub fn request_repaint_after(&self, duration: std::time::Duration) { // Maybe we can check if duration is ZERO, and call self.request_repaint()? - let mut ctx = self.write(); - ctx.repaint_after = ctx.repaint_after.min(duration); + self.write(|ctx| ctx.repaint_after = ctx.repaint_after.min(duration)); } /// For integrations: this callback will be called when an egui user calls [`Self::request_repaint`]. @@ -830,7 +894,7 @@ impl Context { /// Note that only one callback can be set. Any new call overrides the previous callback. pub fn set_request_repaint_callback(&self, callback: impl Fn() + Send + Sync + 'static) { let callback = Box::new(callback); - self.write().request_repaint_callback = Some(callback); + self.write(|ctx| ctx.request_repaint_callback = Some(callback)); } /// Tell `egui` which fonts to use. @@ -840,19 +904,23 @@ impl Context { /// /// The new fonts will become active at the start of the next frame. pub fn set_fonts(&self, font_definitions: FontDefinitions) { - if let Some(current_fonts) = &*self.fonts_mut() { - // NOTE: this comparison is expensive since it checks TTF data for equality - if current_fonts.lock().fonts.definitions() == &font_definitions { - return; // no change - save us from reloading font textures + let update_fonts = self.fonts_mut(|fonts| { + if let Some(current_fonts) = fonts { + // NOTE: this comparison is expensive since it checks TTF data for equality + current_fonts.lock().fonts.definitions() != &font_definitions + } else { + true } - } + }); - self.memory().new_font_definitions = Some(font_definitions); + if update_fonts { + self.memory_mut(|mem| mem.new_font_definitions = Some(font_definitions)); + } } /// The [`Style`] used by all subsequent windows, panels etc. pub fn style(&self) -> Arc