diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d3f384b..7880f2fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,20 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [ ## Unreleased ### Added ⭐ +* Add right and bottom panels (`SidePanel::right` and `Panel::bottom`). +* Add resizable panels. +* Add an option to overwrite frame of a `Panel`. * Add `Style::override_text_style` to easily change the text style of everything in a `Ui` (or globally). * You can now change `TextStyle` on checkboxes, radio buttons and `SelectableLabel`. * Add support for [cint](https://crates.io/crates/cint) under `cint` feature. * Add features `extra_asserts` and `extra_debug_asserts` to enable additional checks. -* Add an option to overwrite frame of `SidePanel` and `TopPanel`. * `TextEdit` now supports edits on a generic buffer using `TextBuffer`. +### Changed 🔧 +* `TopPanel::top` is now `TopBottomPanel::top`. +* `SidePanel::left` no longet takes the default width by argument, but by a builder call. + + ## 0.12.0 - 2021-05-10 - Multitouch, user memory, window pivots, and improved plots ### Added ⭐ diff --git a/egui/src/containers/area.rs b/egui/src/containers/area.rs index 9c10cc01..bf90c255 100644 --- a/egui/src/containers/area.rs +++ b/egui/src/containers/area.rs @@ -282,7 +282,7 @@ impl Prepared { // (except in rare cases where they don't fit). // Adjust clip rect so we don't cast shadows on side panels: let central_area = ctx.available_rect(); - let is_within_central_area = central_area.contains(self.state.pos); + let is_within_central_area = central_area.contains_rect(self.state.rect().shrink(1.0)); if is_within_central_area { clip_rect = clip_rect.intersect(central_area); } diff --git a/egui/src/containers/mod.rs b/egui/src/containers/mod.rs index 7d7af7a7..49711d53 100644 --- a/egui/src/containers/mod.rs +++ b/egui/src/containers/mod.rs @@ -17,9 +17,12 @@ pub use { collapsing_header::*, combo_box::*, frame::Frame, - panel::{CentralPanel, SidePanel, TopPanel}, + panel::{CentralPanel, SidePanel, TopBottomPanel}, popup::*, resize::Resize, scroll_area::ScrollArea, window::Window, }; + +#[allow(deprecated)] +pub use panel::TopPanel; diff --git a/egui/src/containers/panel.rs b/egui/src/containers/panel.rs index dd3a4bfc..989c30b0 100644 --- a/egui/src/containers/panel.rs +++ b/egui/src/containers/panel.rs @@ -4,43 +4,131 @@ //! the only places where you can put you widgets. //! //! The order in which you add panels matter! +//! The first panel you add will always be the outermost, and the last you add will always be the innermost. //! -//! Add [`CentralPanel`] and [`Window`]:s last. +//! Always add any [`CentralPanel`] and [`Window`]:s last. + +use std::ops::RangeInclusive; use crate::*; +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +struct PanelState { + rect: Rect, +} + // ---------------------------------------------------------------------------- -/// A panel that covers the entire left side of the screen. +/// `Left` or `Right` +#[derive(Clone, Copy, Debug, PartialEq)] +enum Side { + Left, + Right, +} + +impl Side { + fn opposite(self) -> Self { + match self { + Side::Left => Self::Right, + Side::Right => Self::Left, + } + } + + fn set_rect_width(self, rect: &mut Rect, width: f32) { + match self { + Side::Left => rect.max.x = rect.min.x + width, + Side::Right => rect.min.x = rect.max.x - width, + } + } + + fn side_x(self, rect: Rect) -> f32 { + match self { + Side::Left => rect.left(), + Side::Right => rect.right(), + } + } +} + +/// A panel that covers the entire left or right side of the screen. /// -/// `SidePanel`s must be added before adding any [`CentralPanel`] or [`Window`]s. +/// The order in which you add panels matter! +/// The first panel you add will always be the outermost, and the last you add will always be the innermost. +/// +/// Always add any [`CentralPanel`] and [`Window`]:s last. /// /// ``` /// # let mut ctx = egui::CtxRef::default(); /// # ctx.begin_frame(Default::default()); /// # let ctx = &ctx; -/// egui::SidePanel::left("my_side_panel", 0.0).show(ctx, |ui| { +/// egui::SidePanel::left("my_left_panel").show(ctx, |ui| { /// ui.label("Hello World!"); /// }); /// ``` +/// +/// See also [`TopBottomPanel`]. #[must_use = "You should call .show()"] pub struct SidePanel { + side: Side, id: Id, - max_width: f32, frame: Option, + resizable: bool, + default_width: f32, + width_range: RangeInclusive, } impl SidePanel { - /// `id_source`: Something unique, e.g. `"my_side_panel"`. - /// The given `max_width` is a soft maximum (as always), and the actual panel may be smaller or larger. - pub fn left(id_source: impl std::hash::Hash, max_width: f32) -> Self { + /// `id_source`: Something unique, e.g. `"my_left_panel"`. + pub fn left(id_source: impl std::hash::Hash) -> Self { + Self::new(Side::Left, id_source) + } + + /// `id_source`: Something unique, e.g. `"my_right_panel"`. + pub fn right(id_source: impl std::hash::Hash) -> Self { + Self::new(Side::Right, id_source) + } + + /// `id_source`: Something unique, e.g. `"my_panel"`. + fn new(side: Side, id_source: impl std::hash::Hash) -> Self { Self { + side, id: Id::new(id_source), - max_width, frame: None, + resizable: true, + default_width: 200.0, + width_range: 96.0..=f32::INFINITY, } } + /// Switch resizable on/off. + /// Default is `true`. + pub fn resizable(mut self, resizable: bool) -> Self { + self.resizable = resizable; + self + } + + /// The initial wrapping width of the `SidePanel`. + pub fn default_width(mut self, default_width: f32) -> Self { + self.default_width = default_width; + self + } + + pub fn min_width(mut self, min_width: f32) -> Self { + self.width_range = min_width..=(*self.width_range.end()); + self + } + + pub fn max_width(mut self, max_width: f32) -> Self { + self.width_range = (*self.width_range.start())..=max_width; + self + } + + /// The allowable width range for resizable panels. + pub fn width_range(mut self, width_range: RangeInclusive) -> Self { + self.width_range = width_range; + self + } + /// Change the background color, margins, etc. pub fn frame(mut self, frame: Frame) -> Self { self.frame = Some(frame); @@ -55,16 +143,63 @@ impl SidePanel { add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { let Self { + side, id, - max_width, frame, + resizable, + default_width, + width_range, } = self; - let mut panel_rect = ctx.available_rect(); - panel_rect.max.x = panel_rect.max.x.at_most(panel_rect.min.x + max_width); - let layer_id = LayerId::background(); + let available_rect = ctx.available_rect(); + let mut panel_rect = available_rect; + { + let mut width = default_width; + if let Some(state) = ctx.memory().id_data.get::(&id) { + width = state.rect.width(); + } + width = clamp_to_range(width, width_range.clone()).at_most(available_rect.width()); + side.set_rect_width(&mut panel_rect, width); + } + + let mut resize_hover = false; + let mut is_resizing = false; + if resizable { + let resize_id = id.with("__resize"); + if let Some(pointer) = ctx.input().pointer.latest_pos() { + let resize_x = side.opposite().side_x(panel_rect); + let mouse_over_resize_line = panel_rect.y_range().contains(&pointer.y) + && (resize_x - pointer.x).abs() + <= ctx.style().interaction.resize_grab_radius_side; + + if ctx.input().pointer.any_pressed() + && ctx.input().pointer.any_down() + && mouse_over_resize_line + { + ctx.memory().interaction.drag_id = Some(resize_id); + } + is_resizing = ctx.memory().interaction.drag_id == Some(resize_id); + if is_resizing { + let width = (pointer.x - side.side_x(panel_rect)).abs(); + let width = clamp_to_range(width, width_range).at_most(available_rect.width()); + side.set_rect_width(&mut panel_rect, width); + } + + let we_are_on_top = ctx + .layer_id_at(pointer) + .map_or(true, |top_layer_id| top_layer_id == layer_id); + let dragging_something_else = + ctx.input().pointer.any_down() || ctx.input().pointer.any_pressed(); + resize_hover = mouse_over_resize_line && !dragging_something_else && we_are_on_top; + + if resize_hover || is_resizing { + ctx.output().cursor_icon = CursorIcon::ResizeHorizontal; + } + } + } + let clip_rect = ctx.input().screen_rect(); let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, panel_rect, clip_rect); @@ -74,9 +209,32 @@ impl SidePanel { add_contents(ui) }); - // Only inform ctx about what we actually used, so we can shrink the native window to fit. - ctx.frame_state() - .allocate_left_panel(inner_response.response.rect); + let rect = inner_response.response.rect; + ctx.memory().id_data.insert(id, PanelState { rect }); + + if resize_hover || is_resizing { + let stroke = if is_resizing { + ctx.style().visuals.widgets.active.bg_stroke + } else { + ctx.style().visuals.widgets.hovered.bg_stroke + }; + // draw on top of ALL panels so that the resize line won't be covered by subsequent panels + let resize_layer = LayerId::new(Order::PanelResizeLine, Id::new("panel_resize")); + let resize_x = side.opposite().side_x(rect); + let top = pos2(resize_x, rect.top()); + let bottom = pos2(resize_x, rect.bottom()); + ctx.layer_painter(resize_layer) + .line_segment([top, bottom], stroke); + } + + 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)), + } inner_response } @@ -84,37 +242,116 @@ impl SidePanel { // ---------------------------------------------------------------------------- -/// A panel that covers the entire top side of the screen. +/// `Top` or `Bottom` +#[derive(Clone, Copy, Debug, PartialEq)] +enum TopBottomSide { + Top, + Bottom, +} + +impl TopBottomSide { + fn opposite(self) -> Self { + match self { + TopBottomSide::Top => Self::Bottom, + TopBottomSide::Bottom => Self::Top, + } + } + + fn set_rect_height(self, rect: &mut Rect, height: f32) { + match self { + TopBottomSide::Top => rect.max.y = rect.min.y + height, + TopBottomSide::Bottom => rect.min.y = rect.max.y - height, + } + } + + fn side_y(self, rect: Rect) -> f32 { + match self { + TopBottomSide::Top => rect.top(), + TopBottomSide::Bottom => rect.bottom(), + } + } +} + +/// A panel that covers the entire top or bottom of the screen. /// -/// `TopPanel`s must be added before adding any [`CentralPanel`] or [`Window`]s. +/// The order in which you add panels matter! +/// The first panel you add will always be the outermost, and the last you add will always be the innermost. +/// +/// Always add any [`CentralPanel`] and [`Window`]:s last. /// /// ``` /// # let mut ctx = egui::CtxRef::default(); /// # ctx.begin_frame(Default::default()); /// # let ctx = &ctx; -/// egui::TopPanel::top("my_top_panel").show(ctx, |ui| { +/// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| { /// ui.label("Hello World!"); /// }); /// ``` +/// +/// See also [`SidePanel`]. #[must_use = "You should call .show()"] -pub struct TopPanel { +pub struct TopBottomPanel { + side: TopBottomSide, id: Id, - max_height: Option, frame: Option, + resizable: bool, + default_height: Option, + height_range: RangeInclusive, } -impl TopPanel { +impl TopBottomPanel { /// `id_source`: Something unique, e.g. `"my_top_panel"`. - /// Default height is that of `interact_size.y` (i.e. a button), - /// but the panel will expand as needed. pub fn top(id_source: impl std::hash::Hash) -> Self { + Self::new(TopBottomSide::Top, id_source) + } + + /// `id_source`: Something unique, e.g. `"my_bottom_panel"`. + pub fn bottom(id_source: impl std::hash::Hash) -> Self { + Self::new(TopBottomSide::Bottom, id_source) + } + + /// `id_source`: Something unique, e.g. `"my_panel"`. + fn new(side: TopBottomSide, id_source: impl std::hash::Hash) -> Self { Self { + side, id: Id::new(id_source), - max_height: None, frame: None, + resizable: false, + default_height: None, + height_range: 20.0..=f32::INFINITY, } } + /// Switch resizable on/off. + /// Default is `false`. + pub fn resizable(mut self, resizable: bool) -> Self { + self.resizable = resizable; + self + } + + /// The initial height of the `SidePanel`. + /// Defaults to [`style::Spacing::interact_size`].y. + pub fn default_height(mut self, default_height: f32) -> Self { + self.default_height = Some(default_height); + self + } + + pub fn min_height(mut self, min_height: f32) -> Self { + self.height_range = min_height..=(*self.height_range.end()); + self + } + + pub fn max_height(mut self, max_height: f32) -> Self { + self.height_range = (*self.height_range.start())..=max_height; + self + } + + /// The allowable height range for resizable panels. + pub fn height_range(mut self, height_range: RangeInclusive) -> Self { + self.height_range = height_range; + self + } + /// Change the background color, margins, etc. pub fn frame(mut self, frame: Frame) -> Self { self.frame = Some(frame); @@ -122,24 +359,73 @@ impl TopPanel { } } -impl TopPanel { +impl TopBottomPanel { pub fn show( self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { let Self { + side, id, - max_height, frame, + resizable, + default_height, + height_range, } = self; - let max_height = max_height.unwrap_or_else(|| ctx.style().spacing.interact_size.y); - - let mut panel_rect = ctx.available_rect(); - panel_rect.max.y = panel_rect.max.y.at_most(panel_rect.min.y + max_height); let layer_id = LayerId::background(); + let available_rect = ctx.available_rect(); + let mut panel_rect = available_rect; + { + let state = ctx.memory().id_data.get::(&id).copied(); + let mut height = if let Some(state) = state { + state.rect.height() + } else { + default_height.unwrap_or_else(|| ctx.style().spacing.interact_size.y) + }; + height = clamp_to_range(height, height_range.clone()).at_most(available_rect.height()); + side.set_rect_height(&mut panel_rect, height); + } + + let mut resize_hover = false; + let mut is_resizing = false; + if resizable { + let resize_id = id.with("__resize"); + if let Some(pointer) = ctx.input().pointer.latest_pos() { + let resize_y = side.opposite().side_y(panel_rect); + let mouse_over_resize_line = panel_rect.x_range().contains(&pointer.x) + && (resize_y - pointer.y).abs() + <= ctx.style().interaction.resize_grab_radius_side; + + if ctx.input().pointer.any_pressed() + && ctx.input().pointer.any_down() + && mouse_over_resize_line + { + ctx.memory().interaction.drag_id = Some(resize_id); + } + is_resizing = ctx.memory().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).at_most(available_rect.height()); + side.set_rect_height(&mut panel_rect, height); + } + + let we_are_on_top = ctx + .layer_id_at(pointer) + .map_or(true, |top_layer_id| top_layer_id == layer_id); + let dragging_something_else = + ctx.input().pointer.any_down() || ctx.input().pointer.any_pressed(); + resize_hover = mouse_over_resize_line && !dragging_something_else && we_are_on_top; + + if resize_hover || is_resizing { + ctx.output().cursor_icon = CursorIcon::ResizeVertical; + } + } + } + let clip_rect = ctx.input().screen_rect(); let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, panel_rect, clip_rect); @@ -149,9 +435,34 @@ impl TopPanel { add_contents(ui) }); - // Only inform ctx about what we actually used, so we can shrink the native window to fit. - ctx.frame_state() - .allocate_top_panel(inner_response.response.rect); + let rect = inner_response.response.rect; + ctx.memory().id_data.insert(id, PanelState { rect }); + + if resize_hover || is_resizing { + let stroke = if is_resizing { + ctx.style().visuals.widgets.active.bg_stroke + } else { + ctx.style().visuals.widgets.hovered.bg_stroke + }; + // draw on top of ALL panels so that the resize line won't be covered by subsequent panels + let resize_layer = LayerId::new(Order::PanelResizeLine, Id::new("panel_resize")); + let resize_y = side.opposite().side_y(rect); + let left = pos2(rect.left(), resize_y); + let right = pos2(rect.right(), resize_y); + ctx.layer_painter(resize_layer) + .line_segment([left, right], stroke); + } + + match side { + TopBottomSide::Top => { + ctx.frame_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)); + } + } inner_response } @@ -159,6 +470,19 @@ impl TopPanel { // ---------------------------------------------------------------------------- +#[deprecated = "Use TopBottomPanel::top instead"] +pub struct TopPanel {} + +#[allow(deprecated)] +impl TopPanel { + #[deprecated = "Use TopBottomPanel::top instead"] + pub fn top(id_source: impl std::hash::Hash) -> TopBottomPanel { + TopBottomPanel::top(id_source) + } +} + +// ---------------------------------------------------------------------------- + /// A panel that covers the remainder of the screen, /// i.e. whatever area is left after adding other panels. /// @@ -216,3 +540,10 @@ impl CentralPanel { inner_response } } + +fn clamp_to_range(x: f32, range: RangeInclusive) -> f32 { + x.clamp( + range.start().min(*range.end()), + range.start().max(*range.end()), + ) +} diff --git a/egui/src/context.rs b/egui/src/context.rs index 8b20492b..00b5b6e6 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -97,7 +97,7 @@ impl CtxRef { /// This will modify the internal reference to point to a new generation of [`Context`]. /// Any old clones of this [`CtxRef`] will refer to the old [`Context`], which will not get new input. /// - /// Put your widgets into a [`SidePanel`], [`TopPanel`], [`CentralPanel`], [`Window`] or [`Area`]. + /// Put your widgets into a [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`]. pub fn begin_frame(&mut self, new_input: RawInput) { let mut self_: Context = (*self.0).clone(); self_.begin_frame_mut(new_input); @@ -291,8 +291,14 @@ impl CtxRef { response } + /// Get a full-screen painter for a new or existing layer + pub fn layer_painter(&self, layer_id: LayerId) -> Painter { + Painter::new(self.clone(), layer_id, self.input.screen_rect()) + } + + /// Paint on top of everything else pub fn debug_painter(&self) -> Painter { - Painter::new(self.clone(), LayerId::debug(), self.input.screen_rect()) + Self::layer_painter(self, LayerId::debug()) } } diff --git a/egui/src/frame_state.rs b/egui/src/frame_state.rs index 337ac979..79b8d1f6 100644 --- a/egui/src/frame_state.rs +++ b/egui/src/frame_state.rs @@ -88,6 +88,17 @@ impl FrameState { self.used_by_panels = self.used_by_panels.union(panel_rect); } + /// Shrink `available_rect`. + pub(crate) fn allocate_right_panel(&mut self, panel_rect: Rect) { + crate::egui_assert!( + panel_rect.max.distance(self.available_rect.max) < 0.1, + "Mismatching right panel. You must not create a panel from within another panel." + ); + self.available_rect.max.x = panel_rect.min.x; + self.unused_rect.max.x = panel_rect.min.x; + self.used_by_panels = self.used_by_panels.union(panel_rect); + } + /// Shrink `available_rect`. pub(crate) fn allocate_top_panel(&mut self, panel_rect: Rect) { crate::egui_assert!( @@ -99,6 +110,17 @@ impl FrameState { self.used_by_panels = self.used_by_panels.union(panel_rect); } + /// Shrink `available_rect`. + pub(crate) fn allocate_bottom_panel(&mut self, panel_rect: Rect) { + crate::egui_assert!( + panel_rect.max.distance(self.available_rect.max) < 0.1, + "Mismatching bottom panel. You must not create a panel from within another panel." + ); + self.available_rect.max.y = panel_rect.min.y; + self.unused_rect.max.y = panel_rect.min.y; + self.used_by_panels = self.used_by_panels.union(panel_rect); + } + pub(crate) fn allocate_central_panel(&mut self, panel_rect: Rect) { // Note: we do not shrink `available_rect`, because // we allow windows to cover the CentralPanel. diff --git a/egui/src/layers.rs b/egui/src/layers.rs index 10cd68cc..2b033b74 100644 --- a/egui/src/layers.rs +++ b/egui/src/layers.rs @@ -13,6 +13,8 @@ use std::sync::Arc; pub enum Order { /// Painted behind all floating windows Background, + /// Special layer between panels and windows + PanelResizeLine, /// Normal moveable windows that you reorder by click Middle, /// Popups, menus etc that should always be painted on top of windows @@ -25,9 +27,10 @@ pub enum Order { Debug, } impl Order { - const COUNT: usize = 5; + const COUNT: usize = 6; const ALL: [Order; Self::COUNT] = [ Self::Background, + Self::PanelResizeLine, Self::Middle, Self::Foreground, Self::Tooltip, @@ -37,7 +40,11 @@ impl Order { #[inline(always)] pub fn allow_interaction(&self) -> bool { match self { - Self::Background | Self::Middle | Self::Foreground | Self::Debug => true, + Self::Background + | Self::PanelResizeLine + | Self::Middle + | Self::Foreground + | Self::Debug => true, Self::Tooltip => false, } } diff --git a/egui/src/lib.rs b/egui/src/lib.rs index 5fb2651e..3df09074 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -41,7 +41,7 @@ //! //! ### Getting a [`Ui`] //! -//! Use one of [`SidePanel`], [`TopPanel`], [`CentralPanel`], [`Window`] or [`Area`] to +//! Use one of [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`] to //! get access to an [`Ui`] where you can put widgets. For example: //! //! ``` diff --git a/egui/src/menu.rs b/egui/src/menu.rs index f3c01c44..e2bdd4af 100644 --- a/egui/src/menu.rs +++ b/egui/src/menu.rs @@ -36,7 +36,7 @@ impl BarState { } } -/// The menu bar goes well in `TopPanel`, +/// The menu bar goes well in a [`TopBottomPanel::top`], /// but can also be placed in a `Window`. /// In the latter case you may want to wrap it in `Frame`. pub fn bar(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { diff --git a/egui/src/ui.rs b/egui/src/ui.rs index e7e34868..9e81b302 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -63,7 +63,7 @@ impl Ui { /// Create a new `Ui`. /// /// Normally you would not use this directly, but instead use - /// [`SidePanel`], [`TopPanel`], [`CentralPanel`], [`Window`] or [`Area`]. + /// [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`]. pub fn new(ctx: CtxRef, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self { let style = ctx.style(); Ui { diff --git a/egui_demo_lib/src/apps/demo/demo_app_windows.rs b/egui_demo_lib/src/apps/demo/demo_app_windows.rs index 36e8c52a..3f72a1fd 100644 --- a/egui_demo_lib/src/apps/demo/demo_app_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_app_windows.rs @@ -152,47 +152,50 @@ impl DemoWindows { egui_windows, } = self; - egui::SidePanel::left("side_panel", 190.0).show(ctx, |ui| { - ui.vertical_centered(|ui| { - ui.heading("✒ egui demos"); - }); - - ui.separator(); - - ScrollArea::auto_sized().show(ui, |ui| { - use egui::special_emojis::{GITHUB, OS_APPLE, OS_LINUX, OS_WINDOWS}; - - ui.label("egui is an immediate mode GUI library written in Rust."); - - ui.label(format!( - "egui runs on the web, or natively on {}{}{}", - OS_APPLE, OS_LINUX, OS_WINDOWS, - )); - + egui::SidePanel::right("egui_demo_panel") + .min_width(150.0) + .default_width(190.0) + .show(ctx, |ui| { ui.vertical_centered(|ui| { - ui.hyperlink_to( - format!("{} egui home page", GITHUB), - "https://github.com/emilk/egui", - ); + ui.heading("✒ egui demos"); }); - ui.separator(); - demos.checkboxes(ui); - ui.separator(); - tests.checkboxes(ui); - ui.separator(); - egui_windows.checkboxes(ui); ui.separator(); - ui.vertical_centered(|ui| { - if ui.button("Organize windows").clicked() { - ui.ctx().memory().reset_areas(); - } + ScrollArea::auto_sized().show(ui, |ui| { + use egui::special_emojis::{GITHUB, OS_APPLE, OS_LINUX, OS_WINDOWS}; + + ui.label("egui is an immediate mode GUI library written in Rust."); + + ui.label(format!( + "egui runs on the web, or natively on {}{}{}", + OS_APPLE, OS_LINUX, OS_WINDOWS, + )); + + ui.vertical_centered(|ui| { + ui.hyperlink_to( + format!("{} egui home page", GITHUB), + "https://github.com/emilk/egui", + ); + }); + + ui.separator(); + demos.checkboxes(ui); + ui.separator(); + tests.checkboxes(ui); + ui.separator(); + egui_windows.checkboxes(ui); + ui.separator(); + + ui.vertical_centered(|ui| { + if ui.button("Organize windows").clicked() { + ui.ctx().memory().reset_areas(); + } + }); }); }); - }); - egui::TopPanel::top("menu_bar").show(ctx, |ui| { + egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { show_menu_bar(ui); }); @@ -302,7 +305,7 @@ fn show_menu_bar(ui: &mut Ui) { } if ui .button("Clear egui memory") - .on_hover_text("Forget scroll, collapsing headers etc") + .on_hover_text("Forget scroll, positions, sizes etc") .clicked() { *ui.ctx().memory() = Default::default(); diff --git a/egui_demo_lib/src/apps/http_app.rs b/egui_demo_lib/src/apps/http_app.rs index 455da8af..4a23538b 100644 --- a/egui_demo_lib/src/apps/http_app.rs +++ b/egui_demo_lib/src/apps/http_app.rs @@ -61,7 +61,7 @@ impl epi::App for HttpApp { } /// Called each time the UI needs repainting, which may be many times per second. - /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. + /// Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`. fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) { if let Some(receiver) = &mut self.in_progress { // Are we there yet? diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index 8f7c080f..760283c2 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -74,49 +74,14 @@ impl epi::App for WrapApp { self.selected_anchor = self.apps.iter_mut().next().unwrap().0.to_owned(); } - egui::TopPanel::top("wrap_app_top_bar").show(ctx, |ui| { - // A menu-bar is a horizontal layout with some special styles applied. - // egui::menu::bar(ui, |ui| { - ui.horizontal_wrapped(|ui| { - dark_light_mode_switch(ui); - - ui.checkbox(&mut self.backend_panel.open, "💻 Backend"); - ui.separator(); - - for (anchor, app) in self.apps.iter_mut() { - if ui - .selectable_label(self.selected_anchor == anchor, app.name()) - .clicked() - { - self.selected_anchor = anchor.to_owned(); - if frame.is_web() { - ui.output().open_url(format!("#{}", anchor)); - } - } - } - - ui.with_layout(egui::Layout::right_to_left(), |ui| { - if false { - // TODO: fix the overlap on small screens - if let Some(seconds_since_midnight) = frame.info().seconds_since_midnight { - if clock_button(ui, seconds_since_midnight).clicked() { - self.selected_anchor = "clock".to_owned(); - if frame.is_web() { - ui.output().open_url("#clock"); - } - } - } - } - - egui::warn_if_debug_build(ui); - }); - }); + egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| { + self.bar_contents(ui, frame); }); self.backend_panel.update(ctx, frame); if self.backend_panel.open || ctx.memory().everything_is_visible() { - egui::SidePanel::left("backend_panel", 150.0).show(ctx, |ui| { + egui::SidePanel::left("backend_panel").show(ctx, |ui| { self.backend_panel.ui(ui, frame); }); } @@ -131,6 +96,47 @@ impl epi::App for WrapApp { } } +impl WrapApp { + fn bar_contents(&mut self, ui: &mut egui::Ui, frame: &mut epi::Frame<'_>) { + // A menu-bar is a horizontal layout with some special styles applied. + // egui::menu::bar(ui, |ui| { + ui.horizontal_wrapped(|ui| { + dark_light_mode_switch(ui); + + ui.checkbox(&mut self.backend_panel.open, "💻 Backend"); + ui.separator(); + + for (anchor, app) in self.apps.iter_mut() { + if ui + .selectable_label(self.selected_anchor == anchor, app.name()) + .clicked() + { + self.selected_anchor = anchor.to_owned(); + if frame.is_web() { + ui.output().open_url(format!("#{}", anchor)); + } + } + } + + ui.with_layout(egui::Layout::right_to_left(), |ui| { + if false { + // TODO: fix the overlap on small screens + if let Some(seconds_since_midnight) = frame.info().seconds_since_midnight { + if clock_button(ui, seconds_since_midnight).clicked() { + self.selected_anchor = "clock".to_owned(); + if frame.is_web() { + ui.output().open_url("#clock"); + } + } + } + } + + egui::warn_if_debug_build(ui); + }); + }); + } +} + fn clock_button(ui: &mut egui::Ui, seconds_since_midnight: f64) -> egui::Response { let time = seconds_since_midnight; let time = format!( @@ -256,10 +262,21 @@ impl BackendPanel { } fn ui(&mut self, ui: &mut egui::Ui, frame: &mut epi::Frame<'_>) { - ui.heading("💻 Backend"); + ui.vertical_centered(|ui| { + ui.heading("💻 Backend"); + }); + ui.separator(); self.run_mode_ui(ui); + if ui + .button("Clear egui memory") + .on_hover_text("Forget scroll, positions, sizes etc") + .clicked() + { + *ui.ctx().memory() = Default::default(); + } + ui.separator(); self.frame_history.ui(ui); @@ -274,6 +291,15 @@ impl BackendPanel { } } + if !frame.is_web() + && ui + .button("📱 Phone Size") + .on_hover_text("Resize the window to be small like a phone.") + .clicked() + { + frame.set_window_size(egui::Vec2::new(375.0, 812.0)); // iPhone 12 mini + } + ui.separator(); if frame.is_web() { @@ -299,17 +325,6 @@ impl BackendPanel { if !ui.ctx().is_using_pointer() { self.max_size_points_active = self.max_size_points_ui; } - } else { - if ui - .button("📱 Phone Size") - .on_hover_text("Resize the window to be small like a phone.") - .clicked() - { - frame.set_window_size(egui::Vec2::new(375.0, 812.0)); // iPhone 12 mini - } - if ui.button("Quit").clicked() { - frame.quit(); - } } let mut screen_reader = ui.ctx().memory().options.screen_reader; @@ -328,6 +343,13 @@ impl BackendPanel { ui.label(format!("{:?}", event)); } }); + + if !frame.is_web() { + ui.separator(); + if ui.button("Quit").clicked() { + frame.quit(); + } + } } fn pixels_per_point_ui( @@ -347,6 +369,7 @@ impl BackendPanel { ui.add( egui::Slider::new(pixels_per_point, 0.5..=5.0) .logarithmic(true) + .clamp_to_range(true) .text("Scale"), ) .on_hover_text("Physical pixels per point."); diff --git a/egui_glium/examples/pure.rs b/egui_glium/examples/pure.rs index 2b05247a..759a4247 100644 --- a/egui_glium/examples/pure.rs +++ b/egui_glium/examples/pure.rs @@ -31,7 +31,7 @@ fn main() { let mut quit = false; - egui::SidePanel::left("my_side_panel", 300.0).show(egui.ctx(), |ui| { + egui::SidePanel::left("my_side_panel").show(egui.ctx(), |ui| { ui.heading("Hello World!"); if ui.button("Quit").clicked() { quit = true; diff --git a/emath/src/rect.rs b/emath/src/rect.rs index 1073c22b..d6131010 100644 --- a/emath/src/rect.rs +++ b/emath/src/rect.rs @@ -202,6 +202,11 @@ impl Rect { self.min.x <= p.x && p.x <= self.max.x && self.min.y <= p.y && p.y <= self.max.y } + #[must_use] + pub fn contains_rect(&self, other: Rect) -> bool { + self.contains(other.min) && self.contains(other.max) + } + /// Return the given points clamped to be inside the rectangle /// Panics if [`Self::is_negative`]. #[must_use] diff --git a/epi/src/lib.rs b/epi/src/lib.rs index 741427da..560324bc 100644 --- a/epi/src/lib.rs +++ b/epi/src/lib.rs @@ -91,7 +91,7 @@ pub use egui; // Re-export for user convenience /// and deployed as a web site using the [`egui_web`](https://crates.io/crates/egui_web) crate. pub trait App { /// Called each time the UI needs repainting, which may be many times per second. - /// Put your widgets into a [`egui::SidePanel`], [`egui::TopPanel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`]. + /// Put your widgets into a [`egui::SidePanel`], [`egui::TopBottomPanel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`]. fn update(&mut self, ctx: &egui::CtxRef, frame: &mut Frame<'_>); /// Called once before the first frame.