use std::{cell::Cell, rc::Rc};
use gpui_kit::{IntoElement, Pixels, Styled as _, Window, canvas, px};
pub(crate) const MIN_LABEL_CHARS: usize = 8;
#[derive(Clone, Default)]
pub(crate) struct Measured(Rc<Cell<Pixels>>);
impl Measured {
pub(crate) fn width(&self) -> Pixels {
self.0.get()
}
pub(crate) fn probe(&self) -> impl IntoElement {
let cell = self.0.clone();
canvas(
move |bounds, window, _| {
let width = bounds.size.width;
if (f32::from(cell.get()) - f32::from(width)).abs() > 0.5 {
cell.set(width);
window.refresh();
}
},
|_, _, _, _| {},
)
.absolute()
.top_0()
.left_0()
.size_full()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Need {
pub(crate) natural: Pixels,
pub(crate) min: Pixels,
pub(crate) icon_only: Option<Pixels>,
}
impl Need {
pub(crate) fn flexible(min: Pixels, icon_only: Option<Pixels>) -> Self {
Self {
natural: min,
min,
icon_only,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Fit {
Natural,
Shrunk(Pixels),
Icon,
}
impl Fit {
pub(crate) fn is_icon(self) -> bool {
self == Fit::Icon
}
}
pub(crate) fn plan(
available: Pixels,
gap: Pixels,
tight_gap: Pixels,
needs: &[Need],
order: &[usize],
) -> Vec<Fit> {
let mut fits = vec![Fit::Natural; needs.len()];
if available <= px(0.) {
return fits;
}
let available = f32::from(available);
let count = needs.len().saturating_sub(1) as f32;
let width = |need: &Need, fit: Fit| match (fit, need.icon_only) {
(Fit::Icon, Some(icon)) => f32::from(icon),
_ => f32::from(need.natural),
};
let minimum = |need: &Need, fit: Fit| match (fit, need.icon_only) {
(Fit::Icon, Some(icon)) => f32::from(icon),
_ => f32::from(need.min),
};
let mut icons = vec![false; needs.len()];
let gap_of = |icons: &[bool]| {
f32::from(if icons.iter().any(|icon| *icon) {
tight_gap
} else {
gap
})
};
let fits_with = |icons: &[bool]| {
let total: f32 = needs
.iter()
.zip(icons)
.map(|(need, &icon)| minimum(need, if icon { Fit::Icon } else { Fit::Natural }))
.sum();
total + gap_of(icons) * count <= available
};
for &index in order {
if fits_with(&icons) {
break;
}
if needs[index].icon_only.is_some() {
icons[index] = true;
}
}
let gaps = gap_of(&icons) * count;
let mut remaining = available - gaps;
let mut open: Vec<usize> = Vec::new();
for (index, need) in needs.iter().enumerate() {
if icons[index] {
fits[index] = Fit::Icon;
remaining -= width(need, Fit::Icon);
} else {
open.push(index);
}
}
loop {
if open.is_empty() {
break;
}
let share = remaining / open.len() as f32;
let small: Vec<usize> = open
.iter()
.copied()
.filter(|&i| f32::from(needs[i].natural) <= share)
.collect();
if small.is_empty() {
for &index in &open {
let cut = share.max(f32::from(needs[index].min));
fits[index] = Fit::Shrunk(px(cut));
}
break;
}
for &index in &small {
remaining -= f32::from(needs[index].natural);
}
open.retain(|index| !small.contains(index));
}
fits
}
pub(crate) fn text_width(text: &str, window: &Window) -> Pixels {
let style = window.text_style();
let size = style.font_size.to_pixels(window.rem_size());
window
.text_system()
.shape_line(
gpui_kit::SharedString::from(text.to_string()),
size,
&[style.to_run(text.len())],
None,
)
.width
}
pub(crate) fn min_label_width(window: &Window, chars: usize) -> Pixels {
text_width(&"n".repeat(chars), window)
}
#[cfg(test)]
mod tests {
use super::*;
fn need(natural: f32, min: f32, icon: Option<f32>) -> Need {
Need {
natural: px(natural),
min: px(min),
icon_only: icon.map(px),
}
}
fn plan_of(available: f32, needs: &[Need], order: &[usize]) -> Vec<Fit> {
plan(px(available), px(10.), px(10.), needs, order)
}
#[test]
fn nothing_changes_while_the_row_fits_or_has_no_width_yet() {
let needs = [need(100., 60., Some(28.)), need(100., 60., Some(28.))];
assert_eq!(plan_of(300., &needs, &[1, 0]), [Fit::Natural; 2]);
assert_eq!(plan_of(0., &needs, &[1, 0]), [Fit::Natural; 2]);
}
#[test]
fn labels_shrink_to_their_minimum_before_any_control_becomes_an_icon() {
let needs = [need(100., 60., Some(28.)), need(100., 60., Some(28.))];
assert_eq!(
plan_of(170., &needs, &[1, 0]),
[Fit::Shrunk(px(80.)), Fit::Shrunk(px(80.))]
);
assert_eq!(
plan_of(130., &needs, &[1, 0]),
[Fit::Shrunk(px(60.)), Fit::Shrunk(px(60.))]
);
}
#[test]
fn controls_become_icons_in_the_order_given_until_the_labels_fit() {
let needs = [
need(100., 60., Some(28.)),
need(100., 60., Some(28.)),
need(100., 60., Some(28.)),
];
let fits = plan_of(150., &needs, &[2, 1, 0]);
assert_eq!(fits[2], Fit::Icon);
assert!(!fits[0].is_icon());
let fits = plan_of(60., &needs, &[2, 1, 0]);
assert_eq!(fits, [Fit::Icon; 3]);
}
#[test]
fn a_short_label_keeps_its_width_and_the_long_ones_share_the_rest() {
let needs = [need(40., 40., None), need(200., 60., Some(28.))];
let fits = plan_of(150., &needs, &[1]);
assert_eq!(fits[0], Fit::Natural);
assert_eq!(fits[1], Fit::Shrunk(px(100.)));
}
#[test]
fn a_control_without_an_icon_never_collapses() {
let needs = [need(100., 100., None), need(100., 60., Some(28.))];
assert_eq!(plan_of(50., &needs, &[0, 1])[0], Fit::Shrunk(px(100.)));
}
}