Files
feresaria/crates/ferese-panel/src/panel/layout.rs
T

810 lines
31 KiB
Rust

use super::{ItemId, OverflowPolicy, Panel, Representation, Zone};
use std::collections::BTreeMap;
#[derive(Clone, Debug)]
pub struct Measurement {
pub id: ItemId,
pub alternatives: Vec<(Representation, f32)>,
pub minimum: Option<f32>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Placement {
Visible { representation: Representation, width: f32 },
Overflow,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Resolution {
pub items: BTreeMap<ItemId, Placement>,
pub overflow: Vec<ItemId>,
pub zone_widths: [f32; 3],
/// Separation used when allocating these zones.
pub zone_gap: f32,
pub forced_overflow: bool,
pub overflow_trigger_width: f32,
/// Width needed by the smallest visible representations before automatic overflow.
pub minimum_width: f32,
}
fn zone_width(panel: &Panel, zone: &Zone, items: &BTreeMap<ItemId, Placement>) -> f32 {
let mut width = 0.0;
let mut group_count: usize = 0;
for group in &zone.groups {
let mut content_width = 0.0;
let mut occupied = false;
for item in &group.items {
if let Some(Placement::Visible { width, .. }) = items.get(&item.id)
&& *width > 0.0
{
if occupied {
content_width += item.gap_before.unwrap_or(group.spacing);
}
content_width += *width;
occupied = true;
}
}
if occupied {
let padding = 2.0 * f32::from(group.padding[1]);
let island = if panel.group_surface() == super::GroupSurface::Island {
2.0 * group.island_padding
} else {
0.0
};
width += content_width + padding + island;
group_count += 1;
}
}
width + panel.zone_spacing(zone) * group_count.saturating_sub(1) as f32
}
impl Resolution {
fn update_widths(&mut self, panel: &Panel, overflow_width: f32) {
self.zone_widths = [
zone_width(panel, &panel.start, &self.items),
zone_width(panel, &panel.center, &self.items),
zone_width(panel, &panel.end, &self.items),
];
if !self.overflow.is_empty() {
self.zone_widths[2] += overflow_width
+ if self.zone_widths[2] > 0.0 {
panel.zone_spacing(&panel.end)
} else {
0.0
};
}
}
fn required_width(&self) -> f32 {
self.required_width_with_gap(self.zone_gap)
}
fn required_width_with_gap(&self, gap: f32) -> f32 {
let [start, center, end] = self.zone_widths;
if center > 0.0 {
let side = start.max(end);
center + if side > 0.0 { 2.0 * (side + gap) } else { 0.0 }
} else {
start + end + if start > 0.0 && end > 0.0 { gap } else { 0.0 }
}
}
pub fn fits(&self, available: f32) -> bool {
self.required_width() <= available
}
/// Clamp a requested inset independently for each output. Content may still
/// overflow on an output too small to fit it even without an inset.
pub fn side_margin(&self, requested: i32, output_width: i32, padding: f32) -> i32 {
let maximum = ((output_width as f32 - 2. * padding - self.minimum_width) / 2.)
.floor()
.max(0.);
requested.max(0).min(maximum as i32)
}
}
pub fn resolve(panel: &Panel, measurements: &[Measurement], available: f32, overflow_width: f32) -> Resolution {
resolve_with_gap(panel, measurements, available, overflow_width, 8.)
}
/// Reserve separation between zones while keeping the center on the panel midpoint.
pub fn resolve_with_gap(
panel: &Panel,
measurements: &[Measurement],
available: f32,
overflow_width: f32,
zone_gap: f32,
) -> Resolution {
let available = available.max(0.0);
let zone_gap = zone_gap.max(0.);
let fits = |resolution: &Resolution| resolution.fits(available);
let mut result = Resolution {
zone_gap,
..Resolution::default()
};
let ordered: Vec<_> = [&panel.start, &panel.center, &panel.end]
.into_iter()
.flat_map(|zone| &zone.groups)
.flat_map(|group| &group.items)
.collect();
let measurements: BTreeMap<_, _> = measurements.iter().map(|measured| (&measured.id, measured)).collect();
// Allocation policy follows composition, never the renderer's sample order.
// Lower priorities yield first; definition order breaks equal-priority ties.
let mut candidates: Vec<_> = ordered.iter().enumerate().collect();
candidates.sort_by_key(|(index, item)| (item.priority, *index));
for item in &ordered {
let Some(measured) = measurements.get(&item.id) else {
continue;
};
let Some(&(representation, width)) = measured.alternatives.first() else {
continue;
};
if item.overflow == OverflowPolicy::Always {
result.items.insert(item.id.clone(), Placement::Overflow);
result.overflow.push(item.id.clone());
} else {
result
.items
.insert(item.id.clone(), Placement::Visible { representation, width });
}
}
result.update_widths(panel, overflow_width);
let mut minimum = result.clone();
for measured in measurements.values() {
if let Some(Placement::Visible { width, .. }) = minimum.items.get_mut(&measured.id) {
for (_, alternative) in &measured.alternatives {
*width = width.min(*alternative);
}
if let Some(flexible) = measured.minimum {
*width = width.min(flexible);
}
}
}
minimum.update_widths(panel, overflow_width);
result.minimum_width = minimum.required_width();
// Flexible content yields space without changing the stored item preference.
for (_, item) in &candidates {
let Some(measured) = measurements.get(&item.id).filter(|measured| measured.minimum.is_some()) else {
continue;
};
if fits(&result) {
break;
}
let Some(Placement::Visible { representation, width }) = result.items.get(&measured.id).cloned() else {
continue;
};
let minimum = measured.minimum.unwrap().min(width);
result.items.insert(
measured.id.clone(),
Placement::Visible {
representation,
width: minimum,
},
);
result.update_widths(panel, overflow_width);
}
for (_, item) in &candidates {
if fits(&result) {
break;
}
let Some(measured) = measurements.get(&item.id) else {
continue;
};
for &(representation, width) in measured.alternatives.iter().skip(1) {
if let Some(Placement::Visible { width: previous, .. }) = result.items.get(&item.id)
&& width < *previous
{
result
.items
.insert(item.id.clone(), Placement::Visible { representation, width });
result.update_widths(panel, overflow_width);
if fits(&result) {
break;
}
}
}
}
// Never is honored whenever the mandatory minimums fit. On physically smaller
// surfaces, retain access through overflow instead of silently clipping actions.
for force in [false, true] {
for (_, item) in &candidates {
if fits(&result) {
break;
}
if item.overflow == OverflowPolicy::Never && !force {
continue;
}
if matches!(result.items.get(&item.id), Some(Placement::Visible { width, .. }) if *width > 0.0) {
result.items.insert(item.id.clone(), Placement::Overflow);
result.overflow.push(item.id.clone());
result.forced_overflow |= force && item.overflow == OverflowPolicy::Never;
result.update_widths(panel, overflow_width);
}
}
}
// Removing the larger side may make an earlier eviction unnecessary. Give
// mandatory items first refusal, then restore by descending priority.
for mandatory in [true, false] {
for (_, item) in candidates.iter().rev() {
if item.overflow == OverflowPolicy::Always
|| (item.overflow == OverflowPolicy::Never) != mandatory
|| !matches!(result.items.get(&item.id), Some(Placement::Overflow))
{
continue;
}
let Some(measured) = measurements.get(&item.id) else {
continue;
};
let Some(&(representation, width)) = measured.alternatives.iter().min_by(|a, b| a.1.total_cmp(&b.1)) else {
continue;
};
result.items.insert(
item.id.clone(),
Placement::Visible {
representation,
width: measured.minimum.map_or(width, |minimum| minimum.min(width)),
},
);
result.overflow.retain(|id| id != &item.id);
result.update_widths(panel, overflow_width);
if !fits(&result) {
result.items.insert(item.id.clone(), Placement::Overflow);
result.overflow.push(item.id.clone());
result.update_widths(panel, overflow_width);
}
}
}
result.forced_overflow = ordered.iter().any(|item| {
item.overflow == OverflowPolicy::Never && matches!(result.items.get(&item.id), Some(Placement::Overflow))
});
// A reduction on the shorter side can leave unused room after the other
// side shrinks. Restore preferred representations before expanding titles.
for (_, item) in candidates.iter().rev() {
let Some(measured) = measurements.get(&item.id) else {
continue;
};
let Some(previous @ Placement::Visible { representation, .. }) = result.items.get(&item.id).cloned() else {
continue;
};
for &(candidate, width) in measured
.alternatives
.iter()
.take_while(|(candidate, _)| *candidate != representation)
{
result.items.insert(
item.id.clone(),
Placement::Visible {
representation: candidate,
width: measured.minimum.map_or(width, |minimum| minimum.min(width)),
},
);
result.update_widths(panel, overflow_width);
if fits(&result) {
break;
}
result.items.insert(item.id.clone(), previous.clone());
}
}
result.update_widths(panel, overflow_width);
// Restore the most protected flexible items first, reversing the yield order.
for (_, item) in candidates.iter().rev() {
let Some(measured) = measurements.get(&item.id).filter(|measured| measured.minimum.is_some()) else {
continue;
};
let Some(Placement::Visible {
representation,
width: minimum,
}) = result.items.get(&measured.id).cloned()
else {
continue;
};
let Some(maximum) = measured
.alternatives
.iter()
.find_map(|(candidate, width)| (*candidate == representation).then_some(*width))
else {
continue;
};
let mut low = minimum;
let mut high = maximum;
for _ in 0..20 {
let width = (low + high) * 0.5;
*result.items.get_mut(&measured.id).unwrap() = Placement::Visible { representation, width };
result.update_widths(panel, overflow_width);
if fits(&result) { low = width } else { high = width }
}
*result.items.get_mut(&measured.id).unwrap() = Placement::Visible {
representation,
width: low,
};
}
result
.overflow
.sort_by_key(|id| ordered.iter().position(|item| &item.id == id).unwrap());
result.update_widths(panel, overflow_width);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn asymmetric_overflow_restores_items_that_fit_after_the_larger_side_yields() {
use super::super::{Group, Item, ItemKind};
for mirrored in [false, true] {
for policy in [OverflowPolicy::Auto, OverflowPolicy::Never, OverflowPolicy::Always] {
let group = |id: &str, priority, overflow| {
let mut item = Item::new(id, ItemKind::Clock);
item.priority = priority;
item.overflow = overflow;
let mut group = Group::new(id, vec![item], [0, 0], 0.);
group.island_padding = 0.;
group
};
let mut panel = Panel::default();
panel.start.groups = vec![group("small", 0, policy)];
panel.center.groups = vec![group("center", 100, OverflowPolicy::Never)];
panel.end.groups = vec![group("large", 50, policy)];
if mirrored {
std::mem::swap(&mut panel.start, &mut panel.end);
}
let measurements: Vec<_> = [("small", 40.), ("center", 40.), ("large", 200.)]
.into_iter()
.map(|(id, width)| Measurement {
id: ItemId(id.into()),
alternatives: vec![(Representation::Wide, width)],
minimum: None,
})
.collect();
let resolution = resolve(&panel, &measurements, 300., 32.);
assert!(resolution.fits(300.));
assert_eq!(
matches!(resolution.items[&ItemId("small".into())], Placement::Visible { .. }),
policy != OverflowPolicy::Always
);
assert!(resolution.overflow.contains(&ItemId("large".into())));
assert_eq!(resolution.forced_overflow, policy == OverflowPolicy::Never);
}
}
}
#[test]
fn zone_width_ignores_empty_groups_and_gaps_before_hidden_items() {
use super::super::{Group, Item, ItemKind};
let mut panel = Panel::default();
let mut first = Item::new("first", ItemKind::Overview);
first.gap_before = Some(99.);
let hidden = Item::new("hidden", ItemKind::Network);
let mut last = Item::new("last", ItemKind::Clock);
last.gap_before = Some(7.);
let mut group = Group::new("content", vec![first, hidden, last], [0, 3], 5.);
panel.style = crate::PanelStyle::Floating;
group.island_padding = 4.;
let empty = Group::new("empty", vec![Item::new("missing", ItemKind::Media)], [0, 99], 99.);
let trailing = Group::new(
"trailing",
vec![Item::new("trailing", ItemKind::Battery { percentage: false })],
[0, 2],
5.,
);
panel.start.groups = vec![empty.clone(), group, empty, trailing];
panel.start.spacing = 11.;
let items = [
(
ItemId("first".into()),
Placement::Visible {
representation: Representation::Icon,
width: 20.,
},
),
(ItemId("hidden".into()), Placement::Overflow),
(
ItemId("last".into()),
Placement::Visible {
representation: Representation::Wide,
width: 30.,
},
),
(
ItemId("trailing".into()),
Placement::Visible {
representation: Representation::Icon,
width: 10.,
},
),
]
.into();
assert_eq!(zone_width(&panel, &panel.start, &items), 106.);
}
#[test]
fn a_center_only_item_keeps_its_preferred_size_without_side_gaps() {
use super::super::{Group, Item, ItemKind};
let mut panel = Panel::default();
panel.start.groups.clear();
panel.end.groups.clear();
panel.center.groups = vec![Group::new(
"center",
vec![Item::new("media", ItemKind::Media)],
[0, 0],
0.,
)];
let measurements = [Measurement {
id: ItemId("media".into()),
alternatives: vec![
(Representation::Wide, 40.),
(Representation::Compact, 24.),
(Representation::Icon, 12.),
],
minimum: None,
}];
for gap in [0., 8., 32.] {
for available in [40., 48.] {
let resolution = resolve_with_gap(&panel, &measurements, available, 24., gap);
assert_eq!(
resolution.items[&ItemId("media".into())],
Placement::Visible {
representation: Representation::Wide,
width: 40.
}
);
assert!(resolution.overflow.is_empty());
assert!(resolution.fits(available));
}
}
}
#[test]
fn center_collision_spacing_applies_only_to_occupied_sides() {
for (widths, required) in [
([0., 40., 0.], 40.),
([20., 40., 0.], 96.),
([0., 40., 20.], 96.),
([20., 40., 30.], 116.),
([20., 0., 30.], 58.),
([0., 0., 30.], 30.),
] {
let resolution = Resolution {
zone_widths: widths,
zone_gap: 8.,
..Default::default()
};
assert!(resolution.fits(required));
assert!(!resolution.fits(required - 1.));
}
}
#[test]
fn a_crowded_opposite_zone_does_not_leave_media_compacted_in_unused_space() {
use super::super::{Group, Item, ItemKind};
let mut panel = Panel::default();
panel.start.groups = vec![Group::new(
"navigation",
vec![
Item::new("overview", ItemKind::Overview),
Item::new("media", ItemKind::Media),
],
[0, 0],
0.,
)];
panel.center.groups = vec![Group::new(
"title",
vec![Item::new("title", ItemKind::FocusedWindow)],
[0, 0],
0.,
)];
panel.end.groups = vec![Group::new(
"status",
vec![Item::new("clock", ItemKind::Clock)],
[0, 0],
0.,
)];
let measured = vec![
Measurement {
id: ItemId("overview".into()),
alternatives: vec![(Representation::Icon, 28.)],
minimum: None,
},
Measurement {
id: ItemId("media".into()),
alternatives: vec![
(Representation::Wide, 180.),
(Representation::Compact, 100.),
(Representation::Icon, 28.),
],
minimum: None,
},
Measurement {
id: ItemId("title".into()),
alternatives: vec![(Representation::Wide, 48.)],
minimum: Some(48.),
},
Measurement {
id: ItemId("clock".into()),
alternatives: vec![
(Representation::Wide, 340.),
(Representation::Compact, 240.),
(Representation::Icon, 140.),
],
minimum: None,
},
];
for mirrored in [false, true] {
if mirrored {
std::mem::swap(&mut panel.start, &mut panel.end);
}
let resolution = resolve(&panel, &measured, 550., 32.);
assert!(resolution.fits(550.));
assert_eq!(
resolution.items[&ItemId("media".into())],
Placement::Visible {
representation: Representation::Wide,
width: 180.
}
);
assert_eq!(
resolution.items[&ItemId("clock".into())],
Placement::Visible {
representation: Representation::Compact,
width: 240.
}
);
}
let mut measured = measured;
measured[1].alternatives[0].1 = 400.;
measured[1].minimum = Some(112.);
for mirrored in [false, true] {
if mirrored {
std::mem::swap(&mut panel.start, &mut panel.end);
}
let resolution = resolve(&panel, &measured, 550., 32.);
assert!(resolution.fits(550.));
assert!(
matches!(resolution.items[&ItemId("media".into())], Placement::Visible { representation: Representation::Wide, width } if width > 200. && width < 400.),
"flexible media must expand into the unused room beside the center"
);
}
}
#[test]
fn fits_uses_the_gap_that_was_resolved() {
let panel = Panel::default();
let measured = measurements(&panel);
for gap in [0., 2., 16., 48.] {
let result = resolve_with_gap(&panel, &measured, 2400., 32., gap);
assert_eq!(result.zone_gap, gap);
let required = result.required_width_with_gap(gap);
assert!(result.fits(required));
assert!(!result.fits(required - 1.));
}
}
#[test]
fn flexible_restoration_stays_within_the_selected_representation() {
let mut panel = Panel::default();
panel.start.groups.clear();
panel.center.groups.clear();
panel.end.groups.retain(|group| group.id.0 == "time");
let mut clock = super::super::Item::new("clock", super::super::ItemKind::Clock);
clock.overflow = OverflowPolicy::Never;
clock.priority = 0;
let audio = super::super::Item::new("audio", super::super::ItemKind::Audio);
panel.end.groups[0].items = vec![clock, audio];
let result = resolve(
&panel,
&[
Measurement {
id: ItemId("clock".into()),
alternatives: vec![(Representation::Wide, 80.), (Representation::Compact, 40.)],
minimum: Some(60.),
},
Measurement {
id: ItemId("audio".into()),
alternatives: vec![(Representation::Icon, 100.)],
minimum: None,
},
],
96.,
32.,
);
assert_eq!(
result.items[&ItemId("clock".into())],
Placement::Visible {
representation: Representation::Compact,
width: 40.
}
);
assert_eq!(result.overflow, [ItemId("audio".into())]);
assert!(result.fits(96.));
}
#[test]
fn allocation_is_independent_of_measurement_order_even_for_flexible_items() {
let panel = Panel::default();
let mut measured = measurements(&panel);
for measurement in &mut measured {
measurement.minimum = Some(24.);
}
for width in [2400., 1400., 1000., 800., 400., 80., 32.] {
let expected = resolve_with_gap(&panel, &measured, width, 32., 16.);
for _ in 0..measured.len() {
measured.rotate_left(1);
assert_eq!(resolve_with_gap(&panel, &measured, width, 32., 16.), expected);
measured.reverse();
assert_eq!(resolve_with_gap(&panel, &measured, width, 32., 16.), expected);
measured.reverse();
}
}
}
fn measurements(panel: &Panel) -> Vec<Measurement> {
[&panel.start, &panel.center, &panel.end]
.into_iter()
.flat_map(|zone| &zone.groups)
.flat_map(|group| &group.items)
.map(|item| Measurement {
id: item.id.clone(),
alternatives: vec![
(Representation::Wide, 80.0),
(Representation::Compact, 40.0),
(Representation::Icon, 24.0),
],
minimum: matches!(item.kind, crate::panel::ItemKind::FocusedWindow).then_some(48.0),
})
.collect()
}
#[test]
fn narrow_panels_adapt_deterministically_and_keep_every_item_reachable() {
for background in [crate::PanelStyle::Flat, crate::PanelStyle::Floating] {
let panel = Panel::from_preset(background);
let measured = measurements(&panel);
for width in [2400., 1200., 800., 400., 160., 80., 32.] {
let resolution = resolve(&panel, &measured, width, 32.);
assert!(resolution.fits(width));
assert_eq!(resolution, resolve(&panel, &measured, width, 32.));
assert_eq!(resolution.items.len(), measured.len());
for id in &resolution.overflow {
assert_eq!(resolution.items.get(id), Some(&Placement::Overflow));
}
if width >= 400. {
assert!(!resolution.forced_overflow);
}
}
}
}
#[test]
fn decoration_and_borders_do_not_change_background_arrangement_or_allocation() {
for background in [crate::PanelStyle::Flat, crate::PanelStyle::Floating] {
let mut panel = Panel::from_preset(background);
let measurements = measurements(&panel);
let plain = resolve(&panel, &measurements, 1000., 32.);
assert!(!panel.border);
panel.border = true;
panel.background_opacity = Some(0.4);
assert_eq!(resolve(&panel, &measurements, 1000., 32.), plain);
assert_eq!(panel.preset(), Some(background));
}
}
#[test]
fn side_margins_stop_before_overflow_and_use_each_outputs_logical_width() {
let panel = Panel::default();
let measurements = measurements(&panel);
let full = resolve(&panel, &measurements, 4000., 32.);
assert!(full.minimum_width < full.required_width());
for output_width in [1600, 2400, 3840] {
for requested in [0, 32, 400, 4096] {
let margin = full.side_margin(requested, output_width, 12.);
let available = output_width as f32 - 2. * (margin as f32 + 12.);
let result = resolve(&panel, &measurements, available, 32.);
assert!(result.overflow.is_empty(), "output {output_width}, margin {margin}");
assert!(result.fits(available));
assert!(margin <= requested);
}
}
assert!(full.side_margin(4096, 3840, 12.) > full.side_margin(4096, 1600, 12.));
assert_eq!(full.side_margin(4096, 100, 12.), 0);
assert!(
!resolve(&panel, &measurements, 76., 32.).overflow.is_empty(),
"physical output shortage still uses existing overflow"
);
}
#[test]
fn margin_limit_includes_group_padding_and_authored_overflow_without_dependence_on_current_surface_width() {
let mut panel = Panel::default();
panel.end.groups[0].items[0].overflow = OverflowPolicy::Always;
let measurements = measurements(&panel);
let full = resolve(&panel, &measurements, 4000., 32.);
let narrow = resolve(&panel, &measurements, 100., 32.);
assert_eq!(full.minimum_width, narrow.minimum_width);
let margin = full.side_margin(4096, 2400, 12.);
let result = resolve(&panel, &measurements, 2400. - 2. * (margin as f32 + 12.), 32.);
assert_eq!(result.overflow, full.overflow);
let original = full.minimum_width;
panel.end.groups[0].padding[1] += 10;
let padded = resolve(&panel, &measurements, 4000., 32.);
assert!(padded.minimum_width > original);
assert!(padded.side_margin(4096, 2400, 12.) < margin);
}
#[test]
fn asymmetric_sides_reserve_a_real_screen_center() {
let panel = Panel::default();
let resolution = resolve(&panel, &measurements(&panel), 1500., 32.);
let [start, center, end] = resolution.zone_widths;
assert!(end > start);
assert!(start + 8. <= (1500. - center) * 0.5);
assert!((1500. + center) * 0.5 + 8. <= 1500. - end);
}
#[test]
fn always_overflow_is_reserved_and_unavailable_items_are_not_allocated() {
let mut panel = Panel::default();
panel.end.groups[0].items[2].overflow = OverflowPolicy::Always;
let mut measured = measurements(&panel);
measured.remove(3);
let resolution = resolve(&panel, &measured, 2000., 32.);
assert!(!resolution.items.contains_key(&ItemId("media".into())));
assert_eq!(resolution.overflow, [ItemId("network".into())]);
}
#[test]
fn priority_controls_displacement_and_expansion_restores_requested_sizes() {
let mut panel = Panel::default();
panel.start.groups.clear();
panel.center.groups.clear();
panel.end.groups.retain(|group| group.id.0 == "time");
let mut first = panel.end.groups[0].items[0].clone();
first.id = ItemId("low".into());
first.priority = 0;
let mut second = first.clone();
second.id = ItemId("high".into());
second.priority = 100;
let mut third = first.clone();
third.id = ItemId("middle".into());
third.priority = 50;
panel.end.groups[0].items = vec![first, second, third];
let measured: Vec<_> = panel.end.groups[0]
.items
.iter()
.map(|item| Measurement {
id: item.id.clone(),
alternatives: vec![(Representation::Wide, 50.)],
minimum: None,
})
.collect();
let initial = panel.clone();
let small = resolve(&panel, &measured, 150., 32.);
assert_eq!(small.overflow, [ItemId("low".into())]);
assert!(small.fits(150.));
for width in [200., 150., 40., 200.] {
let result = resolve(&panel, &measured, width, 32.);
assert!(result.fits(width));
if width == 200. {
assert!(result.overflow.is_empty());
assert!(result.items.values().all(|placement| matches!(placement, Placement::Visible { representation: Representation::Wide, width } if *width == 50.)));
}
assert_eq!(panel, initial, "resolution must not rewrite configuration intent");
}
}
#[test]
fn panel_surface_policy_controls_padding_for_every_group() {
let mut panel = Panel::from_preset(crate::PanelStyle::Floating);
let measured = measurements(&panel);
let initial = resolve(&panel, &measured, 2400., 32.);
panel.style = crate::PanelStyle::Flat;
let plain = resolve(&panel, &measured, 2400., 32.);
for (i, zone) in [&panel.start, &panel.center, &panel.end].into_iter().enumerate() {
let padding: f32 = zone.groups.iter().map(|group| 2. * group.island_padding).sum();
assert_eq!(plain.zone_widths[i], initial.zone_widths[i] - padding);
}
panel.style = crate::PanelStyle::Flat;
assert_eq!(resolve(&panel, &measured, 2400., 32.), plain);
panel.style = crate::PanelStyle::Flat;
assert_eq!(resolve(&panel, &measured, 2400., 32.), plain);
}
}