Resizable panels + right and bottom panels (#438)

* Side panel resizing and add right panels

* Add resizable top/bottom panels

* Deprecate TopPanel

* Final tweaks and update CHANGELOG.md
This commit is contained in:
Emil Ernerfeldt 2021-05-26 22:06:10 +02:00 committed by GitHub
parent 196ddff499
commit c9766f8a7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 538 additions and 131 deletions

View file

@ -8,13 +8,20 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
## Unreleased ## Unreleased
### Added ⭐ ### 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). * 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`. * You can now change `TextStyle` on checkboxes, radio buttons and `SelectableLabel`.
* Add support for [cint](https://crates.io/crates/cint) under `cint` feature. * 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 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`. * `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 ## 0.12.0 - 2021-05-10 - Multitouch, user memory, window pivots, and improved plots
### Added ⭐ ### Added ⭐

View file

@ -282,7 +282,7 @@ impl Prepared {
// (except in rare cases where they don't fit). // (except in rare cases where they don't fit).
// Adjust clip rect so we don't cast shadows on side panels: // Adjust clip rect so we don't cast shadows on side panels:
let central_area = ctx.available_rect(); 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 { if is_within_central_area {
clip_rect = clip_rect.intersect(central_area); clip_rect = clip_rect.intersect(central_area);
} }

View file

@ -17,9 +17,12 @@ pub use {
collapsing_header::*, collapsing_header::*,
combo_box::*, combo_box::*,
frame::Frame, frame::Frame,
panel::{CentralPanel, SidePanel, TopPanel}, panel::{CentralPanel, SidePanel, TopBottomPanel},
popup::*, popup::*,
resize::Resize, resize::Resize,
scroll_area::ScrollArea, scroll_area::ScrollArea,
window::Window, window::Window,
}; };
#[allow(deprecated)]
pub use panel::TopPanel;

View file

@ -4,43 +4,131 @@
//! the only places where you can put you widgets. //! the only places where you can put you widgets.
//! //!
//! The order in which you add panels matter! //! 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::*; 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(); /// # let mut ctx = egui::CtxRef::default();
/// # ctx.begin_frame(Default::default()); /// # ctx.begin_frame(Default::default());
/// # let ctx = &ctx; /// # 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!"); /// ui.label("Hello World!");
/// }); /// });
/// ``` /// ```
///
/// See also [`TopBottomPanel`].
#[must_use = "You should call .show()"] #[must_use = "You should call .show()"]
pub struct SidePanel { pub struct SidePanel {
side: Side,
id: Id, id: Id,
max_width: f32,
frame: Option<Frame>, frame: Option<Frame>,
resizable: bool,
default_width: f32,
width_range: RangeInclusive<f32>,
} }
impl SidePanel { impl SidePanel {
/// `id_source`: Something unique, e.g. `"my_side_panel"`. /// `id_source`: Something unique, e.g. `"my_left_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) -> Self {
pub fn left(id_source: impl std::hash::Hash, max_width: f32) -> 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 { Self {
side,
id: Id::new(id_source), id: Id::new(id_source),
max_width,
frame: None, 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<f32>) -> Self {
self.width_range = width_range;
self
}
/// Change the background color, margins, etc. /// Change the background color, margins, etc.
pub fn frame(mut self, frame: Frame) -> Self { pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame); self.frame = Some(frame);
@ -55,16 +143,63 @@ impl SidePanel {
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> { ) -> InnerResponse<R> {
let Self { let Self {
side,
id, id,
max_width,
frame, frame,
resizable,
default_width,
width_range,
} = self; } = 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 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::<PanelState>(&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 clip_rect = ctx.input().screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, panel_rect, clip_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) add_contents(ui)
}); });
// Only inform ctx about what we actually used, so we can shrink the native window to fit. let rect = inner_response.response.rect;
ctx.frame_state() ctx.memory().id_data.insert(id, PanelState { rect });
.allocate_left_panel(inner_response.response.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 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(); /// # let mut ctx = egui::CtxRef::default();
/// # ctx.begin_frame(Default::default()); /// # ctx.begin_frame(Default::default());
/// # let ctx = &ctx; /// # let ctx = &ctx;
/// egui::TopPanel::top("my_top_panel").show(ctx, |ui| { /// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {
/// ui.label("Hello World!"); /// ui.label("Hello World!");
/// }); /// });
/// ``` /// ```
///
/// See also [`SidePanel`].
#[must_use = "You should call .show()"] #[must_use = "You should call .show()"]
pub struct TopPanel { pub struct TopBottomPanel {
side: TopBottomSide,
id: Id, id: Id,
max_height: Option<f32>,
frame: Option<Frame>, frame: Option<Frame>,
resizable: bool,
default_height: Option<f32>,
height_range: RangeInclusive<f32>,
} }
impl TopPanel { impl TopBottomPanel {
/// `id_source`: Something unique, e.g. `"my_top_panel"`. /// `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 { 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 { Self {
side,
id: Id::new(id_source), id: Id::new(id_source),
max_height: None,
frame: 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<f32>) -> Self {
self.height_range = height_range;
self
}
/// Change the background color, margins, etc. /// Change the background color, margins, etc.
pub fn frame(mut self, frame: Frame) -> Self { pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame); self.frame = Some(frame);
@ -122,24 +359,73 @@ impl TopPanel {
} }
} }
impl TopPanel { impl TopBottomPanel {
pub fn show<R>( pub fn show<R>(
self, self,
ctx: &CtxRef, ctx: &CtxRef,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> { ) -> InnerResponse<R> {
let Self { let Self {
side,
id, id,
max_height,
frame, frame,
resizable,
default_height,
height_range,
} = self; } = 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 layer_id = LayerId::background();
let available_rect = ctx.available_rect();
let mut panel_rect = available_rect;
{
let state = ctx.memory().id_data.get::<PanelState>(&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 clip_rect = ctx.input().screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, panel_rect, clip_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) add_contents(ui)
}); });
// Only inform ctx about what we actually used, so we can shrink the native window to fit. let rect = inner_response.response.rect;
ctx.frame_state() ctx.memory().id_data.insert(id, PanelState { rect });
.allocate_top_panel(inner_response.response.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 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, /// A panel that covers the remainder of the screen,
/// i.e. whatever area is left after adding other panels. /// i.e. whatever area is left after adding other panels.
/// ///
@ -216,3 +540,10 @@ impl CentralPanel {
inner_response inner_response
} }
} }
fn clamp_to_range(x: f32, range: RangeInclusive<f32>) -> f32 {
x.clamp(
range.start().min(*range.end()),
range.start().max(*range.end()),
)
}

View file

@ -97,7 +97,7 @@ impl CtxRef {
/// This will modify the internal reference to point to a new generation of [`Context`]. /// 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. /// 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) { pub fn begin_frame(&mut self, new_input: RawInput) {
let mut self_: Context = (*self.0).clone(); let mut self_: Context = (*self.0).clone();
self_.begin_frame_mut(new_input); self_.begin_frame_mut(new_input);
@ -291,8 +291,14 @@ impl CtxRef {
response 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 { pub fn debug_painter(&self) -> Painter {
Painter::new(self.clone(), LayerId::debug(), self.input.screen_rect()) Self::layer_painter(self, LayerId::debug())
} }
} }

View file

@ -88,6 +88,17 @@ impl FrameState {
self.used_by_panels = self.used_by_panels.union(panel_rect); 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`. /// Shrink `available_rect`.
pub(crate) fn allocate_top_panel(&mut self, panel_rect: Rect) { pub(crate) fn allocate_top_panel(&mut self, panel_rect: Rect) {
crate::egui_assert!( crate::egui_assert!(
@ -99,6 +110,17 @@ impl FrameState {
self.used_by_panels = self.used_by_panels.union(panel_rect); 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) { pub(crate) fn allocate_central_panel(&mut self, panel_rect: Rect) {
// Note: we do not shrink `available_rect`, because // Note: we do not shrink `available_rect`, because
// we allow windows to cover the CentralPanel. // we allow windows to cover the CentralPanel.

View file

@ -13,6 +13,8 @@ use std::sync::Arc;
pub enum Order { pub enum Order {
/// Painted behind all floating windows /// Painted behind all floating windows
Background, Background,
/// Special layer between panels and windows
PanelResizeLine,
/// Normal moveable windows that you reorder by click /// Normal moveable windows that you reorder by click
Middle, Middle,
/// Popups, menus etc that should always be painted on top of windows /// Popups, menus etc that should always be painted on top of windows
@ -25,9 +27,10 @@ pub enum Order {
Debug, Debug,
} }
impl Order { impl Order {
const COUNT: usize = 5; const COUNT: usize = 6;
const ALL: [Order; Self::COUNT] = [ const ALL: [Order; Self::COUNT] = [
Self::Background, Self::Background,
Self::PanelResizeLine,
Self::Middle, Self::Middle,
Self::Foreground, Self::Foreground,
Self::Tooltip, Self::Tooltip,
@ -37,7 +40,11 @@ impl Order {
#[inline(always)] #[inline(always)]
pub fn allow_interaction(&self) -> bool { pub fn allow_interaction(&self) -> bool {
match self { 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, Self::Tooltip => false,
} }
} }

View file

@ -41,7 +41,7 @@
//! //!
//! ### Getting a [`Ui`] //! ### 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: //! get access to an [`Ui`] where you can put widgets. For example:
//! //!
//! ``` //! ```

View file

@ -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`. /// but can also be placed in a `Window`.
/// In the latter case you may want to wrap it in `Frame`. /// In the latter case you may want to wrap it in `Frame`.
pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> { pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {

View file

@ -63,7 +63,7 @@ impl Ui {
/// Create a new `Ui`. /// Create a new `Ui`.
/// ///
/// Normally you would not use this directly, but instead use /// 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 { pub fn new(ctx: CtxRef, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self {
let style = ctx.style(); let style = ctx.style();
Ui { Ui {

View file

@ -152,47 +152,50 @@ impl DemoWindows {
egui_windows, egui_windows,
} = self; } = self;
egui::SidePanel::left("side_panel", 190.0).show(ctx, |ui| { egui::SidePanel::right("egui_demo_panel")
ui.vertical_centered(|ui| { .min_width(150.0)
ui.heading("✒ egui demos"); .default_width(190.0)
}); .show(ctx, |ui| {
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,
));
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.hyperlink_to( ui.heading("✒ egui demos");
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.separator();
ui.vertical_centered(|ui| { ScrollArea::auto_sized().show(ui, |ui| {
if ui.button("Organize windows").clicked() { use egui::special_emojis::{GITHUB, OS_APPLE, OS_LINUX, OS_WINDOWS};
ui.ctx().memory().reset_areas();
} 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); show_menu_bar(ui);
}); });
@ -302,7 +305,7 @@ fn show_menu_bar(ui: &mut Ui) {
} }
if ui if ui
.button("Clear egui memory") .button("Clear egui memory")
.on_hover_text("Forget scroll, collapsing headers etc") .on_hover_text("Forget scroll, positions, sizes etc")
.clicked() .clicked()
{ {
*ui.ctx().memory() = Default::default(); *ui.ctx().memory() = Default::default();

View file

@ -61,7 +61,7 @@ impl epi::App for HttpApp {
} }
/// Called each time the UI needs repainting, which may be many times per second. /// 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<'_>) { fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) {
if let Some(receiver) = &mut self.in_progress { if let Some(receiver) = &mut self.in_progress {
// Are we there yet? // Are we there yet?

View file

@ -74,49 +74,14 @@ impl epi::App for WrapApp {
self.selected_anchor = self.apps.iter_mut().next().unwrap().0.to_owned(); self.selected_anchor = self.apps.iter_mut().next().unwrap().0.to_owned();
} }
egui::TopPanel::top("wrap_app_top_bar").show(ctx, |ui| { egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| {
// A menu-bar is a horizontal layout with some special styles applied. self.bar_contents(ui, frame);
// 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);
});
});
}); });
self.backend_panel.update(ctx, frame); self.backend_panel.update(ctx, frame);
if self.backend_panel.open || ctx.memory().everything_is_visible() { 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); 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 { fn clock_button(ui: &mut egui::Ui, seconds_since_midnight: f64) -> egui::Response {
let time = seconds_since_midnight; let time = seconds_since_midnight;
let time = format!( let time = format!(
@ -256,10 +262,21 @@ impl BackendPanel {
} }
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut epi::Frame<'_>) { 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); 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(); ui.separator();
self.frame_history.ui(ui); 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(); ui.separator();
if frame.is_web() { if frame.is_web() {
@ -299,17 +325,6 @@ impl BackendPanel {
if !ui.ctx().is_using_pointer() { if !ui.ctx().is_using_pointer() {
self.max_size_points_active = self.max_size_points_ui; 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; let mut screen_reader = ui.ctx().memory().options.screen_reader;
@ -328,6 +343,13 @@ impl BackendPanel {
ui.label(format!("{:?}", event)); ui.label(format!("{:?}", event));
} }
}); });
if !frame.is_web() {
ui.separator();
if ui.button("Quit").clicked() {
frame.quit();
}
}
} }
fn pixels_per_point_ui( fn pixels_per_point_ui(
@ -347,6 +369,7 @@ impl BackendPanel {
ui.add( ui.add(
egui::Slider::new(pixels_per_point, 0.5..=5.0) egui::Slider::new(pixels_per_point, 0.5..=5.0)
.logarithmic(true) .logarithmic(true)
.clamp_to_range(true)
.text("Scale"), .text("Scale"),
) )
.on_hover_text("Physical pixels per point."); .on_hover_text("Physical pixels per point.");

View file

@ -31,7 +31,7 @@ fn main() {
let mut quit = false; 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!"); ui.heading("Hello World!");
if ui.button("Quit").clicked() { if ui.button("Quit").clicked() {
quit = true; quit = true;

View file

@ -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 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 /// Return the given points clamped to be inside the rectangle
/// Panics if [`Self::is_negative`]. /// Panics if [`Self::is_negative`].
#[must_use] #[must_use]

View file

@ -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. /// and deployed as a web site using the [`egui_web`](https://crates.io/crates/egui_web) crate.
pub trait App { pub trait App {
/// Called each time the UI needs repainting, which may be many times per second. /// 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<'_>); fn update(&mut self, ctx: &egui::CtxRef, frame: &mut Frame<'_>);
/// Called once before the first frame. /// Called once before the first frame.