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.