spiral.rs (7286B)
1 /// Spiral layout for media galleries 2 3 use egui::{pos2, vec2, Color32, Rect, Sense, TextureId, Vec2}; 4 5 #[derive(Clone, Copy, Debug)] 6 pub struct ImageItem { 7 pub texture: TextureId, 8 pub ar: f32, // width / height (must be > 0) 9 } 10 11 #[derive(Clone, Debug)] 12 struct Placed { 13 texture: TextureId, 14 rect: Rect, 15 } 16 17 #[derive(Clone, Copy, Debug)] 18 pub struct LayoutParams { 19 pub gutter: f32, 20 pub h_min: f32, 21 pub h_max: f32, 22 pub w_min: f32, 23 pub w_max: f32, 24 pub seed_center: bool, 25 } 26 27 pub fn layout_spiral(images: &[ImageItem], params: LayoutParams) -> (Vec<Placed>, Vec2) { 28 if images.is_empty() { 29 return (Vec::new(), vec2(0.0, 0.0)); 30 } 31 32 let eps = f32::EPSILON; 33 let g = params.gutter.max(0.0); 34 let h_min = params.h_min.max(1.0); 35 let h_max = params.h_max.max(h_min); 36 let w_min = params.w_min.max(1.0); 37 let w_max = params.w_max.max(w_min); 38 39 let mut placed = Vec::with_capacity(images.len()); 40 41 // Build around origin; normalize at the end. 42 let mut x_min = 0.0f32; 43 let mut x_max = 0.0f32; 44 let mut y_min = 0.0f32; 45 let mut y_max = 0.0f32; 46 47 // dir: 0 right-col, 1 top-row, 2 left-col, 3 bottom-row 48 let mut dir = 0usize; 49 let mut i = 0usize; 50 51 // Optional seed: center a single image 52 if params.seed_center && i < images.len() { 53 let ar = images[i].ar.max(eps); 54 let h = ((h_min + h_max) * 0.5).clamp(h_min, h_max); 55 let w = ar * h; 56 57 let rect = Rect::from_center_size(pos2(0.0, 0.0), vec2(w, h)); 58 placed.push(Placed { texture: images[i].texture, rect }); 59 60 x_min = rect.min.x; 61 x_max = rect.max.x; 62 y_min = rect.min.y; 63 y_max = rect.max.y; 64 65 i += 1; 66 dir = 1; // start by adding a row above 67 } else { 68 // ensure non-empty bbox for the first strip 69 x_min = 0.0; x_max = 1.0; y_min = 0.0; y_max = 1.0; 70 } 71 72 // --- helpers ------------------------------------------------------------- 73 74 // Choose how many items fit and the strip size S (W for column, H for row). 75 fn choose_k<F: Fn(&ImageItem) -> f32>( 76 images: &[ImageItem], 77 L: f32, 78 g: f32, 79 s_min: f32, 80 s_max: f32, 81 weight: F, 82 ) -> (usize, f32) { 83 // prefix sums of weights (sum over first k items) 84 let mut pref = Vec::with_capacity(images.len() + 1); 85 pref.push(0.0); 86 for im in images { 87 pref.push(pref.last().copied().unwrap_or(0.0) + weight(im)); 88 } 89 90 let k_max = images.len().max(1); 91 let mut chosen_k = 1usize; 92 let mut chosen_s = f32::NAN; 93 94 for k in 1..=k_max { 95 let L_eff = (L - g * (k as f32 - 1.0)).max(1.0); 96 let sum_w = pref[k].max(f32::EPSILON); 97 let s = (L_eff / sum_w).max(1.0); 98 99 if s > s_max && k < k_max { 100 continue; // too big; add one more to thin the strip 101 } 102 if s < s_min { 103 // prefer one fewer if possible 104 if k > 1 { 105 let k2 = k - 1; 106 let L_eff2 = (L - g * (k2 as f32 - 1.0)).max(1.0); 107 let sum_w2 = pref[k2].max(f32::EPSILON); 108 chosen_k = k2; 109 chosen_s = (L_eff2 / sum_w2).max(1.0); 110 } else { 111 chosen_k = 1; 112 chosen_s = s_min; 113 } 114 return (chosen_k, chosen_s); 115 } 116 return (k, s); // within bounds 117 } 118 119 // Fell through: use k_max and clamp 120 let L_eff = (L - g * (k_max as f32 - 1.0)).max(1.0); 121 let sum_w = pref[k_max].max(f32::EPSILON); 122 let s = (L_eff / sum_w).clamp(s_min, s_max); 123 (k_max, s) 124 } 125 126 // Place a column (top→bottom). Returns the new right/left edge. 127 fn place_column( 128 placed: &mut Vec<Placed>, 129 strip: &[ImageItem], 130 W: f32, 131 x: f32, 132 y_top: f32, 133 g: f32, 134 ) -> f32 { 135 let mut y = y_top; 136 for (idx, im) in strip.iter().enumerate() { 137 let h = (W / im.ar.max(f32::EPSILON)).max(1.0); 138 let rect = Rect::from_min_size(pos2(x, y), vec2(W, h)); 139 placed.push(Placed { texture: im.texture, rect }); 140 y += h; 141 if idx + 1 != strip.len() { y += g; } 142 } 143 x + W 144 } 145 146 // Place a row (left→right). Returns the new top/bottom edge. 147 fn place_row( 148 placed: &mut Vec<Placed>, 149 strip: &[ImageItem], 150 H: f32, 151 x_left: f32, 152 y: f32, 153 g: f32, 154 ) -> f32 { 155 let mut x = x_left; 156 for (idx, im) in strip.iter().enumerate() { 157 let w = (im.ar.max(f32::EPSILON) * H).max(1.0); 158 let rect = Rect::from_min_size(pos2(x, y), vec2(w, H)); 159 placed.push(Placed { texture: im.texture, rect }); 160 x += w; 161 if idx + 1 != strip.len() { x += g; } 162 } 163 y + H 164 } 165 166 // --- main loop ----------------------------------------------------------- 167 168 while i < images.len() { 169 let remaining = &images[i..]; 170 171 if dir % 2 == 0 { 172 // COLUMN (dir 0: right, 2: left) 173 let L = (y_max - y_min).max(1.0); 174 let (k, W) = choose_k( 175 remaining, 176 L, g, w_min, w_max, 177 |im| 1.0 / im.ar.max(f32::EPSILON), 178 ); 179 180 let x = if dir == 0 { x_max + g } else { x_min - g - W }; 181 let new_edge = place_column(&mut placed, &remaining[..k], W, x, y_min, g); 182 if dir == 0 { x_max = new_edge; } else { x_min = x; } 183 i += k; 184 } else { 185 // ROW (dir 1: top, 3: bottom) 186 let L = (x_max - x_min).max(1.0); 187 let (k, H) = choose_k( 188 remaining, 189 L, g, h_min, h_max, 190 |im| im.ar.max(f32::EPSILON), 191 ); 192 193 let y = if dir == 1 { y_max + g } else { y_min - g - H }; 194 let new_edge = place_row(&mut placed, &remaining[..k], H, x_min, y, g); 195 if dir == 1 { y_max = new_edge; } else { y_min = y; } 196 i += k; 197 } 198 199 dir = (dir + 1) % 4; 200 } 201 202 // Normalize so bbox top-left is (0,0) 203 let shift = vec2(-x_min, -y_min); 204 for p in &mut placed { 205 p.rect = p.rect.translate(shift); 206 } 207 let total_size = vec2(x_max - x_min, y_max - y_min); 208 (placed, total_size) 209 } 210 211 pub fn spiral_gallery(ui: &mut egui::Ui, images: &[ImageItem], params: LayoutParams) { 212 use egui::{ScrollArea, Stroke}; 213 214 let (placed, size) = layout_spiral(images, params); 215 216 ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { 217 let (rect, _resp) = ui.allocate_exact_size(size, Sense::hover()); 218 let painter = ui.painter_at(rect); 219 painter.rect_stroke( 220 Rect::from_min_size(rect.min, size), 221 0.0, 222 Stroke::new(1.0, Color32::DARK_GRAY), 223 ); 224 225 let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)); 226 for p in &placed { 227 let r = Rect::from_min_max(rect.min + p.rect.min.to_vec2(), 228 rect.min + p.rect.max.to_vec2()); 229 painter.image(p.texture, r, uv, Color32::WHITE); 230 } 231 }); 232 }