Panels: Added Context::panel_left and panel_top

Context will keep track of the avilable space left after panels.
Windows will be constrained to that available space.
So add panels first, THEN add windows.
This commit is contained in:
Emil Ernerfeldt 2020-10-21 18:05:36 +02:00
parent 15c5e0b04d
commit 8b51ae5dea
9 changed files with 188 additions and 62 deletions

View file

@ -7,6 +7,7 @@
* Refactored the interface for `egui::app::App` * Refactored the interface for `egui::app::App`
* Demo App: Add slider to scale all of Egui * Demo App: Add slider to scale all of Egui
* Windows are now constrained to the screen * Windows are now constrained to the screen
* Panels: you can now add side panels using `Context::panel_left` and `Context::panel_top`.
* Fix a bug where some regions would slowly grow for non-integral scales (`pixels_per_point`). * Fix a bug where some regions would slowly grow for non-integral scales (`pixels_per_point`).
## 0.2.0 - 2020-10-10 ## 0.2.0 - 2020-10-10

View file

@ -158,11 +158,14 @@ impl Prepared {
} }
pub(crate) fn content_ui(&self, ctx: &Arc<Context>) -> Ui { pub(crate) fn content_ui(&self, ctx: &Arc<Context>) -> Ui {
let max_rect = Rect::from_min_size(self.state.pos, Vec2::infinity());
let clip_rect = max_rect.expand(ctx.style().visuals.clip_rect_margin);
Ui::new( Ui::new(
ctx.clone(), ctx.clone(),
self.layer_id, self.layer_id,
self.layer_id.id, self.layer_id.id,
Rect::from_min_size(self.state.pos, Vec2::infinity()), max_rect,
clip_rect,
) )
} }
@ -176,7 +179,6 @@ impl Prepared {
state.size = content_ui.min_rect().size(); state.size = content_ui.min_rect().size();
let area_rect = Rect::from_min_size(state.pos, state.size); let area_rect = Rect::from_min_size(state.pos, state.size);
let clip_rect = ctx.rect();
let interact_id = if movable { let interact_id = if movable {
Some(layer_id.id.with("move")) Some(layer_id.id.with("move"))
@ -185,7 +187,7 @@ impl Prepared {
}; };
let move_response = ctx.interact( let move_response = ctx.interact(
layer_id, layer_id,
clip_rect, Rect::everything(),
area_rect, area_rect,
interact_id, interact_id,
Sense::click_and_drag(), Sense::click_and_drag(),
@ -243,9 +245,11 @@ fn automatic_area_position(ctx: &Context) -> Pos2 {
.collect(); .collect();
existing.sort_by_key(|r| r.left().round() as i32); existing.sort_by_key(|r| r.left().round() as i32);
let left = 16.0; let available_rect = ctx.available_rect();
let top = 32.0; // allow existence of menu bar. TODO: get from ui.available()
let spacing = 16.0; let spacing = 16.0;
let left = available_rect.left() + spacing;
let top = available_rect.top() + spacing;
if existing.is_empty() { if existing.is_empty() {
return pos2(left, top); return pos2(left, top);
@ -279,14 +283,14 @@ fn automatic_area_position(ctx: &Context) -> Pos2 {
// Find first column with some available space at the bottom of it: // Find first column with some available space at the bottom of it:
for col_bb in &column_bbs { for col_bb in &column_bbs {
if col_bb.bottom() < ctx.input().screen_size.y * 0.5 { if col_bb.bottom() < available_rect.center().y {
return pos2(col_bb.left(), col_bb.bottom() + spacing); return pos2(col_bb.left(), col_bb.bottom() + spacing);
} }
} }
// Maybe we can fit a new column? // Maybe we can fit a new column?
let rightmost = column_bbs.last().unwrap().right(); let rightmost = column_bbs.last().unwrap().right();
if rightmost < ctx.input().screen_size.x - 200.0 { if rightmost + 200.0 < available_rect.right() {
return pos2(rightmost + spacing, top); return pos2(rightmost + spacing, top);
} }

View file

@ -3,7 +3,7 @@
use crate::{layers::PaintCmdIdx, paint::*, *}; use crate::{layers::PaintCmdIdx, paint::*, *};
/// Adds a rectangular frame and background to some `Ui`. /// Adds a rectangular frame and background to some `Ui`.
#[derive(Clone, Debug)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct Frame { pub struct Frame {
// On each side // On each side
pub margin: Vec2, pub margin: Vec2,
@ -58,6 +58,10 @@ impl Frame {
} }
} }
pub fn panel(style: &Style) -> Self {
Self::popup(style)
}
pub fn fill(mut self, fill: Srgba) -> Self { pub fn fill(mut self, fill: Srgba) -> Self {
self.fill = fill; self.fill = fill;
self self

View file

@ -42,6 +42,9 @@ pub struct Context {
input: InputState, input: InputState,
/// Starts off as the screen_rect, shrinks as panels are added.
available_rect: Mutex<Option<Rect>>,
// The output of a frame: // The output of a frame:
graphics: Mutex<GraphicLayers>, graphics: Mutex<GraphicLayers>,
output: Mutex<Output>, output: Mutex<Output>,
@ -62,6 +65,7 @@ impl Clone for Context {
memory: self.memory.clone(), memory: self.memory.clone(),
animation_manager: self.animation_manager.clone(), animation_manager: self.animation_manager.clone(),
input: self.input.clone(), input: self.input.clone(),
available_rect: self.available_rect.clone(),
graphics: self.graphics.clone(), graphics: self.graphics.clone(),
output: self.output.clone(), output: self.output.clone(),
used_ids: self.used_ids.clone(), used_ids: self.used_ids.clone(),
@ -76,8 +80,13 @@ impl Context {
Arc::new(Self::default()) Arc::new(Self::default())
} }
pub fn rect(&self) -> Rect { /// How much space is still available after panels has been added.
Rect::from_min_size(pos2(0.0, 0.0), self.input.screen_size) /// This is the "background" area, what Egui doesn't cover with panels (but may cover with windows).
/// This is also the area to which windows are constrained.
pub fn available_rect(&self) -> Rect {
self.available_rect
.lock()
.expect("Called `avaiblable_rect()` before `begin_frame()`")
} }
pub fn memory(&self) -> MutexGuard<'_, Memory> { pub fn memory(&self) -> MutexGuard<'_, Memory> {
@ -168,7 +177,7 @@ impl Context {
/// Constraint the position of a window/area /// Constraint the position of a window/area
/// so it fits within the screen. /// so it fits within the screen.
pub(crate) fn constrain_window_rect(&self, window: Rect) -> Rect { pub(crate) fn constrain_window_rect(&self, window: Rect) -> Rect {
let screen = self.rect(); let screen = self.available_rect();
let mut pos = window.min; let mut pos = window.min;
@ -203,6 +212,8 @@ impl Context {
self.used_ids.lock().clear(); self.used_ids.lock().clear();
self.input = std::mem::take(&mut self.input).begin_frame(new_raw_input); self.input = std::mem::take(&mut self.input).begin_frame(new_raw_input);
*self.available_rect.lock() = Some(self.input.screen_rect());
let mut font_definitions = self.options.lock().font_definitions.clone(); let mut font_definitions = self.options.lock().font_definitions.clone();
font_definitions.pixels_per_point = self.input.pixels_per_point(); font_definitions.pixels_per_point = self.input.pixels_per_point();
let same_as_current = match &self.fonts { let same_as_current = match &self.fonts {
@ -255,7 +266,7 @@ impl Context {
/// A `Ui` for the entire screen, behind any windows. /// A `Ui` for the entire screen, behind any windows.
fn fullscreen_ui(self: &Arc<Self>) -> Ui { fn fullscreen_ui(self: &Arc<Self>) -> Ui {
let rect = Rect::from_min_size(Default::default(), self.input().screen_size); let rect = self.input.screen_rect();
let id = Id::background(); let id = Id::background();
let layer_id = LayerId { let layer_id = LayerId {
order: Order::Background, order: Order::Background,
@ -271,7 +282,82 @@ impl Context {
vel: Default::default(), vel: Default::default(),
}, },
); );
Ui::new(self.clone(), layer_id, id, rect) Ui::new(self.clone(), layer_id, id, rect, rect)
}
// ---------------------------------------------------------------------
/// Create a panel that covers the entire left side of the screen.
/// The given `max_width` is a soft maximum (as always), and the actual panel may be smaller or larger.
/// You should call this *before* adding windows to Egui.
pub fn panel_left<R>(
self: &Arc<Context>,
max_width: f32,
frame: Frame,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> (R, Response) {
let mut panel_rect = self.available_rect();
panel_rect.max.x = panel_rect.max.x.at_most(panel_rect.min.x + max_width);
let id = Id::background();
let layer_id = LayerId {
order: Order::Background,
id,
};
let clip_rect = self.input.screen_rect();
let mut panel_ui = Ui::new(self.clone(), layer_id, id, panel_rect, clip_rect);
let r = frame.show(&mut panel_ui, |ui| {
ui.set_min_height(ui.max_rect_finite().height()); // fill full height
add_contents(ui)
});
let panel_rect = panel_ui.min_rect();
let response = panel_ui.interact_hover(panel_rect);
// Shrink out `available_rect`:
let mut remainder = self.available_rect();
remainder.min.x = panel_rect.max.x;
*self.available_rect.lock() = Some(remainder);
(r, response)
}
/// Create a panel that covers the entire top side of the screen.
/// This can be useful to add a menu bar to the whole window.
/// The given `max_height` is a soft maximum (as always), and the actual panel may be smaller or larger.
/// You should call this *before* adding windows to Egui.
pub fn panel_top<R>(
self: &Arc<Context>,
max_height: f32,
frame: Frame,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> (R, Response) {
let mut panel_rect = self.available_rect();
panel_rect.max.y = panel_rect.max.y.at_most(panel_rect.min.y + max_height);
let id = Id::background();
let layer_id = LayerId {
order: Order::Background,
id,
};
let clip_rect = self.input.screen_rect();
let mut panel_ui = Ui::new(self.clone(), layer_id, id, panel_rect, clip_rect);
let r = frame.show(&mut panel_ui, |ui| {
ui.set_min_width(ui.max_rect_finite().width()); // fill full width
add_contents(ui)
});
let panel_rect = panel_ui.min_rect();
let response = panel_ui.interact_hover(panel_rect);
// Shrink out `available_rect`:
let mut remainder = self.available_rect();
remainder.min.y = panel_rect.max.y;
*self.available_rect.lock() = Some(remainder);
(r, response)
} }
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
@ -328,9 +414,16 @@ impl Context {
pub fn is_mouse_over_area(&self) -> bool { pub fn is_mouse_over_area(&self) -> bool {
if let Some(mouse_pos) = self.input.mouse.pos { if let Some(mouse_pos) = self.input.mouse.pos {
if let Some(layer) = self.layer_id_at(mouse_pos) { if let Some(layer) = self.layer_id_at(mouse_pos) {
// TODO: this currently returns false for hovering the menu bar. if layer.order == Order::Background {
// We should probably move the menu bar to its own area to fix this. if let Some(available_rect) = *self.available_rect.lock() {
layer.order != Order::Background // "available_rect" is the area that Egui is NOT using.
!available_rect.contains(mouse_pos)
} else {
false
}
} else {
true
}
} else { } else {
false false
} }
@ -521,7 +614,7 @@ impl Context {
/// ## Painting /// ## Painting
impl Context { impl Context {
pub fn debug_painter(self: &Arc<Self>) -> Painter { pub fn debug_painter(self: &Arc<Self>) -> Painter {
Painter::new(self.clone(), LayerId::debug(), self.rect()) Painter::new(self.clone(), LayerId::debug(), self.input.screen_rect())
} }
} }

View file

@ -278,20 +278,12 @@ impl app::App for DemoApp {
info: &app::BackendInfo, info: &app::BackendInfo,
tex_allocator: Option<&mut dyn app::TextureAllocator>, tex_allocator: Option<&mut dyn app::TextureAllocator>,
) -> app::AppOutput { ) -> app::AppOutput {
let mut output = app::AppOutput::default();
crate::Window::new("Backend")
.min_width(360.0)
.scroll(false)
.show(ui.ctx(), |ui| {
output = self.backend_ui(ui, info);
});
let web_location_hash = info let web_location_hash = info
.web_info .web_info
.as_ref() .as_ref()
.map(|info| info.web_location_hash.clone()) .map(|info| info.web_location_hash.clone())
.unwrap_or_default(); .unwrap_or_default();
let link = if web_location_hash == "clock" { let link = if web_location_hash == "clock" {
Some(demos::DemoLink::Clock) Some(demos::DemoLink::Clock)
} else { } else {
@ -305,6 +297,15 @@ impl app::App for DemoApp {
self.demo_windows.ui(ui, &demo_environment, tex_allocator); self.demo_windows.ui(ui, &demo_environment, tex_allocator);
let mut output = app::AppOutput::default();
crate::Window::new("Backend")
.min_width(360.0)
.scroll(false)
.show(ui.ctx(), |ui| {
output = self.backend_ui(ui, info);
});
if self.run_mode == RunMode::Continuous { if self.run_mode == RunMode::Continuous {
// Tell the backend to repaint as soon as possible // Tell the backend to repaint as soon as possible
ui.ctx().request_repaint(); ui.ctx().request_repaint();

View file

@ -31,15 +31,6 @@ impl Default for DemoWindow {
impl DemoWindow { impl DemoWindow {
pub fn ui(&mut self, ui: &mut Ui) { pub fn ui(&mut self, ui: &mut Ui) {
ui.collapsing("About Egui", |ui| {
ui.label("Egui is an experimental immediate mode GUI written in Rust.");
ui.horizontal(|ui| {
ui.label("Project home page:");
ui.hyperlink("https://github.com/emilk/egui");
});
});
CollapsingHeader::new("Widgets") CollapsingHeader::new("Widgets")
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {

View file

@ -48,6 +48,8 @@ impl DemoWindows {
env: &DemoEnvironment, env: &DemoEnvironment,
tex_allocator: Option<&mut dyn app::TextureAllocator>, tex_allocator: Option<&mut dyn app::TextureAllocator>,
) { ) {
let ctx = ui.ctx();
if self.previous_link != env.link { if self.previous_link != env.link {
match env.link { match env.link {
None => {} None => {}
@ -61,8 +63,31 @@ impl DemoWindows {
self.previous_link = env.link; self.previous_link = env.link;
} }
show_menu_bar(ui, &mut self.open_windows, env.seconds_since_midnight); let frame = crate::Frame::panel(ui.style());
self.windows(ui.ctx(), env, tex_allocator); ctx.panel_left(240.0, frame, |ui| {
ui.heading("Egui Demo");
ui.label("Egui is an immediate mode GUI library written in Rust.");
ui.add(crate::Hyperlink::new("https://github.com/emilk/egui").text("Egui home page"));
ui.separator();
ui.label(
"This is an example of a panel. Windows are constrained to the area that remain.",
);
if ui.button("Organize windows").clicked {
ui.ctx().memory().reset_areas();
}
ui.separator();
ui.heading("Windows:");
ui.indent("windows", |ui| {
self.open_windows.ui(ui);
});
});
ctx.panel_top(0.0, crate::Frame::none(), |ui| {
show_menu_bar(ui, &mut self.open_windows, env.seconds_since_midnight);
});
self.windows(ctx, env, tex_allocator);
} }
/// Show the open windows. /// Show the open windows.
@ -218,6 +243,28 @@ impl OpenWindows {
color_test: false, color_test: false,
} }
} }
fn ui(&mut self, ui: &mut Ui) {
let Self {
demo,
fractal_clock,
settings,
inspection,
memory,
resize,
color_test,
} = self;
ui.checkbox(demo, "Demo");
ui.checkbox(fractal_clock, "Fractal Clock");
ui.separator();
ui.checkbox(settings, "Settings");
ui.checkbox(inspection, "Inspection");
ui.checkbox(memory, "Memory");
ui.checkbox(resize, "Resize examples");
ui.separator();
ui.checkbox(color_test, "Color test")
.on_hover_text("For testing the integrations painter");
}
} }
fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows, seconds_since_midnight: Option<f64>) { fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows, seconds_since_midnight: Option<f64>) {
@ -225,7 +272,7 @@ fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows, seconds_since_midnight:
menu::bar(ui, |ui| { menu::bar(ui, |ui| {
menu::menu(ui, "File", |ui| { menu::menu(ui, "File", |ui| {
if ui.button("Reorganize windows").clicked { if ui.button("Organize windows").clicked {
ui.ctx().memory().reset_areas(); ui.ctx().memory().reset_areas();
} }
if ui if ui
@ -236,27 +283,7 @@ fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows, seconds_since_midnight:
*ui.ctx().memory() = Default::default(); *ui.ctx().memory() = Default::default();
} }
}); });
menu::menu(ui, "Windows", |ui| { menu::menu(ui, "Windows", |ui| windows.ui(ui));
let OpenWindows {
demo,
fractal_clock,
settings,
inspection,
memory,
resize,
color_test,
} = windows;
ui.checkbox(demo, "Demo");
ui.checkbox(fractal_clock, "Fractal Clock");
ui.separator();
ui.checkbox(settings, "Settings");
ui.checkbox(inspection, "Inspection");
ui.checkbox(memory, "Memory");
ui.checkbox(resize, "Resize examples");
ui.separator();
ui.checkbox(color_test, "Color test")
.on_hover_text("For testing the integrations painter");
});
menu::menu(ui, "About", |ui| { menu::menu(ui, "About", |ui| {
ui.label("This is Egui"); ui.label("This is Egui");
ui.add(Hyperlink::new("https://github.com/emilk/egui").text("Egui home page")); ui.add(Hyperlink::new("https://github.com/emilk/egui").text("Egui home page"));

View file

@ -39,7 +39,7 @@ impl FractalClock {
) { ) {
Window::new("FractalClock") Window::new("FractalClock")
.open(open) .open(open)
.default_rect(ctx.rect().expand(-42.0)) .default_rect(ctx.available_rect().expand(-42.0))
.scroll(false) .scroll(false)
// Dark background frame to make it pop: // Dark background frame to make it pop:
.frame(Frame::window(&ctx.style()).fill(Srgba::black_alpha(250))) .frame(Frame::window(&ctx.style()).fill(Srgba::black_alpha(250)))

View file

@ -60,9 +60,14 @@ impl Ui {
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Creation: // Creation:
pub fn new(ctx: Arc<Context>, layer_id: LayerId, id: Id, max_rect: Rect) -> Self { pub fn new(
ctx: Arc<Context>,
layer_id: LayerId,
id: Id,
max_rect: Rect,
clip_rect: Rect,
) -> Self {
let style = ctx.style(); let style = ctx.style();
let clip_rect = max_rect.expand(style.visuals.clip_rect_margin);
let layout = Layout::default(); let layout = Layout::default();
let cursor = layout.initial_cursor(max_rect); let cursor = layout.initial_cursor(max_rect);
let min_size = Vec2::zero(); // TODO: From Style let min_size = Vec2::zero(); // TODO: From Style