You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Is your feature request related to a problem? Please describe.
I am trying to create a library around both SidePanel and TopBottomPanel so that they have the same behavior when used in a specific way (my specific case is toolbars and properties panels). Having two distinct type makes it hard to create such wrapper that abstract the specific of each of them.
In my use case, the toolbar will have a fixed width/height and will not be resizable. The use of the wrapper allows me to use a pre-made toolbar instead of duplicating options code.
Describe the solution you'd like
Seeing that the panels impl have almost the same signature, I think it make sense to create a trait so that we can create generic wrapper using impl trait type
Also, on a personal opinion, I think the name TopBottom and Side are a bit confusing at first glance, even more when nesting panels, making it seems that having a BottomPanel inside a TopPanel inside a LeftPanel is not possible. They could be renamed to HorizontalPanel (for the TopBottomPanel) and VerticalPanel (for the SidePanel). That way, no matter the nesting they'll always be horizontal or vertical.
I can create a pull requests, but chances that the changes I made are not working as-is (excluding the breaking change of the names), me being a noob in Rust 😉
Draft code change proposal
//! Panels are [`Ui`] regions taking up e.g. the left side of a [`Ui`] or screen.//!//! Panels can either be a child of a [`Ui`] (taking up a portion of the parent)//! or be top-level (taking up a portion of the whole screen).//!//! Together with [`crate::Window`] and [`crate::Area`]:s, top-level panels are//! the only places where you can put your 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.//!//! You must never open one top-level panel from within another panel. Add one panel, then the next.//!//! ⚠ Always add any [`CentralPanel`] last.//!//! Add your [`crate::Window`]:s after any top-level panels.usecrate::{
lerp, vec2,Align,Context,CursorIcon,Frame,Id,InnerResponse,LayerId,Layout,NumExt,Rangef,Rect,Sense,Stroke,Ui,UiBuilder,UiKind,UiStackInfo,Vec2,};fnanimate_expansion(ctx:&Context,id:Id,is_expanded:bool) -> f32{
ctx.animate_bool_responsive(id, is_expanded)}/// State regarding panels.#[derive(Clone,Copy,Debug)]#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]pubstructPanelState{pubrect:Rect,}implPanelState{pubfnload(ctx:&Context,bar_id:Id) -> Option<Self>{
ctx.data_mut(|d| d.get_persisted(bar_id))}/// The size of the panel (from previous frame).pubfnsize(&self) -> Vec2{self.rect.size()}fnstore(self,ctx:&Context,bar_id:Id){
ctx.data_mut(|d| d.insert_persisted(bar_id,self));}}// ----------------------------------------------------------------------------pubtraitSide{fnopposite(self) -> Self;}pubtraitPanelOptions{/// The id should be globally unique, e.g. `Id::new("my_panel")`.fnnew(side:implSide,id:implInto<Id>) -> Self;/// Can panel be resized by dragging the edge of it?////// Default is `true`.////// If you want your panel to be resizable you also need a widget in it that/// takes up more space as you resize it, such as:/// * Wrapping text ([`Ui::horizontal_wrapped`])./// * A [`crate::ScrollArea`]./// * A [`crate::Separator`]./// * A [`crate::TextEdit`]./// * …fnresizable(mutself,resizable:bool) -> Self;/// Show a separator line, even when not interacting with it?////// Default: `true`.fnshow_separator_line(mutself,show_separator_line:bool) -> Self;/// Change the background color, margins, etc.fnframe(mutself,frame:Frame) -> Self;}pubtraitPanelSize{/// The initial wrapping size (width or height) of the panel including margins.fndefault_size(mutself,default_size:f32) -> Self;/// Minimum size (width or height) of the panel, including margins.fnmin_size(mutself,min_size:f32) -> Self;/// Maximum size (width or height) of the panel, including margins.fnmax_size(mutself,max_size:f32) -> Self;/// The allowable size (width or height) range for the panel, including margins.fnsize_range(mutself,size_range:implInto<Rangef>) -> Self;/// Enforce this exact size (width or height), including margins.fnexact_size(mutself,size:f32) -> Self;}pubtraitPanelActions{/// Show the panel inside a [`Ui`].fnshow_inside<R>(self,ui:&mutUi,add_contents:implFnOnce(&mutUi) -> R,) -> InnerResponse<R>;/// Show the panel at the top level.fnshow<R>(self,ctx:&Context,add_contents:implFnOnce(&mutUi) -> R) -> InnerResponse<R>;/// Show the panel if `is_expanded` is `true`,/// otherwise don't show it, but with a nice animation between collapsed and expanded.fnshow_animated<R>(self,ctx:&Context,is_expanded:bool,add_contents:implFnOnce(&mutUi) -> R,) -> Option<InnerResponse<R>>;/// Show the panel if `is_expanded` is `true`,/// otherwise don't show it, but with a nice animation between collapsed and expanded.fnshow_animated_inside<R>(self,ui:&mutUi,is_expanded:bool,add_contents:implFnOnce(&mutUi) -> R,) -> Option<InnerResponse<R>>;/// Show either a collapsed or an expanded panel, with a nice animation between.fnshow_animated_between<R>(ctx:&Context,is_expanded:bool,collapsed_panel:Self,expanded_panel:Self,add_contents:implFnOnce(&mutUi,f32) -> R,) -> Option<InnerResponse<R>>;/// Show either a collapsed or an expanded panel, with a nice animation between.fnshow_animated_between_inside<R>(ui:&mutUi,is_expanded:bool,collapsed_panel:Self,expanded_panel:Self,add_contents:implFnOnce(&mutUi,f32) -> R,) -> InnerResponse<R>;}// ----------------------------------------------------------------------------/// [`Left`](VerticalSide::Left) or [`Right`](VerticalSide::Right)#[derive(Clone,Copy,Debug,PartialEq,Eq)]pubenumVerticalSide{Left,Right,}implSideforVerticalSide{fnopposite(self) -> Self{matchself{Self::Left => Self::Right,Self::Right => Self::Left,}}}implVerticalSide{fnset_rect_width(self,rect:&mutRect,width:f32){matchself{Self::Left => rect.max.x = rect.min.x + width,Self::Right => rect.min.x = rect.max.x - width,}}fnside_x(self,rect:Rect) -> f32{matchself{Self::Left => rect.left(),Self::Right => rect.right(),}}}/// A panel that covers the entire left or right side of a [`Ui`] or screen.////// 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`] last.////// See the [module level docs](crate::containers::panel) for more details.////// ```/// # egui::__run_test_ctx(|ctx| {/// egui::SidePanel::left("my_left_panel").show(ctx, |ui| {/// ui.label("Hello World!");/// });/// # });/// ```////// See also [`HorizontalPanel`].#[must_use = "You should call .show()"]pubstructVerticalPanel{side:VerticalSide,id:Id,frame:Option<Frame>,resizable:bool,show_separator_line:bool,default_width:f32,width_range:Rangef,}implPanelOptionsforVerticalPanel{/// The id should be globally unique, e.g. `Id::new("my_panel")`.fnnew(side:VerticalSide,id:implInto<Id>) -> Self{Self{
side,id: id.into(),frame:None,resizable:true,show_separator_line:true,default_width:200.0,width_range:Rangef::new(96.0, f32::INFINITY),}}#[inline]fnresizable(mutself,resizable:bool) -> Self{self.resizable = resizable;self}#[inline]fnshow_separator_line(mutself,show_separator_line:bool) -> Self{self.show_separator_line = show_separator_line;self}#[inline]fnframe(mutself,frame:Frame) -> Self{self.frame = Some(frame);self}}implPanelSizeforVerticalPanel{fndefault_size(mutself,default_size:f32) -> Self{self.default_width(default_size)}fnmin_size(mutself,min_size:f32) -> Self{self.min_width(min_size)}fnmax_size(mutself,max_size:f32) -> Self{self.max_width(max_size)}fnsize_range(mutself,size_range:implInto<Rangef>) -> Self{self.width_range(size_range)}fnexact_size(mutself,size:f32) -> Self{self.exact_width(size)}}implVerticalPanel{/// The id should be globally unique, e.g. `Id::new("my_left_panel")`.pubfnleft(id:implInto<Id>) -> Self{Self::new(VerticalSide::Left, id)}/// The id should be globally unique, e.g. `Id::new("my_right_panel")`.pubfnright(id:implInto<Id>) -> Self{Self::new(VerticalSide::Right, id)}/// The initial wrapping width of the [`VerticalPanel`], including margins.#[inline]pubfndefault_width(mutself,default_width:f32) -> Self{self.default_width = default_width;self.width_range = Rangef::new(self.width_range.min.at_most(default_width),self.width_range.max.at_least(default_width),);self}/// Minimum width of the panel, including margins.#[inline]pubfnmin_width(mutself,min_width:f32) -> Self{self.width_range = Rangef::new(min_width,self.width_range.max.at_least(min_width));self}/// Maximum width of the panel, including margins.#[inline]pubfnmax_width(mutself,max_width:f32) -> Self{self.width_range = Rangef::new(self.width_range.min.at_most(max_width), max_width);self}/// The allowable width range for the panel, including margins.#[inline]pubfnwidth_range(mutself,width_range:implInto<Rangef>) -> Self{let width_range = width_range.into();self.default_width = clamp_to_range(self.default_width, width_range);self.width_range = width_range;self}/// Enforce this exact width, including margins.#[inline]pubfnexact_width(mutself,width:f32) -> Self{self.default_width = width;self.width_range = Rangef::point(width);self}}implPanelActionsforVerticalPanel{/// Show the panel inside a [`Ui`].fnshow_inside<R>(self,ui:&mutUi,add_contents:implFnOnce(&mutUi) -> R,) -> InnerResponse<R>{self.show_inside_dyn(ui,Box::new(add_contents))}/// Show the panel at the top level.fnshow<R>(self,ctx:&Context,add_contents:implFnOnce(&mutUi) -> R) -> InnerResponse<R>{self.show_dyn(ctx,Box::new(add_contents))}/// Show the panel if `is_expanded` is `true`,/// otherwise don't show it, but with a nice animation between collapsed and expanded.fnshow_animated<R>(self,ctx:&Context,is_expanded:bool,add_contents:implFnOnce(&mutUi) -> R,) -> Option<InnerResponse<R>>{let how_expanded = animate_expansion(ctx,self.id.with("animation"), is_expanded);if0.0 == how_expanded {None}elseif how_expanded < 1.0{// Show a fake panel in this in-between animation state:// TODO(emilk): move the panel out-of-screen instead of changing its width.// Then we can actually paint it as it animates.let expanded_width = PanelState::load(ctx,self.id).map_or(self.default_width, |state| state.rect.width());let fake_width = how_expanded * expanded_width;Self{id:self.id.with("animating_panel"),
..self}.resizable(false).exact_width(fake_width).show(ctx, |_ui| {});None}else{// Show the real panel:Some(self.show(ctx, add_contents))}}/// Show the panel if `is_expanded` is `true`,/// otherwise don't show it, but with a nice animation between collapsed and expanded.fnshow_animated_inside<R>(self,ui:&mutUi,is_expanded:bool,add_contents:implFnOnce(&mutUi) -> R,) -> Option<InnerResponse<R>>{let how_expanded = animate_expansion(ui.ctx(),self.id.with("animation"), is_expanded);if0.0 == how_expanded {None}elseif how_expanded < 1.0{// Show a fake panel in this in-between animation state:// TODO(emilk): move the panel out-of-screen instead of changing its width.// Then we can actually paint it as it animates.let expanded_width = PanelState::load(ui.ctx(),self.id).map_or(self.default_width, |state| state.rect.width());let fake_width = how_expanded * expanded_width;Self{id:self.id.with("animating_panel"),
..self}.resizable(false).exact_width(fake_width).show_inside(ui, |_ui| {});None}else{// Show the real panel:Some(self.show_inside(ui, add_contents))}}/// Show either a collapsed or a expanded panel, with a nice animation between.fnshow_animated_between<R>(ctx:&Context,is_expanded:bool,collapsed_panel:Self,expanded_panel:Self,add_contents:implFnOnce(&mutUi,f32) -> R,) -> Option<InnerResponse<R>>{let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);if0.0 == how_expanded {Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))}elseif how_expanded < 1.0{// Show animation:let collapsed_width = PanelState::load(ctx, collapsed_panel.id).map_or(collapsed_panel.default_width, |state| state.rect.width());let expanded_width = PanelState::load(ctx, expanded_panel.id).map_or(expanded_panel.default_width, |state| state.rect.width());let fake_width = lerp(collapsed_width..=expanded_width, how_expanded);Self{id: expanded_panel.id.with("animating_panel"),
..expanded_panel
}.resizable(false).exact_width(fake_width).show(ctx, |ui| add_contents(ui, how_expanded));None}else{Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded)))}}/// Show either a collapsed or a expanded panel, with a nice animation between.fnshow_animated_between_inside<R>(ui:&mutUi,is_expanded:bool,collapsed_panel:Self,expanded_panel:Self,add_contents:implFnOnce(&mutUi,f32) -> R,) -> InnerResponse<R>{let how_expanded =
animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);if0.0 == how_expanded {
collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))}elseif how_expanded < 1.0{// Show animation:let collapsed_width = PanelState::load(ui.ctx(), collapsed_panel.id).map_or(collapsed_panel.default_width, |state| state.rect.width());let expanded_width = PanelState::load(ui.ctx(), expanded_panel.id).map_or(expanded_panel.default_width, |state| state.rect.width());let fake_width = lerp(collapsed_width..=expanded_width, how_expanded);Self{id: expanded_panel.id.with("animating_panel"),
..expanded_panel
}.resizable(false).exact_width(fake_width).show_inside(ui, |ui| add_contents(ui, how_expanded))}else{
expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))}}}implVerticalPanel{/// Show the panel inside a [`Ui`].fnshow_inside_dyn<'c,R>(self,ui:&mutUi,add_contents:Box<dynFnOnce(&mutUi) -> R + 'c>,) -> InnerResponse<R>{letSelf{
side,
id,
frame,
resizable,
show_separator_line,
default_width,
width_range,} = self;let available_rect = ui.available_rect_before_wrap();letmut panel_rect = available_rect;letmut width = default_width;{ifletSome(state) = PanelState::load(ui.ctx(), id){
width = state.rect.width();}
width = clamp_to_range(width, width_range).at_most(available_rect.width());
side.set_rect_width(&mut panel_rect, width);
ui.ctx().check_for_id_clash(id, panel_rect,"SidePanel");}let resize_id = id.with("__resize");letmut resize_hover = false;letmut is_resizing = false;if resizable {// First we read the resize interaction results, to avoid frame latency in the resize:ifletSome(resize_response) = ui.ctx().read_response(resize_id){
resize_hover = resize_response.hovered();
is_resizing = resize_response.dragged();if is_resizing {ifletSome(pointer) = resize_response.interact_pointer_pos(){
width = (pointer.x - side.side_x(panel_rect)).abs();
width = clamp_to_range(width, width_range).at_most(available_rect.width());
side.set_rect_width(&mut panel_rect, width);}}}}letmut panel_ui = ui.new_child(UiBuilder::new().id_salt(id).ui_stack_info(UiStackInfo::new(match side {VerticalSide::Left => UiKind::LeftPanel,VerticalSide::Right => UiKind::RightPanel,})).max_rect(panel_rect).layout(Layout::top_down(Align::Min)),);
panel_ui.expand_to_include_rect(panel_rect);
panel_ui.set_clip_rect(panel_rect);// If we overflow, don't do so visibly (#4475)let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));let inner_response = frame.show(&mut panel_ui, |ui| {
ui.set_min_height(ui.max_rect().height());// Make sure the frame fills the full height
ui.set_min_width((width_range.min - frame.inner_margin.sum().x).at_least(0.0));add_contents(ui)});let rect = inner_response.response.rect;{letmut cursor = ui.cursor();match side {VerticalSide::Left => {
cursor.min.x = rect.max.x;}VerticalSide::Right => {
cursor.max.x = rect.min.x;}}
ui.set_cursor(cursor);}
ui.expand_to_include_rect(rect);if resizable {// Now we do the actual resize interaction, on top of all the contents.// Otherwise its input could be eaten by the contents, e.g. a// `ScrollArea` on either side of the panel boundary.let resize_x = side.opposite().side_x(panel_rect);let resize_rect = Rect::from_x_y_ranges(resize_x..=resize_x, panel_rect.y_range()).expand2(vec2(ui.style().interaction.resize_grab_radius_side,0.0));let resize_response = ui.interact(resize_rect, resize_id,Sense::drag());
resize_hover = resize_response.hovered();
is_resizing = resize_response.dragged();}if resize_hover || is_resizing {let cursor_icon = if width <= width_range.min{matchself.side{VerticalSide::Left => CursorIcon::ResizeEast,VerticalSide::Right => CursorIcon::ResizeWest,}}elseif width < width_range.max{CursorIcon::ResizeHorizontal}else{matchself.side{VerticalSide::Left => CursorIcon::ResizeWest,VerticalSide::Right => CursorIcon::ResizeEast,}};
ui.ctx().set_cursor_icon(cursor_icon);}PanelState{ rect }.store(ui.ctx(), id);{let stroke = if is_resizing {
ui.style().visuals.widgets.active.fg_stroke// highly visible}elseif resize_hover {
ui.style().visuals.widgets.hovered.fg_stroke// highly visible}elseif show_separator_line {// TODO(emilk): distinguish resizable from non-resizable
ui.style().visuals.widgets.noninteractive.bg_stroke// dim}else{Stroke::NONE};// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is donelet resize_x = side.opposite().side_x(rect);// This makes it pixel-perfect for odd-sized strokes (width=1.0, width=3.0, etc)let resize_x = ui.painter().round_to_pixel_center(resize_x);// We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for// left-side panelslet resize_x = resize_x - if side == VerticalSide::Left{1.0}else{0.0};
ui.painter().vline(resize_x, panel_rect.y_range(), stroke);}
inner_response
}/// Show the panel at the top level.fnshow_dyn<'c,R>(self,ctx:&Context,add_contents:Box<dynFnOnce(&mutUi) -> R + 'c>,) -> InnerResponse<R>{let side = self.side;let available_rect = ctx.available_rect();letmut panel_ui = Ui::new(
ctx.clone(),self.id,UiBuilder::new().layer_id(LayerId::background()).max_rect(available_rect),);
panel_ui.set_clip_rect(ctx.screen_rect());let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);let rect = inner_response.response.rect;match side {VerticalSide::Left => ctx.pass_state_mut(|state| {
state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));}),VerticalSide::Right => ctx.pass_state_mut(|state| {
state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));}),}
inner_response
}}// ----------------------------------------------------------------------------/// [`Top`](HorizontalSide::Top) or [`Bottom`](HorizontalSide::Bottom)#[derive(Clone,Copy,Debug,PartialEq,Eq)]pubenumHorizontalSide{Top,Bottom,}implSideforHorizontalSide{fnopposite(self) -> Self{matchself{Self::Top => Self::Bottom,Self::Bottom => Self::Top,}}}implHorizontalSide{fnset_rect_height(self,rect:&mutRect,height:f32){matchself{Self::Top => rect.max.y = rect.min.y + height,Self::Bottom => rect.min.y = rect.max.y - height,}}fnside_y(self,rect:Rect) -> f32{matchself{Self::Top => rect.top(),Self::Bottom => rect.bottom(),}}}/// A panel that covers the entire top or bottom of a [`Ui`] or screen.////// 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`] last.////// See the [module level docs](crate::containers::panel) for more details.////// ```/// # egui::__run_test_ctx(|ctx| {/// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {/// ui.label("Hello World!");/// });/// # });/// ```////// See also [`VerticalPanel`].#[must_use = "You should call .show()"]pubstructHorizontalPanel{side:HorizontalSide,id:Id,frame:Option<Frame>,resizable:bool,show_separator_line:bool,default_height:Option<f32>,height_range:Rangef,}implPanelOptionsforHorizontalPanel{fnnew(side:HorizontalSide,id:implInto<Id>) -> Self{Self{
side,id: id.into(),frame:None,resizable:false,show_separator_line:true,default_height:None,height_range:Rangef::new(20.0, f32::INFINITY),}}#[inline]fnresizable(mutself,resizable:bool) -> Self{self.resizable = resizable;self}#[inline]fnshow_separator_line(mutself,show_separator_line:bool) -> Self{self.show_separator_line = show_separator_line;self}#[inline]fnframe(mutself,frame:Frame) -> Self{self.frame = Some(frame);self}}implPanelSizeforHorizontalPanel{fndefault_size(mutself,default_size:f32) -> Self{self.default_height(default_size)}fnmin_size(mutself,min_size:f32) -> Self{self.min_height(min_size)}fnmax_size(mutself,max_size:f32) -> Self{self.max_height(max_size)}fnsize_range(mutself,size_range:implInto<Rangef>) -> Self{self.height_range(size_range)}fnexact_size(mutself,size:f32) -> Self{self.exact_height(size)}}implHorizontalPanel{/// The id should be globally unique, e.g. `Id::new("my_top_panel")`.pubfntop(id:implInto<Id>) -> Self{Self::new(HorizontalSide::Top, id)}/// The id should be globally unique, e.g. `Id::new("my_bottom_panel")`.pubfnbottom(id:implInto<Id>) -> Self{Self::new(HorizontalSide::Bottom, id)}/// The initial height of the [`HorizontalPanel`], including margins./// Defaults to [`crate::style::Spacing::interact_size`].y, plus frame margins.#[inline]pubfndefault_height(mutself,default_height:f32) -> Self{self.default_height = Some(default_height);self.height_range = Rangef::new(self.height_range.min.at_most(default_height),self.height_range.max.at_least(default_height),);self}/// Minimum height of the panel, including margins.#[inline]pubfnmin_height(mutself,min_height:f32) -> Self{self.height_range = Rangef::new(min_height,self.height_range.max.at_least(min_height));self}/// Maximum height of the panel, including margins.#[inline]pubfnmax_height(mutself,max_height:f32) -> Self{self.height_range = Rangef::new(self.height_range.min.at_most(max_height), max_height);self}/// The allowable height range for the panel, including margins.#[inline]pubfnheight_range(mutself,height_range:implInto<Rangef>) -> Self{let height_range = height_range.into();self.default_height = self.default_height.map(|default_height| clamp_to_range(default_height, height_range));self.height_range = height_range;self}/// Enforce this exact height, including margins.#[inline]pubfnexact_height(mutself,height:f32) -> Self{self.default_height = Some(height);self.height_range = Rangef::point(height);self}}implPanelActionsforHorizontalPanel{/// Show the panel inside a [`Ui`].fnshow_inside<R>(self,ui:&mutUi,add_contents:implFnOnce(&mutUi) -> R,) -> InnerResponse<R>{self.show_inside_dyn(ui,Box::new(add_contents))}/// Show the panel at the top level.fnshow<R>(self,ctx:&Context,add_contents:implFnOnce(&mutUi) -> R) -> InnerResponse<R>{self.show_dyn(ctx,Box::new(add_contents))}/// Show the panel if `is_expanded` is `true`,/// otherwise don't show it, but with a nice animation between collapsed and expanded.fnshow_animated<R>(self,ctx:&Context,is_expanded:bool,add_contents:implFnOnce(&mutUi) -> R,) -> Option<InnerResponse<R>>{let how_expanded = animate_expansion(ctx,self.id.with("animation"), is_expanded);if0.0 == how_expanded {None}elseif how_expanded < 1.0{// Show a fake panel in this in-between animation state:// TODO(emilk): move the panel out-of-screen instead of changing its height.// Then we can actually paint it as it animates.let expanded_height = PanelState::load(ctx,self.id).map(|state| state.rect.height()).or(self.default_height).unwrap_or_else(|| ctx.style().spacing.interact_size.y);let fake_height = how_expanded * expanded_height;Self{id:self.id.with("animating_panel"),
..self}.resizable(false).exact_height(fake_height).show(ctx, |_ui| {});None}else{// Show the real panel:Some(self.show(ctx, add_contents))}}/// Show the panel if `is_expanded` is `true`,/// otherwise don't show it, but with a nice animation between collapsed and expanded.fnshow_animated_inside<R>(self,ui:&mutUi,is_expanded:bool,add_contents:implFnOnce(&mutUi) -> R,) -> Option<InnerResponse<R>>{let how_expanded = animate_expansion(ui.ctx(),self.id.with("animation"), is_expanded);if0.0 == how_expanded {None}elseif how_expanded < 1.0{// Show a fake panel in this in-between animation state:// TODO(emilk): move the panel out-of-screen instead of changing its height.// Then we can actually paint it as it animates.let expanded_height = PanelState::load(ui.ctx(),self.id).map(|state| state.rect.height()).or(self.default_height).unwrap_or_else(|| ui.style().spacing.interact_size.y);let fake_height = how_expanded * expanded_height;Self{id:self.id.with("animating_panel"),
..self}.resizable(false).exact_height(fake_height).show_inside(ui, |_ui| {});None}else{// Show the real panel:Some(self.show_inside(ui, add_contents))}}/// Show either a collapsed or a expanded panel, with a nice animation between.fnshow_animated_between<R>(ctx:&Context,is_expanded:bool,collapsed_panel:Self,expanded_panel:Self,add_contents:implFnOnce(&mutUi,f32) -> R,) -> Option<InnerResponse<R>>{let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);if0.0 == how_expanded {Some(collapsed_panel.show(ctx, |ui| add_contents(ui, how_expanded)))}elseif how_expanded < 1.0{// Show animation:let collapsed_height = PanelState::load(ctx, collapsed_panel.id).map(|state| state.rect.height()).or(collapsed_panel.default_height).unwrap_or_else(|| ctx.style().spacing.interact_size.y);let expanded_height = PanelState::load(ctx, expanded_panel.id).map(|state| state.rect.height()).or(expanded_panel.default_height).unwrap_or_else(|| ctx.style().spacing.interact_size.y);let fake_height = lerp(collapsed_height..=expanded_height, how_expanded);Self{id: expanded_panel.id.with("animating_panel"),
..expanded_panel
}.resizable(false).exact_height(fake_height).show(ctx, |ui| add_contents(ui, how_expanded));None}else{Some(expanded_panel.show(ctx, |ui| add_contents(ui, how_expanded)))}}/// Show either a collapsed or a expanded panel, with a nice animation between.fnshow_animated_between_inside<R>(ui:&mutUi,is_expanded:bool,collapsed_panel:Self,expanded_panel:Self,add_contents:implFnOnce(&mutUi,f32) -> R,) -> InnerResponse<R>{let how_expanded =
animate_expansion(ui.ctx(), expanded_panel.id.with("animation"), is_expanded);if0.0 == how_expanded {
collapsed_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))}elseif how_expanded < 1.0{// Show animation:let collapsed_height = PanelState::load(ui.ctx(), collapsed_panel.id).map(|state| state.rect.height()).or(collapsed_panel.default_height).unwrap_or_else(|| ui.style().spacing.interact_size.y);let expanded_height = PanelState::load(ui.ctx(), expanded_panel.id).map(|state| state.rect.height()).or(expanded_panel.default_height).unwrap_or_else(|| ui.style().spacing.interact_size.y);let fake_height = lerp(collapsed_height..=expanded_height, how_expanded);Self{id: expanded_panel.id.with("animating_panel"),
..expanded_panel
}.resizable(false).exact_height(fake_height).show_inside(ui, |ui| add_contents(ui, how_expanded))}else{
expanded_panel.show_inside(ui, |ui| add_contents(ui, how_expanded))}}}implHorizontalPanel{/// Show the panel inside a [`Ui`].fnshow_inside_dyn<'c,R>(self,ui:&mutUi,add_contents:Box<dynFnOnce(&mutUi) -> R + 'c>,) -> InnerResponse<R>{letSelf{
side,
id,
frame,
resizable,
show_separator_line,
default_height,
height_range,} = self;let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));let available_rect = ui.available_rect_before_wrap();letmut panel_rect = available_rect;letmut height = ifletSome(state) = PanelState::load(ui.ctx(), id){
state.rect.height()}else{
default_height
.unwrap_or_else(|| ui.style().spacing.interact_size.y + frame.inner_margin.sum().y)};{
height = clamp_to_range(height, height_range).at_most(available_rect.height());
side.set_rect_height(&mut panel_rect, height);
ui.ctx().check_for_id_clash(id, panel_rect,"TopBottomPanel");}let resize_id = id.with("__resize");letmut resize_hover = false;letmut is_resizing = false;if resizable {// First we read the resize interaction results, to avoid frame latency in the resize:ifletSome(resize_response) = ui.ctx().read_response(resize_id){
resize_hover = resize_response.hovered();
is_resizing = resize_response.dragged();if is_resizing {ifletSome(pointer) = resize_response.interact_pointer_pos(){
height = (pointer.y - side.side_y(panel_rect)).abs();
height =
clamp_to_range(height, height_range).at_most(available_rect.height());
side.set_rect_height(&mut panel_rect, height);}}}}letmut panel_ui = ui.new_child(UiBuilder::new().id_salt(id).ui_stack_info(UiStackInfo::new(match side {HorizontalSide::Top => UiKind::TopPanel,HorizontalSide::Bottom => UiKind::BottomPanel,})).max_rect(panel_rect).layout(Layout::top_down(Align::Min)),);
panel_ui.expand_to_include_rect(panel_rect);
panel_ui.set_clip_rect(panel_rect);// If we overflow, don't do so visibly (#4475)let inner_response = frame.show(&mut panel_ui, |ui| {
ui.set_min_width(ui.max_rect().width());// Make the frame fill full width
ui.set_min_height((height_range.min - frame.inner_margin.sum().y).at_least(0.0));add_contents(ui)});let rect = inner_response.response.rect;{letmut cursor = ui.cursor();match side {HorizontalSide::Top => {
cursor.min.y = rect.max.y;}HorizontalSide::Bottom => {
cursor.max.y = rect.min.y;}}
ui.set_cursor(cursor);}
ui.expand_to_include_rect(rect);if resizable {// Now we do the actual resize interaction, on top of all the contents.// Otherwise its input could be eaten by the contents, e.g. a// `ScrollArea` on either side of the panel boundary.let resize_y = side.opposite().side_y(panel_rect);let resize_rect = Rect::from_x_y_ranges(panel_rect.x_range(), resize_y..=resize_y).expand2(vec2(0.0, ui.style().interaction.resize_grab_radius_side));let resize_response = ui.interact(resize_rect, resize_id,Sense::drag());
resize_hover = resize_response.hovered();
is_resizing = resize_response.dragged();}if resize_hover || is_resizing {let cursor_icon = if height <= height_range.min{matchself.side{HorizontalSide::Top => CursorIcon::ResizeSouth,HorizontalSide::Bottom => CursorIcon::ResizeNorth,}}elseif height < height_range.max{CursorIcon::ResizeVertical}else{matchself.side{HorizontalSide::Top => CursorIcon::ResizeNorth,HorizontalSide::Bottom => CursorIcon::ResizeSouth,}};
ui.ctx().set_cursor_icon(cursor_icon);}PanelState{ rect }.store(ui.ctx(), id);{let stroke = if is_resizing {
ui.style().visuals.widgets.active.fg_stroke// highly visible}elseif resize_hover {
ui.style().visuals.widgets.hovered.fg_stroke// highly visible}elseif show_separator_line {// TODO(emilk): distinguish resizable from non-resizable
ui.style().visuals.widgets.noninteractive.bg_stroke// dim}else{Stroke::NONE};// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is donelet resize_y = side.opposite().side_y(rect);// This makes it pixel-perfect for odd-sized strokes (width=1.0, width=3.0, etc)let resize_y = ui.painter().round_to_pixel_center(resize_y);// We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for// top-side panelslet resize_y = resize_y
- if side == HorizontalSide::Top{1.0}else{0.0};
ui.painter().hline(panel_rect.x_range(), resize_y, stroke);}
inner_response
}/// Show the panel at the top level.fnshow_dyn<'c,R>(self,ctx:&Context,add_contents:Box<dynFnOnce(&mutUi) -> R + 'c>,) -> InnerResponse<R>{let available_rect = ctx.available_rect();let side = self.side;letmut panel_ui = Ui::new(
ctx.clone(),self.id,UiBuilder::new().layer_id(LayerId::background()).max_rect(available_rect),);
panel_ui.set_clip_rect(ctx.screen_rect());let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);let rect = inner_response.response.rect;match side {HorizontalSide::Top => {
ctx.pass_state_mut(|state| {
state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));});}HorizontalSide::Bottom => {
ctx.pass_state_mut(|state| {
state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));});}}
inner_response
}}// ----------------------------------------------------------------------------/// A panel that covers the remainder of the screen,/// i.e. whatever area is left after adding other panels.////// 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.////// ⚠ [`CentralPanel`] must be added after all other panels!////// NOTE: Any [`crate::Window`]s and [`crate::Area`]s will cover the top-level [`CentralPanel`].////// See the [module level docs](crate::containers::panel) for more details.////// ```/// # egui::__run_test_ctx(|ctx| {/// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {/// ui.label("Hello World! From `TopBottomPanel`, that must be before `CentralPanel`!");/// });/// egui::CentralPanel::default().show(ctx, |ui| {/// ui.label("Hello World!");/// });/// # });/// ```#[must_use = "You should call .show()"]#[derive(Default)]pubstructCentralPanel{frame:Option<Frame>,}implCentralPanel{/// Change the background color, margins, etc.#[inline]pubfnframe(mutself,frame:Frame) -> Self{self.frame = Some(frame);self}}implCentralPanel{/// Show the panel inside a [`Ui`].pubfnshow_inside<R>(self,ui:&mutUi,add_contents:implFnOnce(&mutUi) -> R,) -> InnerResponse<R>{self.show_inside_dyn(ui,Box::new(add_contents))}/// Show the panel inside a [`Ui`].fnshow_inside_dyn<'c,R>(self,ui:&mutUi,add_contents:Box<dynFnOnce(&mutUi) -> R + 'c>,) -> InnerResponse<R>{letSelf{ frame } = self;let panel_rect = ui.available_rect_before_wrap();letmut panel_ui = ui.new_child(UiBuilder::new().ui_stack_info(UiStackInfo::new(UiKind::CentralPanel)).max_rect(panel_rect).layout(Layout::top_down(Align::Min)),);
panel_ui.set_clip_rect(panel_rect);// If we overflow, don't do so visibly (#4475)let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style()));
frame.show(&mut panel_ui, |ui| {
ui.expand_to_include_rect(ui.max_rect());// Expand frame to include it alladd_contents(ui)})}/// Show the panel at the top level.pubfnshow<R>(self,ctx:&Context,add_contents:implFnOnce(&mutUi) -> R,) -> InnerResponse<R>{self.show_dyn(ctx,Box::new(add_contents))}/// Show the panel at the top level.fnshow_dyn<'c,R>(self,ctx:&Context,add_contents:Box<dynFnOnce(&mutUi) -> R + 'c>,) -> InnerResponse<R>{let available_rect = ctx.available_rect();let id = Id::new((ctx.viewport_id(),"central_panel"));letmut panel_ui = Ui::new(
ctx.clone(),
id,UiBuilder::new().layer_id(LayerId::background()).max_rect(available_rect),);
panel_ui.set_clip_rect(ctx.screen_rect());let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);// Only inform ctx about what we actually used, so we can shrink the native window to fit.
ctx.pass_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));
inner_response
}}fnclamp_to_range(x:f32,range:Rangef) -> f32{let range = range.as_positive();
x.clamp(range.min, range.max)}
Describe alternatives you've considered
Having a wrapper that duplicate the functions for either TopBottom and Side, going a bit against the goal I wanted.
Additional context
I'm really new to Rust, for forgive me if my proposal is a Rustaceans crime 😆.
The text was updated successfully, but these errors were encountered:
Is your feature request related to a problem? Please describe.
I am trying to create a library around both
SidePanel
andTopBottomPanel
so that they have the same behavior when used in a specific way (my specific case is toolbars and properties panels). Having two distinct type makes it hard to create such wrapper that abstract the specific of each of them.In my use case, the toolbar will have a fixed width/height and will not be resizable. The use of the wrapper allows me to use a pre-made toolbar instead of duplicating options code.
Describe the solution you'd like
Seeing that the panels
impl
have almost the same signature, I think it make sense to create a trait so that we can create generic wrapper usingimpl trait type
Also, on a personal opinion, I think the name
TopBottom
andSide
are a bit confusing at first glance, even more when nesting panels, making it seems that having aBottomPanel
inside aTopPanel
inside aLeftPanel
is not possible. They could be renamed toHorizontalPanel
(for theTopBottomPanel
) andVerticalPanel
(for theSidePanel
). That way, no matter the nesting they'll always be horizontal or vertical.The changes would be in crates/egui/src/containers/panel.rs
I can create a pull requests, but chances that the changes I made are not working as-is (excluding the breaking change of the names), me being a noob in Rust 😉
Draft code change proposal
Describe alternatives you've considered
Having a wrapper that duplicate the functions for either
TopBottom
andSide
, going a bit against the goal I wanted.Additional context
I'm really new to Rust, for forgive me if my proposal is a Rustaceans crime 😆.
The text was updated successfully, but these errors were encountered: