use std::boxed::Box;
use std::collections::HashMap;
use std::hash::Hash;
use egui::scroll_area::ScrollBarVisibility;
use egui::{Color32, CornerRadius, Id, PointerButton, Rect, ScrollArea, Sense, StrokeKind, Ui, UiBuilder, Vec2};
#[derive(Debug)]
pub struct VerticalStack {
id_source: Id,
max_height: Option<f32>,
min_panel_height: f32,
max_panel_height: Option<f32>,
default_panel_height: f32,
scroll_bar_visibility: ScrollBarVisibility,
framed: bool,
inner_margin: f32,
panel_heights: HashMap<Id, f32>,
content_sizes: HashMap<Id, (f32, f32)>,
active_drag_handle: Option<usize>,
drag_start_y: Option<f32>,
drag_start_height: Option<f32>,
initialized: bool,
last_available_height: f32,
last_panel_count: usize,
}
impl VerticalStack {
pub fn new() -> Self {
Self {
id_source: Id::new("vertical_stack"),
max_height: None,
min_panel_height: 50.0,
max_panel_height: None,
default_panel_height: 100.0,
scroll_bar_visibility: ScrollBarVisibility::VisibleWhenNeeded,
inner_margin: 4.0,
framed: true,
panel_heights: HashMap::new(),
content_sizes: HashMap::new(),
active_drag_handle: None,
drag_start_y: None,
drag_start_height: None,
initialized: false,
last_available_height: 0.0,
last_panel_count: 0,
}
}
pub fn id_salt(&mut self, id: impl Hash) -> &mut Self {
self.id_source = Id::new(id);
self
}
pub fn inner_margin(mut self, inner_margin: f32) -> Self {
self.inner_margin = inner_margin;
self
}
pub fn framed(mut self, framed: bool) -> Self {
self.framed = framed;
self
}
pub fn max_height(mut self, height: Option<f32>) -> Self {
self.max_height = height;
self
}
pub fn min_panel_height(mut self, height: f32) -> Self {
self.min_panel_height = height;
self
}
pub fn max_panel_height(mut self, height: Option<f32>) -> Self {
self.max_panel_height = height;
self
}
pub fn default_panel_height(mut self, height: f32) -> Self {
self.default_panel_height = height;
self
}
pub fn scroll_bar_visibility(mut self, visibility: ScrollBarVisibility) -> Self {
self.scroll_bar_visibility = visibility;
self
}
pub fn body<F>(&mut self, ui: &mut Ui, mut collect_panels: F)
where
F: FnMut(&mut StackBodyBuilder),
{
let available_rect = ui.available_rect_before_wrap();
let available_height = match self.max_height {
Some(max_height) => max_height.min(available_rect.height()),
None => available_rect.height(),
};
let mut body = StackBodyBuilder {
panels: Vec::new(),
};
collect_panels(&mut body);
let panel_count = body.panels.len();
if panel_count == 0 {
return;
}
let seen_all_panels = body
.panels
.iter()
.all(|(id, _fn)| self.panel_heights.contains_key(id));
if !self.initialized
|| (self.last_available_height - available_height).abs() > 1.0
|| panel_count != self.last_panel_count
|| !seen_all_panels
{
self.last_available_height = available_height;
self.last_panel_count = panel_count;
self.do_sizing_pass(ui, &mut body);
ui.ctx().request_discard("sizing");
self.initialized = true;
return;
}
self.do_render_pass(ui, body, available_height);
}
fn do_sizing_pass(&mut self, ui: &mut Ui, body: &mut StackBodyBuilder) {
ui.allocate_ui(Vec2::new(ui.available_width(), 0.0), |ui| {
let panel_width = ui.available_width();
for (hash, panel_fn) in body.panels.iter_mut() {
let mut panel_height = *self
.panel_heights
.get(hash)
.unwrap_or(&self.default_panel_height);
panel_height = panel_height.max(self.min_panel_height);
if let Some(max_panel_height) = self.max_panel_height {
panel_height = panel_height.min(max_panel_height);
}
let panel_rect = Rect::from_min_size(ui.cursor().min, Vec2::new(panel_width, f32::MAX));
let mut measuring_ui = ui.new_child(
UiBuilder::new()
.max_rect(panel_rect)
.sizing_pass(),
);
panel_fn(&mut measuring_ui);
let content_rect = measuring_ui.min_rect();
let content_height = content_rect.height();
self.content_sizes
.insert(*hash, (content_rect.width(), content_height));
self.panel_heights
.insert(*hash, panel_height);
}
});
}
fn do_render_pass(&mut self, ui: &mut Ui, body: StackBodyBuilder, available_height: f32) {
let pointer_is_down = ui.input(|i| {
i.pointer
.button_down(PointerButton::Primary)
});
if self.active_drag_handle.is_some() && !pointer_is_down {
self.active_drag_handle = None;
}
let (max_content_width, _max_content_height) = self
.content_sizes
.values()
.fold((0.0_f32, 0.0_f32), |acc, (w, h)| (acc.0.max(*w), acc.1.max(*h)));
ScrollArea::both()
.id_salt(self.id_source.with("scroll_area"))
.max_height(available_height)
.scroll_bar_visibility(self.scroll_bar_visibility)
.auto_shrink([false, true])
.show(ui, |ui| {
let scroll_area_rect_before_wrap = ui.available_rect_before_wrap();
#[cfg(feature = "layout_debugging")]
{
let debug_stroke = Stroke::new(1.0, Color32::PURPLE);
ui.painter().rect(
scroll_area_rect_before_wrap,
CornerRadius::ZERO,
Color32::TRANSPARENT,
debug_stroke,
StrokeKind::Outside,
);
}
ui.spacing_mut().item_spacing.y = 0.0;
let panel_rect = ui.available_rect_before_wrap();
let style = ui.style();
let frame_stroke = style.visuals.widgets.noninteractive.bg_stroke;
for (idx, (id, mut panel_fn)) in body.panels.into_iter().enumerate() {
let panel_height = *self.panel_heights.get(&id).unwrap();
let desired_size = Vec2::new(panel_rect.width(), panel_height);
ui.allocate_ui(desired_size, |ui| {
ui.set_min_height(panel_height);
let stroke_width = if self.framed {
frame_stroke.width
} else {
0_f32
};
let intial_frame_width = max_content_width + ((self.inner_margin + stroke_width) * 2.0);
let frame_width = intial_frame_width.max(scroll_area_rect_before_wrap.width());
ui.set_min_width(frame_width);
let mut frame_rect = ui.max_rect();
frame_rect.max.x = frame_rect.min.x + frame_width;
frame_rect.max.y = frame_rect.min.y + panel_height;
if self.framed {
ui.painter().rect(
frame_rect,
CornerRadius::ZERO,
Color32::TRANSPARENT,
frame_stroke,
StrokeKind::Outside,
);
}
let content_rect = frame_rect.shrink(self.inner_margin);
let mut panel_ui = ui.new_child(UiBuilder::new().max_rect(content_rect));
let panel_rect = content_rect.intersect(ui.clip_rect());
#[cfg(feature = "layout_debugging")]
{
let debug_stroke = Stroke::new(1.0, Color32::GREEN);
ui.painter().rect(
panel_rect,
CornerRadius::ZERO,
Color32::TRANSPARENT,
debug_stroke,
StrokeKind::Outside,
);
}
panel_ui.set_clip_rect(panel_rect);
panel_fn(&mut panel_ui);
});
self.add_resize_handle(ui, idx, &id);
}
});
}
fn add_resize_handle(&mut self, ui: &mut Ui, panel_idx: usize, id: &Id) {
let handle_height = 7.0;
let is_last_panel = panel_idx == self.panel_heights.len() - 1;
if !is_last_panel && (panel_idx >= self.panel_heights.len() || panel_idx + 1 >= self.panel_heights.len()) {
return;
}
let available_rect = ui.available_rect_before_wrap();
let handle_rect = Rect::from_min_size(available_rect.min, Vec2::new(available_rect.width(), handle_height));
let (rect, response) = ui.allocate_exact_size(handle_rect.size(), Sense::drag());
if response.hovered() || response.dragged() {
ui.ctx()
.set_cursor_icon(egui::CursorIcon::ResizeVertical);
}
let painter = ui.painter();
let handle_stroke = if response.hovered() || response.dragged() {
ui.style()
.visuals
.widgets
.active
.fg_stroke
} else {
ui.style()
.visuals
.widgets
.noninteractive
.bg_stroke
};
let line_y = rect.min.y + (rect.height() / 2.0);
painter.line_segment(
[
egui::Pos2::new(rect.left(), line_y),
egui::Pos2::new(rect.right(), line_y),
],
handle_stroke,
);
#[cfg(feature = "layout_debugging")]
{
let debug_stroke = Stroke::new(1.0, Color32::YELLOW);
painter.rect_stroke(rect, 0.0, debug_stroke, StrokeKind::Outside);
}
if response.drag_started() {
self.active_drag_handle = Some(panel_idx);
if let Some(pointer_pos) = ui.ctx().pointer_latest_pos() {
self.drag_start_y = Some(pointer_pos.y);
let panel_height = *self.panel_heights.get(id).unwrap();
self.drag_start_height = Some(panel_height);
}
return;
}
let is_active_handle = self.active_drag_handle == Some(panel_idx);
if !is_active_handle {
return;
}
if response.dragged() {
if let (Some(start_y), Some(start_height)) = (self.drag_start_y, self.drag_start_height) {
if let Some(current_pos) = ui.ctx().pointer_latest_pos() {
let panel_height = self.panel_heights.get_mut(id).unwrap();
let total_delta = current_pos.y - start_y;
let mut new_height = (start_height + total_delta).max(self.min_panel_height);
if let Some(max_panel_height) = self.max_panel_height {
new_height = new_height.min(max_panel_height);
}
*panel_height = new_height;
}
}
}
if response.drag_stopped() {
self.active_drag_handle = None;
self.drag_start_y = None;
self.drag_start_height = None;
}
}
}
pub struct StackBodyBuilder {
panels: Vec<(Id, Box<dyn FnMut(&mut Ui)>)>,
}
impl StackBodyBuilder {
pub fn add_panel<F>(&mut self, id_salt: impl Hash, add_contents: F)
where
F: FnMut(&mut Ui) + 'static,
{
let id = Id::new(id_salt);
self.panels
.push((id, Box::new(add_contents)));
}
}
impl Default for VerticalStack {
fn default() -> Self {
Self::new()
}
}