notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }