diff --git a/CHANGELOG.md b/CHANGELOG.md index 48e29634..fa9f3eb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Refactored the interface for `egui::app::App` * Demo App: Add slider to scale all of Egui * 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`). ## 0.2.0 - 2020-10-10 diff --git a/egui/src/containers/area.rs b/egui/src/containers/area.rs index 5fbcf7f7..ef6ba306 100644 --- a/egui/src/containers/area.rs +++ b/egui/src/containers/area.rs @@ -158,11 +158,14 @@ impl Prepared { } pub(crate) fn content_ui(&self, ctx: &Arc) -> 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( ctx.clone(), self.layer_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(); let area_rect = Rect::from_min_size(state.pos, state.size); - let clip_rect = ctx.rect(); let interact_id = if movable { Some(layer_id.id.with("move")) @@ -185,7 +187,7 @@ impl Prepared { }; let move_response = ctx.interact( layer_id, - clip_rect, + Rect::everything(), area_rect, interact_id, Sense::click_and_drag(), @@ -243,9 +245,11 @@ fn automatic_area_position(ctx: &Context) -> Pos2 { .collect(); existing.sort_by_key(|r| r.left().round() as i32); - let left = 16.0; - let top = 32.0; // allow existence of menu bar. TODO: get from ui.available() + let available_rect = ctx.available_rect(); + let spacing = 16.0; + let left = available_rect.left() + spacing; + let top = available_rect.top() + spacing; if existing.is_empty() { 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: 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); } } // Maybe we can fit a new column? 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); } diff --git a/egui/src/containers/frame.rs b/egui/src/containers/frame.rs index 97ac4954..fdb7380b 100644 --- a/egui/src/containers/frame.rs +++ b/egui/src/containers/frame.rs @@ -3,7 +3,7 @@ use crate::{layers::PaintCmdIdx, paint::*, *}; /// Adds a rectangular frame and background to some `Ui`. -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Frame { // On each side 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 { self.fill = fill; self diff --git a/egui/src/context.rs b/egui/src/context.rs index 0554b161..215e98f8 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -42,6 +42,9 @@ pub struct Context { input: InputState, + /// Starts off as the screen_rect, shrinks as panels are added. + available_rect: Mutex>, + // The output of a frame: graphics: Mutex, output: Mutex, @@ -62,6 +65,7 @@ impl Clone for Context { memory: self.memory.clone(), animation_manager: self.animation_manager.clone(), input: self.input.clone(), + available_rect: self.available_rect.clone(), graphics: self.graphics.clone(), output: self.output.clone(), used_ids: self.used_ids.clone(), @@ -76,8 +80,13 @@ impl Context { Arc::new(Self::default()) } - pub fn rect(&self) -> Rect { - Rect::from_min_size(pos2(0.0, 0.0), self.input.screen_size) + /// How much space is still available after panels has been added. + /// This is the "background" area, what Egui doesn't cover with panels (but may cover with windows). + /// This is also the area to which windows are constrained. + pub fn available_rect(&self) -> Rect { + self.available_rect + .lock() + .expect("Called `avaiblable_rect()` before `begin_frame()`") } pub fn memory(&self) -> MutexGuard<'_, Memory> { @@ -168,7 +177,7 @@ impl Context { /// Constraint the position of a window/area /// so it fits within the screen. pub(crate) fn constrain_window_rect(&self, window: Rect) -> Rect { - let screen = self.rect(); + let screen = self.available_rect(); let mut pos = window.min; @@ -203,6 +212,8 @@ impl Context { self.used_ids.lock().clear(); 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(); font_definitions.pixels_per_point = self.input.pixels_per_point(); let same_as_current = match &self.fonts { @@ -255,7 +266,7 @@ impl Context { /// A `Ui` for the entire screen, behind any windows. fn fullscreen_ui(self: &Arc) -> Ui { - let rect = Rect::from_min_size(Default::default(), self.input().screen_size); + let rect = self.input.screen_rect(); let id = Id::background(); let layer_id = LayerId { order: Order::Background, @@ -271,7 +282,82 @@ impl Context { 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( + self: &Arc, + 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( + self: &Arc, + 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 { if let Some(mouse_pos) = self.input.mouse.pos { if let Some(layer) = self.layer_id_at(mouse_pos) { - // TODO: this currently returns false for hovering the menu bar. - // We should probably move the menu bar to its own area to fix this. - layer.order != Order::Background + if layer.order == Order::Background { + if let Some(available_rect) = *self.available_rect.lock() { + // "available_rect" is the area that Egui is NOT using. + !available_rect.contains(mouse_pos) + } else { + false + } + } else { + true + } } else { false } @@ -521,7 +614,7 @@ impl Context { /// ## Painting impl Context { pub fn debug_painter(self: &Arc) -> Painter { - Painter::new(self.clone(), LayerId::debug(), self.rect()) + Painter::new(self.clone(), LayerId::debug(), self.input.screen_rect()) } } diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index d6b89fd3..8719572f 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -278,20 +278,12 @@ impl app::App for DemoApp { info: &app::BackendInfo, tex_allocator: Option<&mut dyn app::TextureAllocator>, ) -> 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 .web_info .as_ref() .map(|info| info.web_location_hash.clone()) .unwrap_or_default(); + let link = if web_location_hash == "clock" { Some(demos::DemoLink::Clock) } else { @@ -305,6 +297,15 @@ impl app::App for DemoApp { 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 { // Tell the backend to repaint as soon as possible ui.ctx().request_repaint(); diff --git a/egui/src/demos/demo_window.rs b/egui/src/demos/demo_window.rs index 0c055093..917bd772 100644 --- a/egui/src/demos/demo_window.rs +++ b/egui/src/demos/demo_window.rs @@ -31,15 +31,6 @@ impl Default for DemoWindow { impl DemoWindow { 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") .default_open(true) .show(ui, |ui| { diff --git a/egui/src/demos/demo_windows.rs b/egui/src/demos/demo_windows.rs index 994b196a..21629e1b 100644 --- a/egui/src/demos/demo_windows.rs +++ b/egui/src/demos/demo_windows.rs @@ -48,6 +48,8 @@ impl DemoWindows { env: &DemoEnvironment, tex_allocator: Option<&mut dyn app::TextureAllocator>, ) { + let ctx = ui.ctx(); + if self.previous_link != env.link { match env.link { None => {} @@ -61,8 +63,31 @@ impl DemoWindows { self.previous_link = env.link; } - show_menu_bar(ui, &mut self.open_windows, env.seconds_since_midnight); - self.windows(ui.ctx(), env, tex_allocator); + let frame = crate::Frame::panel(ui.style()); + 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. @@ -218,6 +243,28 @@ impl OpenWindows { 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) { @@ -225,7 +272,7 @@ fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows, seconds_since_midnight: menu::bar(ui, |ui| { menu::menu(ui, "File", |ui| { - if ui.button("Reorganize windows").clicked { + if ui.button("Organize windows").clicked { ui.ctx().memory().reset_areas(); } if ui @@ -236,27 +283,7 @@ fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows, seconds_since_midnight: *ui.ctx().memory() = Default::default(); } }); - menu::menu(ui, "Windows", |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, "Windows", |ui| windows.ui(ui)); menu::menu(ui, "About", |ui| { ui.label("This is Egui"); ui.add(Hyperlink::new("https://github.com/emilk/egui").text("Egui home page")); diff --git a/egui/src/demos/fractal_clock.rs b/egui/src/demos/fractal_clock.rs index 261bde7c..84e33a9e 100644 --- a/egui/src/demos/fractal_clock.rs +++ b/egui/src/demos/fractal_clock.rs @@ -39,7 +39,7 @@ impl FractalClock { ) { Window::new("FractalClock") .open(open) - .default_rect(ctx.rect().expand(-42.0)) + .default_rect(ctx.available_rect().expand(-42.0)) .scroll(false) // Dark background frame to make it pop: .frame(Frame::window(&ctx.style()).fill(Srgba::black_alpha(250))) diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 4875c038..61c1c86b 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -60,9 +60,14 @@ impl Ui { // ------------------------------------------------------------------------ // Creation: - pub fn new(ctx: Arc, layer_id: LayerId, id: Id, max_rect: Rect) -> Self { + pub fn new( + ctx: Arc, + layer_id: LayerId, + id: Id, + max_rect: Rect, + clip_rect: Rect, + ) -> Self { let style = ctx.style(); - let clip_rect = max_rect.expand(style.visuals.clip_rect_margin); let layout = Layout::default(); let cursor = layout.initial_cursor(max_rect); let min_size = Vec2::zero(); // TODO: From Style