diff --git a/Cargo.lock b/Cargo.lock index d773900c9..aec1b86bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1913,6 +1913,14 @@ dependencies = [ "bitflags 2.4.0", ] +[[package]] +name = "group_layout" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", +] + [[package]] name = "gtk-sys" version = "0.18.0" diff --git a/crates/egui/src/containers/group.rs b/crates/egui/src/containers/group.rs new file mode 100644 index 000000000..19fc274ee --- /dev/null +++ b/crates/egui/src/containers/group.rs @@ -0,0 +1,159 @@ +//! Frame container + +use crate::{layers::ShapeIdx, *}; +use epaint::*; + +/// A group of widgets with a unique id +/// that can be used in centered layouts. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[must_use = "You should call .show()"] +pub struct Group { + id_source: Id, + frame: Frame, +} + +impl Group { + pub fn new(id_source: impl Into) -> Self { + Self { + id_source: id_source.into(), + frame: Frame::default(), + } + } + + pub fn frame(mut self, frame: Frame) -> Self { + self.frame = frame; + self + } +} + +// ---------------------------------------------------------------------------- + +pub struct Prepared { + id: Id, + + /// The frame that was prepared. + /// + /// The margin has already been read and used, + /// but the rest of the fields may be modified. + pub frame: Frame, + + /// This is where we will insert the frame shape so it ends up behind the content. + where_to_put_background: ShapeIdx, + + /// Add your widgets to this UI so it ends up within the frame. + pub content_ui: Ui, +} + +impl Group { + /// Begin a dynamically colored frame. + /// + /// This is a more advanced API. + /// Usually you want to use [`Self::show`] instead. + /// + /// See docs for [`Group`] for an example. + pub fn begin(self, ui: &mut Ui) -> Prepared { + let Self { id_source, frame } = self; + let id = ui.make_persistent_id(id_source); + + let where_to_put_background = ui.painter().add(Shape::Noop); + + let prev_inner_size: Option = ui.data(|data| data.get_temp(id)); + + let mut inner_rect = if let Some(prev_inner_size) = prev_inner_size { + let (_, outer_rect) = ui.allocate_space(prev_inner_size + frame.total_margin().sum()); + outer_rect - frame.total_margin() + } else { + // Invisible sizing pass + let outer_rect_bounds = ui.available_rect_before_wrap(); + outer_rect_bounds - frame.total_margin() + }; + + // Make sure we don't shrink to the negative: + inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x); + inner_rect.max.y = inner_rect.max.y.max(inner_rect.min.y); + + let sizing_pass = prev_inner_size.is_none(); + + let mut layout = *ui.layout(); + + if sizing_pass { + // TODO(emilk): this code is duplicated from `ui.rs` + // During the sizing pass we want widgets to use up as little space as possible, + // so that we measure the only the space we _need_. + layout.cross_justify = false; + if layout.cross_align == Align::Center { + layout.cross_align = Align::Min; + } + } + + let mut content_ui = ui.child_ui( + inner_rect, + layout, + Some(UiStackInfo::new(UiKind::Frame).with_frame(frame)), + ); + + if sizing_pass { + content_ui.set_sizing_pass(); + } + + Prepared { + id, + frame, + where_to_put_background, + content_ui, + } + } + + /// Show the given ui surrounded by this frame. + pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { + self.show_dyn(ui, Box::new(add_contents)) + } + + /// Show using dynamic dispatch. + pub fn show_dyn<'c, R>( + self, + ui: &mut Ui, + add_contents: Box R + 'c>, + ) -> InnerResponse { + let mut prepared = self.begin(ui); + let ret = add_contents(&mut prepared.content_ui); + let response = prepared.end(ui); + InnerResponse::new(ret, response) + } +} + +impl Prepared { + /// Allocate the space that was used by [`Self::content_ui`]. + /// + /// This MUST be called, or the parent ui will not know how much space this widget used. + /// + /// This can be called before or after [`Self::paint`]. + pub fn allocate_space(&self, ui: &mut Ui) -> Response { + let inner_rect = self.content_ui.min_rect(); + let outer_rect = inner_rect + self.frame.total_margin(); + + // Remember size to next frame + ui.data_mut(|data| data.insert_temp(self.id, inner_rect.size())); + + ui.allocate_rect(outer_rect, Sense::hover()) + } + + /// Paint the frame. + /// + /// This can be called before or after [`Self::allocate_space`]. + pub fn paint(&self, ui: &Ui) { + let paint_rect = self.content_ui.min_rect() + self.frame.inner_margin; + + if ui.is_rect_visible(paint_rect) { + let shape = self.frame.paint(paint_rect); + ui.painter().set(self.where_to_put_background, shape); + } + } + + /// Convenience for calling [`Self::allocate_space`] and [`Self::paint`]. + pub fn end(self, ui: &mut Ui) -> Response { + self.paint(ui); + self.allocate_space(ui) + } +} diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index 2e5903239..0cfc8393e 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod area; pub mod collapsing_header; mod combo_box; pub(crate) mod frame; +pub mod group; pub mod panel; pub mod popup; pub(crate) mod resize; @@ -17,6 +18,7 @@ pub use { collapsing_header::{CollapsingHeader, CollapsingResponse}, combo_box::*, frame::Frame, + group::Group, panel::{CentralPanel, SidePanel, TopBottomPanel}, popup::*, resize::Resize, diff --git a/examples/group_layout/Cargo.toml b/examples/group_layout/Cargo.toml new file mode 100644 index 000000000..87cff126b --- /dev/null +++ b/examples/group_layout/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "group_layout" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.76" +publish = false + +[lints] +workspace = true + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } diff --git a/examples/group_layout/src/main.rs b/examples/group_layout/src/main.rs new file mode 100644 index 000000000..769912584 --- /dev/null +++ b/examples/group_layout/src/main.rs @@ -0,0 +1,32 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +use eframe::egui; + +fn main() -> Result<(), eframe::Error> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]), + ..Default::default() + }; + + eframe::run_simple_native("My egui App", options, move |ctx, _frame| { + egui::CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered(|ui| { + egui::Group::new("my_group") + .frame(egui::Frame::group(ui.style())) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label("Hello"); + ui.code("world!"); + }); + + if ui.button("Reset egui").clicked() { + ui.memory_mut(|mem| *mem = Default::default()); + } + }); + }); + }); + }) +}