diff --git a/src/widgets.rs b/src/widgets.rs index abb6b8b..f2fe1d7 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -4,6 +4,7 @@ mod cursor; mod either; mod empty; mod float; +mod join; mod layer; mod padding; mod text; @@ -14,6 +15,7 @@ pub use cursor::*; pub use either::*; pub use empty::*; pub use float::*; +pub use join::*; pub use layer::*; pub use padding::*; pub use text::*; diff --git a/src/widgets/join.rs b/src/widgets/join.rs new file mode 100644 index 0000000..4cc301b --- /dev/null +++ b/src/widgets/join.rs @@ -0,0 +1,191 @@ +use std::cmp::Ordering; + +// The following algorithm has three goals, listed in order of importance: +// +// 1. Use the available space +// 2. Avoid shrinking segments where possible +// 3. Match the given weights as closely as possible +// +// Its input is a list of weighted segments where each segment wants to use a +// certain amount of space. The weights signify how the available space would be +// assigned if goal 2 was irrelevant. +// +// First, the algorithm must decide whether it must grow or shrink segments. +// Because goal 2 has a higher priority than goal 3, it never makes sense to +// shrink a segment in order to make another larger. In both cases, a segment's +// actual size is compared to its allotment, i. e. what size it should be based +// on its weight. +// +// Growth +// ====== +// +// If segments must be grown, an important observation can be made: If all +// segments are smaller than their allotment, then each segment can be assigned +// its allotment without violating goal 2, thereby fulfilling goal 3. +// +// Another important observation can be made: If a segment is at least as large +// as its allotment, it must never be grown as that would violate goal 3. +// +// Based on these two observations, the growth algorithm first repeatedly +// removes all segments that are at least as large as their allotment. It then +// resizes the remaining segments to their allotments. +// +// Shrinkage +// ========= +// +// If segments must be shrunk, an important observation can be made: If all +// segments are larger than their allotment, then each segment can be assigned +// its allotment, thereby fulfilling goal 3. Since goal 1 is more important than +// goal 2, we know that some elements must be shrunk. +// +// Another important observation can be made: If a segment is at least as small +// as its allotment, it must never be shrunk as that would violate goal 3. +// +// Based on these two observations, the shrinkage algorithm first repeatedly +// removes all segments that are at least as small as their allotment. It then +// resizes the remaining segments to their allotments. + +struct Segment { + size: u16, + weight: f32, +} + +fn balance(segments: &mut [Segment], available: u16) { + if segments.is_empty() { + return; + } + + let total_size = segments.iter().map(|s| s.size).sum::(); + match total_size.cmp(&available) { + Ordering::Less => grow(segments, available), + Ordering::Greater => shrink(segments, available), + Ordering::Equal => {} + } + + assert!(available >= segments.iter().map(|s| s.size).sum::()); +} + +fn grow(segments: &mut [Segment], mut available: u16) { + assert!(available > segments.iter().map(|s| s.size).sum::()); + let mut segments = segments.iter_mut().collect::>(); + + // Repeatedly remove all segments that do not need to grow, i. e. that are + // at least as large as their allotment. + loop { + let mut total_weight = segments.iter().map(|s| s.weight).sum::(); + + // If there are no segments with a weight > 0, space is distributed + // evenly among all remaining segments. + if total_weight <= 0.0 { + for segment in &mut segments { + segment.weight = 1.0; + } + total_weight = segments.len() as f32; + } + + let mut changed = false; + segments.retain(|s| { + let allotment = s.weight / total_weight * available as f32; + if (s.size as f32) < allotment { + return true; // May need to grow + } + available -= s.size; + changed = true; + false + }); + + // If all segments were at least as large as their allotments, we would + // be trying to shrink, not grow them. Hence, there must be at least one + // segment that is smaller than its allotment. + assert!(!segments.is_empty()); + + if !changed { + break; // All remaining segments are smaller than their allotments + } + } + + // Size each remaining segment according to its allotment. + let total_weight = segments.iter().map(|s| s.weight).sum::(); + let mut used = 0; + for segment in &mut segments { + let allotment = segment.weight / total_weight * available as f32; + segment.size = allotment.floor() as u16; + used += segment.size; + } + + // Distribute remaining unused space from left to right. + // + // The rounding error on each segment is at most 1, so we only need to loop + // over the segments once. + let remaining = available - used; + assert!(remaining as usize <= segments.len()); + for segment in segments.into_iter().take(remaining.into()) { + segment.size += 1; + } +} + +fn shrink(segments: &mut [Segment], mut available: u16) { + assert!(available < segments.iter().map(|s| s.size).sum::()); + let mut segments = segments.iter_mut().collect::>(); + + // Repeatedly remove all segments that do not need to shrink, i. e. that are + // at least as small as their allotment. + loop { + let mut total_weight = segments.iter().map(|s| s.weight).sum::(); + + // If there are no segments with a weight > 0, space is distributed + // evenly among all remaining segments. + if total_weight <= 0.0 { + for segment in &mut segments { + segment.weight = 1.0; + } + total_weight = segments.len() as f32; + } + + let mut changed = false; + segments.retain(|s| { + let allotment = s.weight / total_weight * available as f32; + if (s.size as f32) > allotment { + return true; // May need to shrink + } + + // The size subtracted from `available` is always smaller than or + // equal to its allotment. It must be smaller in at least one case, + // or we wouldn't be shrinking. Since `available` is the sum of all + // allotments, it never reaches 0. + assert!(available > s.size); + + available -= s.size; + changed = true; + false + }); + + // If all segments were smaller or the same size as their allotments, we + // would be trying to grow, not shrink them. Hence, there must be at + // least one segment bigger than its allotment. + assert!(!segments.is_empty()); + + if !changed { + break; // All segments want more than their weight allows. + } + } + + // Size each remaining segment according to its allotment. + let total_weight = segments.iter().map(|s| s.weight).sum::(); + let mut used = 0; + for segment in &mut segments { + let allotment = segment.weight / total_weight * available as f32; + segment.size = allotment.floor() as u16; + used += segment.size; + } + + // Distribute remaining unused space from left to right. + // + // The rounding error on each segment is at most 1, so we only need to loop + // over the segments once. + let remaining = available - used; + assert!(remaining as usize <= segments.len()); + for segment in segments.into_iter().take(remaining.into()) { + segment.size += 1; + } +}