diff --git a/crates/egui_extras/src/dock/branch/grid.rs b/crates/egui_extras/src/dock/branch/grid.rs index a101c1fbf..f4e9de13c 100644 --- a/crates/egui_extras/src/dock/branch/grid.rs +++ b/crates/egui_extras/src/dock/branch/grid.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet}; use egui::{emath::Rangef, pos2, vec2, NumExt as _, Rect}; use itertools::Itertools as _; @@ -28,15 +28,45 @@ pub struct GridLoc { } impl GridLoc { + #[inline] pub fn from_col_row(col: usize, row: usize) -> Self { Self { col, row } } } +#[derive( + Clone, + Copy, + Debug, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + serde::Serialize, + serde::Deserialize, +)] +pub enum GridLayout { + /// Place children in a grid, with a dynamic number of columns and rows. + /// Resizing the window may change the number of columns and rows. + #[default] + Auto, + + /// Place children in a grid with this many columns, + /// and as many rows as needed. + Columns(usize), +} + #[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] pub struct Grid { pub children: Vec, + pub layout: GridLayout, + + /// Where each child is located. + /// + /// If the chils is missing from this set, it will be assgined a location during layout. pub locations: HashMap, /// Share of the available width assigned to each column. @@ -72,22 +102,30 @@ impl Grid { behavior: &mut dyn Behavior, rect: Rect, ) { + let gap = behavior.gap_width(style); let child_ids: HashSet = self.children.iter().copied().collect(); - let mut num_cols = 2.min(self.children.len()); // les than 2 and it is not a grid + let num_cols = match self.layout { + GridLayout::Auto => num_columns_heuristic(self.children.len(), rect, gap), + GridLayout::Columns(num_columns) => num_columns.at_least(1), + }; + let num_rows = (self.children.len() + num_cols - 1) / num_cols; // Where to place each node? let mut node_id_from_location: BTreeMap = Default::default(); self.locations.retain(|&child_id, &mut loc| { if child_ids.contains(&child_id) { match node_id_from_location.entry(loc) { - std::collections::btree_map::Entry::Occupied(_) => { + btree_map::Entry::Occupied(_) => { false // two nodes assigned to the same position - forget this one for now } - std::collections::btree_map::Entry::Vacant(entry) => { - entry.insert(child_id); - num_cols = num_cols.max(loc.col + 1); - true + btree_map::Entry::Vacant(entry) => { + if num_cols <= loc.col || num_rows <= loc.row { + false // out of bounds + } else { + entry.insert(child_id); + true + } } } } else { @@ -98,8 +136,7 @@ impl Grid { // Find location for nodes that don't have one yet let mut next_pos = 0; for &child_id in &self.children { - if let std::collections::hash_map::Entry::Vacant(entry) = self.locations.entry(child_id) - { + if let hash_map::Entry::Vacant(entry) = self.locations.entry(child_id) { // find a position: loop { let loc = GridLoc::from_col_row(next_pos % num_cols, next_pos / num_cols); @@ -121,7 +158,6 @@ impl Grid { self.col_shares.resize(num_cols, 1.0); self.row_shares.resize(num_rows, 1.0); - let gap = behavior.gap_width(style); let col_widths = sizes_from_shares(&self.col_shares, rect.width(), gap); let row_heights = sizes_from_shares(&self.row_shares, rect.height(), gap); @@ -271,6 +307,34 @@ impl Grid { } } +/// How many columns should we use to fit `n` children in a grid? +fn num_columns_heuristic(n: usize, rect: Rect, gap: f32) -> usize { + let desired_aspect = 4.0 / 3.0; + + let mut best_loss = f32::INFINITY; + let mut best_num_columns = 1; + + for ncols in 1..=n { + let nrows = (n + ncols - 1) / ncols; + + let cell_width = (rect.width() - gap * (ncols as f32 - 1.0)) / (ncols as f32); + let cell_height = (rect.height() - gap * (nrows as f32 - 1.0)) / (nrows as f32); + + let cell_aspect = cell_width / cell_height; + let aspect_diff = (desired_aspect - cell_aspect).abs(); + let num_empty_cells = ncols * nrows - n; + + let loss = aspect_diff + 0.1 * num_empty_cells as f32; // TODO(emilk): weight differently? + + if loss < best_loss { + best_loss = loss; + best_num_columns = ncols; + } + } + + best_num_columns +} + fn resize_interaction( behavior: &mut dyn Behavior, ranges: &[Rangef], diff --git a/examples/dock/src/main.rs b/examples/dock/src/main.rs index a12dca22c..e5d958db9 100644 --- a/examples/dock/src/main.rs +++ b/examples/dock/src/main.rs @@ -182,7 +182,7 @@ impl Default for MyApp { nodes.insert_vertical_node(children) }); tabs.push({ - let cells = (0..12).map(|_| nodes.insert_leaf(gen_view())).collect(); + let cells = (0..11).map(|_| nodes.insert_leaf(gen_view())).collect(); nodes.insert_grid_node(cells) }); tabs.push(nodes.insert_leaf(gen_view()));