Format Rust sources at 100 columns
This commit is contained in:
1 parent
4661f595f8
commit
610478f8c7
336 files changed
+23497
-7892
No files matched your search
@@ -88,7 +88,8 @@ impl Adaptive {
|
||||
}
|
||||
|
||||
pub fn deadline(&self) -> Option<Duration> {
|
||||
self.pending.map(|(_, since)| since.saturating_add(HYSTERESIS))
|
||||
self.pending
|
||||
.map(|(_, since)| since.saturating_add(HYSTERESIS))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,9 @@ impl Hub {
|
||||
pins.iter()
|
||||
.any(|value| !value.is_finite() || !(0. ..=f64::from(f32::MAX)).contains(value))
|
||||
}) {
|
||||
return Err("hub radius pins must be finite, nonnegative pixel values representable as f32");
|
||||
return Err(
|
||||
"hub radius pins must be finite, nonnegative pixel values representable as f32",
|
||||
);
|
||||
}
|
||||
self.pins = pins;
|
||||
let mut target = self.motion.target();
|
||||
@@ -221,7 +223,10 @@ impl Hub {
|
||||
}
|
||||
|
||||
pub fn retarget(&mut self, mode: Mode, available: [f64; 2]) -> Result<(), &'static str> {
|
||||
if available.iter().any(|value| !value.is_finite() || *value <= 0.) {
|
||||
if available
|
||||
.iter()
|
||||
.any(|value| !value.is_finite() || *value <= 0.)
|
||||
{
|
||||
return Err("hub requires a finite positive available area");
|
||||
}
|
||||
let [width, height] = self.profile.size(mode, self.height);
|
||||
@@ -302,12 +307,14 @@ impl Hub {
|
||||
|
||||
pub fn sample(&self) -> Sample {
|
||||
let fade = |elapsed: Duration| (elapsed.as_secs_f32() / FADE.as_secs_f32()).clamp(0., 1.);
|
||||
let opacity =
|
||||
self.initial_opacity + (1. - self.initial_opacity) * fade(self.elapsed.saturating_sub(CONTENT_DELAY));
|
||||
let opacity = self.initial_opacity
|
||||
+ (1. - self.initial_opacity) * fade(self.elapsed.saturating_sub(CONTENT_DELAY));
|
||||
let mut geometry = self.motion.current();
|
||||
geometry.bounds.width = geometry.bounds.width.clamp(0., self.available[0]);
|
||||
geometry.bounds.height = geometry.bounds.height.clamp(0., self.available[1]);
|
||||
geometry.radii = geometry.radii.map(|radius| radius.clamp(0., f64::from(f32::MAX)));
|
||||
geometry.radii = geometry
|
||||
.radii
|
||||
.map(|radius| radius.clamp(0., f64::from(f32::MAX)));
|
||||
Sample {
|
||||
geometry,
|
||||
previous: self.previous,
|
||||
@@ -352,7 +359,8 @@ mod tests {
|
||||
#[test]
|
||||
fn measured_height_preserves_fade_and_motion_and_caps_long_content() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Controls), [800., 600.]).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Controls), [800., 600.])
|
||||
.unwrap();
|
||||
hub.advance(Duration::from_millis(180), false);
|
||||
let before = hub.sample();
|
||||
hub.fit_content_height(148.).unwrap();
|
||||
@@ -363,7 +371,10 @@ mod tests {
|
||||
assert_eq!(hub.sample().geometry.bounds.height, 148.);
|
||||
assert_eq!(hub.sample().opacity, 1.);
|
||||
hub.fit_content_height(900.).unwrap();
|
||||
assert_eq!(hub.target_size(), [340., Profile::default().controls_height]);
|
||||
assert_eq!(
|
||||
hub.target_size(),
|
||||
[340., Profile::default().controls_height]
|
||||
);
|
||||
hub.retarget(Mode::Idle, [800., 600.]).unwrap();
|
||||
hub.fit_content_height(100.).unwrap();
|
||||
assert_eq!(hub.target_size(), [150., 34.]);
|
||||
@@ -372,7 +383,8 @@ mod tests {
|
||||
#[test]
|
||||
fn a_content_swap_fades_without_restarting_the_geometry() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Controls), [800., 600.]).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Controls), [800., 600.])
|
||||
.unwrap();
|
||||
hub.advance(Duration::from_secs(2), false);
|
||||
hub.advance(Duration::from_secs(1), false);
|
||||
let geometry = hub.sample().geometry;
|
||||
@@ -394,7 +406,8 @@ mod tests {
|
||||
fn content_swap_uses_a_fixed_delay_instead_of_waiting_for_spring_settlement() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
assert!(!hub.active());
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.]).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.])
|
||||
.unwrap();
|
||||
assert_eq!(hub.target_size(), [340., 230.]);
|
||||
hub.advance(Duration::from_millis(120), false);
|
||||
assert_eq!(hub.sample().opacity, 0.);
|
||||
@@ -403,7 +416,10 @@ mod tests {
|
||||
assert!((hub.sample().opacity - 0.5).abs() < 0.001);
|
||||
hub.advance(Duration::from_millis(60), false);
|
||||
assert_eq!(hub.sample().opacity, 1.);
|
||||
assert!(hub.active(), "content finishes while the shape is still settling");
|
||||
assert!(
|
||||
hub.active(),
|
||||
"content finishes while the shape is still settling"
|
||||
);
|
||||
for _ in 0..50 {
|
||||
hub.advance(Duration::from_millis(16), false);
|
||||
}
|
||||
@@ -415,9 +431,11 @@ mod tests {
|
||||
#[test]
|
||||
fn an_interrupted_content_delay_never_reveals_the_cancelled_panel() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Media), [800., 600.]).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Media), [800., 600.])
|
||||
.unwrap();
|
||||
hub.advance(Duration::from_millis(80), false);
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.]).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.])
|
||||
.unwrap();
|
||||
for _ in 0..130 {
|
||||
hub.advance(Duration::from_millis(8), false);
|
||||
let sample = hub.sample();
|
||||
@@ -431,7 +449,8 @@ mod tests {
|
||||
#[test]
|
||||
fn spring_overshoots_then_settles_within_its_reserved_surface() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.]).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.])
|
||||
.unwrap();
|
||||
let allocation = hub.allocation_size();
|
||||
let mut peak = 340_f64;
|
||||
for _ in 0..200 {
|
||||
@@ -450,7 +469,8 @@ mod tests {
|
||||
#[test]
|
||||
fn overshoot_is_clipped_to_small_outputs_and_reduced_motion_removes_all_fades() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.]).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.])
|
||||
.unwrap();
|
||||
for _ in 0..60 {
|
||||
hub.advance(Duration::from_millis(8), false);
|
||||
let bounds = hub.sample().geometry.bounds;
|
||||
@@ -467,7 +487,8 @@ mod tests {
|
||||
#[test]
|
||||
fn interrupted_expansion_keeps_the_presented_shape() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Media), [800., 600.]).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Media), [800., 600.])
|
||||
.unwrap();
|
||||
hub.advance(Duration::from_millis(40), false);
|
||||
let before = hub.sample().geometry;
|
||||
hub.retarget(Mode::Idle, [800., 600.]).unwrap();
|
||||
@@ -482,7 +503,8 @@ mod tests {
|
||||
#[test]
|
||||
fn narrow_outputs_and_reduced_motion_keep_the_hub_bounded() {
|
||||
let mut hub = Hub::new(Profile::default(), 30.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.]).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.])
|
||||
.unwrap();
|
||||
hub.advance(Duration::ZERO, true);
|
||||
assert_eq!(hub.sample().geometry.bounds, Rect::new(0., 0., 240., 180.));
|
||||
assert_eq!(hub.sample().opacity, 1.);
|
||||
|
||||
@@ -31,7 +31,12 @@ impl Geometry {
|
||||
|
||||
fn values(self) -> [f64; COUNT] {
|
||||
let mut values = [0.; COUNT];
|
||||
values[..4].copy_from_slice(&[self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height]);
|
||||
values[..4].copy_from_slice(&[
|
||||
self.bounds.x,
|
||||
self.bounds.y,
|
||||
self.bounds.width,
|
||||
self.bounds.height,
|
||||
]);
|
||||
values[4..8].copy_from_slice(&self.radii);
|
||||
values[8..12].copy_from_slice(&self.margin);
|
||||
values[12..16].copy_from_slice(&self.thickness);
|
||||
@@ -53,7 +58,9 @@ impl Geometry {
|
||||
|
||||
fn validate(self) -> Result<Self, &'static str> {
|
||||
let values = self.values();
|
||||
if values.iter().any(|value| !value.is_finite()) || values[2..].iter().any(|value| *value < 0.) {
|
||||
if values.iter().any(|value| !value.is_finite())
|
||||
|| values[2..].iter().any(|value| *value < 0.)
|
||||
{
|
||||
Err("panel animation geometry must be finite with nonnegative extents")
|
||||
} else {
|
||||
Ok(self)
|
||||
@@ -82,7 +89,10 @@ impl Motion {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn with_spring(geometry: Geometry, spring: SpringConfig) -> Result<Self, &'static str> {
|
||||
pub(crate) fn with_spring(
|
||||
geometry: Geometry,
|
||||
spring: SpringConfig,
|
||||
) -> Result<Self, &'static str> {
|
||||
Ok(Self {
|
||||
values: geometry.validate()?.values().map(AnimatedValue::new),
|
||||
spring,
|
||||
@@ -106,14 +116,16 @@ impl Motion {
|
||||
// Extrema alternate; later peaks decay, so the first two suffice.
|
||||
for phase in [first, first + std::f64::consts::PI] {
|
||||
let t = phase / frequency;
|
||||
let position =
|
||||
value.target + (-decay * t).exp() * (displacement * phase.cos() + sine * phase.sin());
|
||||
let position = value.target
|
||||
+ (-decay * t).exp() * (displacement * phase.cos() + sine * phase.sin());
|
||||
maximum = maximum.max(position);
|
||||
}
|
||||
} else {
|
||||
// Total spring energy bounds displacement for any nonnegative damping.
|
||||
maximum = maximum.max(
|
||||
value.target + (displacement * displacement + value.velocity * value.velocity / omega2).sqrt(),
|
||||
value.target
|
||||
+ (displacement * displacement + value.velocity * value.velocity / omega2)
|
||||
.sqrt(),
|
||||
);
|
||||
}
|
||||
maximum
|
||||
@@ -229,7 +241,11 @@ mod tests {
|
||||
let before = motion.current();
|
||||
let velocity = motion.values.map(|value| value.velocity);
|
||||
motion
|
||||
.retarget(if turn % 2 == 0 { expanded() } else { collapsed() })
|
||||
.retarget(if turn % 2 == 0 {
|
||||
expanded()
|
||||
} else {
|
||||
collapsed()
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(motion.current(), before);
|
||||
assert_eq!(motion.values.map(|value| value.velocity), velocity);
|
||||
|
||||
@@ -4,10 +4,14 @@ use std::f64::consts::{FRAC_PI_2, PI};
|
||||
pub type Contour = Vec<[f64; 2]>;
|
||||
|
||||
fn arc(points: &mut Contour, center: [f64; 2], radius: f64, start: f64, tolerance: f64) {
|
||||
let segments = ((FRAC_PI_2 / (8. * tolerance / radius.max(tolerance)).sqrt()).ceil() as usize).clamp(1, 128);
|
||||
let segments = ((FRAC_PI_2 / (8. * tolerance / radius.max(tolerance)).sqrt()).ceil() as usize)
|
||||
.clamp(1, 128);
|
||||
for i in 0..=segments {
|
||||
let angle = start + FRAC_PI_2 * i as f64 / segments as f64;
|
||||
points.push([center[0] + radius * angle.cos(), center[1] + radius * angle.sin()]);
|
||||
points.push([
|
||||
center[0] + radius * angle.cos(),
|
||||
center[1] + radius * angle.sin(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,20 +70,32 @@ pub fn contours(scene: &Scene, tolerance: f64) -> Result<Vec<Contour>, &'static
|
||||
.collect();
|
||||
if scene.fusion > 0. {
|
||||
for zone in 1..=3 {
|
||||
let shapes: Vec<_> = scene.paint.iter().filter(|p| p.blend_zone == zone).copied().collect();
|
||||
let shapes: Vec<_> = scene
|
||||
.paint
|
||||
.iter()
|
||||
.filter(|p| p.blend_zone == zone)
|
||||
.copied()
|
||||
.collect();
|
||||
if shapes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Fused rows share their vertical baseline. Each component is
|
||||
// vertically convex; solve its upper and lower boundary, rather
|
||||
// than sampling every pixel of a full-output bitmap.
|
||||
let top = shapes.iter().map(|p| p.bounds.y).fold(f64::INFINITY, f64::min);
|
||||
let top = shapes
|
||||
.iter()
|
||||
.map(|p| p.bounds.y)
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
let bottom = shapes
|
||||
.iter()
|
||||
.map(|p| p.bounds.y + p.bounds.height)
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
let mid = (top + bottom) * 0.5;
|
||||
let left = shapes.iter().map(|p| p.bounds.x).fold(f64::INFINITY, f64::min) - scene.fusion;
|
||||
let left = shapes
|
||||
.iter()
|
||||
.map(|p| p.bounds.x)
|
||||
.fold(f64::INFINITY, f64::min)
|
||||
- scene.fusion;
|
||||
let right = shapes
|
||||
.iter()
|
||||
.map(|p| p.bounds.x + p.bounds.width)
|
||||
@@ -135,7 +151,11 @@ pub fn contours(scene: &Scene, tolerance: f64) -> Result<Vec<Contour>, &'static
|
||||
|
||||
/// Open border paths for screen-attached notches. Split shared straight edges
|
||||
/// before cancelling them, so the body and its fillets have one continuous rim.
|
||||
pub fn notch_border(scene: &Scene, tolerance: f64, edge: crate::Edge) -> Result<Vec<Contour>, &'static str> {
|
||||
pub fn notch_border(
|
||||
scene: &Scene,
|
||||
tolerance: f64,
|
||||
edge: crate::Edge,
|
||||
) -> Result<Vec<Contour>, &'static str> {
|
||||
let contours = contours(scene, tolerance)?;
|
||||
let vertices: Vec<_> = contours.iter().flatten().copied().collect();
|
||||
let screen_edge = vertices.iter().map(|p| p[1]).fold(
|
||||
@@ -144,13 +164,23 @@ pub fn notch_border(scene: &Scene, tolerance: f64, edge: crate::Edge) -> Result<
|
||||
} else {
|
||||
f64::NEG_INFINITY
|
||||
},
|
||||
if edge == crate::Edge::Top { f64::min } else { f64::max },
|
||||
if edge == crate::Edge::Top {
|
||||
f64::min
|
||||
} else {
|
||||
f64::max
|
||||
},
|
||||
);
|
||||
let close = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).abs() < 1e-6 && (a[1] - b[1]).abs() < 1e-6;
|
||||
let mut segments: Vec<([f64; 2], [f64; 2])> = Vec::new();
|
||||
for contour in &contours {
|
||||
for (&a, &b) in contour.iter().zip(contour.iter().cycle().skip(1)).take(contour.len()) {
|
||||
if close(a, b) || ((a[1] - screen_edge).abs() < 1e-6 && (b[1] - screen_edge).abs() < 1e-6) {
|
||||
for (&a, &b) in contour
|
||||
.iter()
|
||||
.zip(contour.iter().cycle().skip(1))
|
||||
.take(contour.len())
|
||||
{
|
||||
if close(a, b)
|
||||
|| ((a[1] - screen_edge).abs() < 1e-6 && (b[1] - screen_edge).abs() < 1e-6)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let delta = [b[0] - a[0], b[1] - a[1]];
|
||||
@@ -181,7 +211,10 @@ pub fn notch_border(scene: &Scene, tolerance: f64, edge: crate::Edge) -> Result<
|
||||
let mut paths = Vec::new();
|
||||
while let Some((a, b)) = segments.pop() {
|
||||
let mut path = vec![a, b];
|
||||
while let Some(index) = segments.iter().position(|&(a, _)| close(a, *path.last().unwrap())) {
|
||||
while let Some(index) = segments
|
||||
.iter()
|
||||
.position(|&(a, _)| close(a, *path.last().unwrap()))
|
||||
{
|
||||
path.push(segments.swap_remove(index).1);
|
||||
}
|
||||
while let Some(index) = segments.iter().position(|&(_, b)| close(b, path[0])) {
|
||||
@@ -202,7 +235,11 @@ mod tests {
|
||||
fn winding(contours: &[Contour], point: [f64; 2]) -> i32 {
|
||||
let mut winding = 0;
|
||||
for points in contours {
|
||||
for (a, b) in points.iter().zip(points.iter().cycle().skip(1)).take(points.len()) {
|
||||
for (a, b) in points
|
||||
.iter()
|
||||
.zip(points.iter().cycle().skip(1))
|
||||
.take(points.len())
|
||||
{
|
||||
let cross = (b[0] - a[0]) * (point[1] - a[1]) - (point[0] - a[0]) * (b[1] - a[1]);
|
||||
if a[1] <= point[1] && b[1] > point[1] && cross > 0. {
|
||||
winding += 1;
|
||||
@@ -241,14 +278,22 @@ mod tests {
|
||||
let screen_y = if edge == crate::Edge::Top { 0. } else { 44. };
|
||||
assert!((path.first().unwrap()[1] - screen_y).abs() < 1e-6);
|
||||
assert!((path.last().unwrap()[1] - screen_y).abs() < 1e-6);
|
||||
let body = scene.paint.iter().find(|p| p.operation == 0).unwrap().bounds;
|
||||
let body = scene
|
||||
.paint
|
||||
.iter()
|
||||
.find(|p| p.operation == 0)
|
||||
.unwrap()
|
||||
.bounds;
|
||||
assert!(path.iter().any(|p| p[0] < body.x));
|
||||
assert!(path.iter().any(|p| p[0] > body.x + body.width));
|
||||
for segment in path.windows(2) {
|
||||
let [a, b] = [segment[0], segment[1]];
|
||||
assert!((a[1] - screen_y).abs() > 1e-6 || (b[1] - screen_y).abs() > 1e-6);
|
||||
let mid = [(a[0] + b[0]) / 2., (a[1] + b[1]) / 2.];
|
||||
assert!(scene.distance(mid).abs() < 0.12 / scale, "off outline: {mid:?}");
|
||||
assert!(
|
||||
scene.distance(mid).abs() < 0.12 / scale,
|
||||
"off outline: {mid:?}"
|
||||
);
|
||||
// A shared body/fillet seam has fill on both sides, unlike a visible rim.
|
||||
let delta = [b[0] - a[0], b[1] - a[1]];
|
||||
let length = delta[0].hypot(delta[1]);
|
||||
@@ -266,7 +311,9 @@ mod tests {
|
||||
fn attached_morph_rim_follows_animated_body_at_fractional_scales() {
|
||||
for scale in [1., 1.25, 1.5, 2.] {
|
||||
for edge in [crate::Edge::Top, crate::Edge::Bottom] {
|
||||
for (width, height, radius) in [(150., 34., 17.), (218.3, 41.7, 20.85), (340., 230., 26.)] {
|
||||
for (width, height, radius) in
|
||||
[(150., 34., 17.), (218.3, 41.7, 20.85), (340., 230., 26.)]
|
||||
{
|
||||
let scene = derive(
|
||||
Parameters {
|
||||
style: Style::Morph,
|
||||
@@ -283,7 +330,11 @@ mod tests {
|
||||
.unwrap();
|
||||
let body = scene.paint[0];
|
||||
assert_eq!(scene.paint.len(), 3);
|
||||
assert_eq!(scene.input.len(), 1, "decorative shoulders must not take pointer input");
|
||||
assert_eq!(
|
||||
scene.input.len(),
|
||||
1,
|
||||
"decorative shoulders must not take pointer input"
|
||||
);
|
||||
let screen_y = if edge == crate::Edge::Top {
|
||||
body.bounds.y
|
||||
} else {
|
||||
@@ -299,7 +350,10 @@ mod tests {
|
||||
assert!((path.first().unwrap()[1] - screen_y).abs() < 1e-6);
|
||||
assert!((path.last().unwrap()[1] - screen_y).abs() < 1e-6);
|
||||
assert!(path.iter().any(|p| p[0] < body.bounds.x));
|
||||
assert!(path.iter().any(|p| p[0] > body.bounds.x + body.bounds.width));
|
||||
assert!(
|
||||
path.iter()
|
||||
.any(|p| p[0] > body.bounds.x + body.bounds.width)
|
||||
);
|
||||
for segment in path.windows(2) {
|
||||
let [a, b] = [segment[0], segment[1]];
|
||||
assert!(
|
||||
|
||||
@@ -220,9 +220,12 @@ impl<'de> Deserialize<'de> for Item {
|
||||
return Err(serde::de::Error::unknown_field(key, OPTIONS));
|
||||
}
|
||||
}
|
||||
if fields.contains_key("style") && fields.get("kind").and_then(serde_json::Value::as_str) != Some("workspaces")
|
||||
if fields.contains_key("style")
|
||||
&& fields.get("kind").and_then(serde_json::Value::as_str) != Some("workspaces")
|
||||
{
|
||||
return Err(serde::de::Error::custom("style is only supported by Workspaces items"));
|
||||
return Err(serde::de::Error::custom(
|
||||
"style is only supported by Workspaces items",
|
||||
));
|
||||
}
|
||||
if fields.contains_key("percentage")
|
||||
&& fields.get("kind").and_then(serde_json::Value::as_str) != Some("battery")
|
||||
@@ -447,7 +450,10 @@ pub fn validate(panels: &[Panel]) -> Result<(), String> {
|
||||
let mut panel_ids = HashSet::new();
|
||||
for panel in panels {
|
||||
if !PanelStyle::AVAILABLE.contains(&panel.style) {
|
||||
return Err(format!("panel style {} is not available yet", panel.style.key()));
|
||||
return Err(format!(
|
||||
"panel style {} is not available yet",
|
||||
panel.style.key()
|
||||
));
|
||||
}
|
||||
panel.geometry.validate()?;
|
||||
panel.morph.validate()?;
|
||||
@@ -497,7 +503,10 @@ pub fn validate(panels: &[Panel]) -> Result<(), String> {
|
||||
return Err("panel item priority must be 0..100 and gaps 0..64".into());
|
||||
}
|
||||
if item.kind == ItemKind::Overflow {
|
||||
return Err("overflow is supplied by the panel; it cannot be configured as an item".into());
|
||||
return Err(
|
||||
"overflow is supplied by the panel; it cannot be configured as an item"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -538,7 +547,11 @@ impl Panel {
|
||||
}
|
||||
|
||||
pub fn zone_spacing(&self, zone: &Zone) -> f32 {
|
||||
if self.style.connected() { 0. } else { zone.spacing }
|
||||
if self.style.connected() {
|
||||
0.
|
||||
} else {
|
||||
zone.spacing
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shape_parameters(&self) -> crate::scene::Parameters {
|
||||
@@ -606,7 +619,8 @@ impl Panel {
|
||||
}
|
||||
|
||||
pub fn resolved_radius(&self, shell_radius: f32) -> CornerRadii {
|
||||
self.corner_radius.unwrap_or_else(|| CornerRadii::uniform(shell_radius))
|
||||
self.corner_radius
|
||||
.unwrap_or_else(|| CornerRadii::uniform(shell_radius))
|
||||
}
|
||||
|
||||
/// The overflow trigger is generated, never persisted as a configured item.
|
||||
@@ -629,7 +643,9 @@ impl Panel {
|
||||
.end
|
||||
.groups
|
||||
.first()
|
||||
.map_or(crate::default_island_padding(), |group| group.island_padding),
|
||||
.map_or(crate::default_island_padding(), |group| {
|
||||
group.island_padding
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,7 +701,12 @@ impl Panel {
|
||||
let mut controls = controls;
|
||||
controls.extend([clock, display]);
|
||||
(
|
||||
vec![Group::new("navigation", vec![overview, workspaces], [2, 3], padding)],
|
||||
vec![Group::new(
|
||||
"navigation",
|
||||
vec![overview, workspaces],
|
||||
[2, 3],
|
||||
padding,
|
||||
)],
|
||||
Group::new("title", vec![title], [0, 0], padding),
|
||||
vec![Group::new("status", controls, [2, 3], padding)],
|
||||
)
|
||||
@@ -703,7 +724,11 @@ impl Panel {
|
||||
corner_radius: Option::None,
|
||||
start: Zone {
|
||||
groups: start,
|
||||
spacing: if preset == PanelStyle::Floating { 8.0 } else { 3.0 },
|
||||
spacing: if preset == PanelStyle::Floating {
|
||||
8.0
|
||||
} else {
|
||||
3.0
|
||||
},
|
||||
},
|
||||
center: Zone {
|
||||
groups: vec![center],
|
||||
@@ -821,7 +846,9 @@ mod tests {
|
||||
battery: true,
|
||||
external_display: true
|
||||
}),
|
||||
all.iter().map(|item| item.id.0.as_str()).collect::<Vec<_>>()
|
||||
all.iter()
|
||||
.map(|item| item.id.0.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(items(&panel).len(), 13);
|
||||
}
|
||||
@@ -842,7 +869,10 @@ mod tests {
|
||||
{
|
||||
group.island_padding = 13.;
|
||||
}
|
||||
assert_eq!(panel.center.groups[0].items[0].kind, ItemKind::FocusedWindow);
|
||||
assert_eq!(
|
||||
panel.center.groups[0].items[0].kind,
|
||||
ItemKind::FocusedWindow
|
||||
);
|
||||
assert!(!panel.center.groups[0].items[0].visible);
|
||||
let all = items(&panel);
|
||||
assert_eq!(
|
||||
@@ -854,7 +884,13 @@ mod tests {
|
||||
assert_eq!(group.island_padding, 13.0);
|
||||
}
|
||||
}
|
||||
assert_eq!(all.iter().find(|item| item.id.0 == "clock").unwrap().gap_before, None);
|
||||
assert_eq!(
|
||||
all.iter()
|
||||
.find(|item| item.id.0 == "clock")
|
||||
.unwrap()
|
||||
.gap_before,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -25,8 +25,16 @@ impl GeometryIntent {
|
||||
Morph => 34.,
|
||||
_ => 36.,
|
||||
}),
|
||||
edge_margin: if flush { 0 } else { self.edge_margin.unwrap_or(8) },
|
||||
side_margins: if flush { 0 } else { self.side_margins.unwrap_or(8) },
|
||||
edge_margin: if flush {
|
||||
0
|
||||
} else {
|
||||
self.edge_margin.unwrap_or(8)
|
||||
},
|
||||
side_margins: if flush {
|
||||
0
|
||||
} else {
|
||||
self.side_margins.unwrap_or(8)
|
||||
},
|
||||
window_clearance: self.window_clearance.unwrap_or(0),
|
||||
inner_padding: self.inner_padding.unwrap_or(12.),
|
||||
}
|
||||
@@ -74,7 +82,10 @@ impl PanelGeometry {
|
||||
.into_iter()
|
||||
.any(|value| !(0..=4096).contains(&value))
|
||||
{
|
||||
return Err("panel height must be 24..128, inner padding 0..64, and margins/clearance 0..4096".into());
|
||||
return Err(
|
||||
"panel height must be 24..128, inner padding 0..64, and margins/clearance 0..4096"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -101,7 +112,13 @@ mod tests {
|
||||
assert_eq!(pins.resolve(Flat).side_margins, 0);
|
||||
assert_eq!(pins.resolve(Floating).side_margins, 18);
|
||||
assert_eq!(pins.resolve(Notch).edge_margin, 0);
|
||||
assert!(serde_json::to_value(intent).unwrap().as_object().unwrap().is_empty());
|
||||
assert!(
|
||||
serde_json::to_value(intent)
|
||||
.unwrap()
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert!(
|
||||
GeometryIntent {
|
||||
edge_margin: Some(-1),
|
||||
|
||||
@@ -10,7 +10,10 @@ pub struct Measurement {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Placement {
|
||||
Visible { representation: Representation, width: f32 },
|
||||
Visible {
|
||||
representation: Representation,
|
||||
width: f32,
|
||||
},
|
||||
Overflow,
|
||||
}
|
||||
|
||||
@@ -103,7 +106,12 @@ impl Resolution {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(panel: &Panel, measurements: &[Measurement], available: f32, overflow_width: f32) -> Resolution {
|
||||
pub fn resolve(
|
||||
panel: &Panel,
|
||||
measurements: &[Measurement],
|
||||
available: f32,
|
||||
overflow_width: f32,
|
||||
) -> Resolution {
|
||||
resolve_with_gap(panel, measurements, available, overflow_width, 8.)
|
||||
}
|
||||
|
||||
@@ -127,7 +135,10 @@ pub fn resolve_with_gap(
|
||||
.flat_map(|zone| &zone.groups)
|
||||
.flat_map(|group| &group.items)
|
||||
.collect();
|
||||
let measurements: BTreeMap<_, _> = measurements.iter().map(|measured| (&measured.id, measured)).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();
|
||||
@@ -143,9 +154,13 @@ pub fn resolve_with_gap(
|
||||
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.items.insert(
|
||||
item.id.clone(),
|
||||
Placement::Visible {
|
||||
representation,
|
||||
width,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
result.update_widths(panel, overflow_width);
|
||||
@@ -164,13 +179,20 @@ pub fn resolve_with_gap(
|
||||
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 {
|
||||
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 {
|
||||
let Some(Placement::Visible {
|
||||
representation,
|
||||
width,
|
||||
}) = result.items.get(&measured.id).cloned()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let minimum = measured.minimum.unwrap().min(width);
|
||||
@@ -191,12 +213,18 @@ pub fn resolve_with_gap(
|
||||
continue;
|
||||
};
|
||||
for &(representation, width) in measured.alternatives.iter().skip(1) {
|
||||
if let Some(Placement::Visible { width: previous, .. }) = result.items.get(&item.id)
|
||||
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.items.insert(
|
||||
item.id.clone(),
|
||||
Placement::Visible {
|
||||
representation,
|
||||
width,
|
||||
},
|
||||
);
|
||||
result.update_widths(panel, overflow_width);
|
||||
if fits(&result) {
|
||||
break;
|
||||
@@ -214,7 +242,8 @@ pub fn resolve_with_gap(
|
||||
if item.overflow == OverflowPolicy::Never && !force {
|
||||
continue;
|
||||
}
|
||||
if matches!(result.items.get(&item.id), Some(Placement::Visible { width, .. }) if *width > 0.0) {
|
||||
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;
|
||||
@@ -235,7 +264,11 @@ pub fn resolve_with_gap(
|
||||
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 {
|
||||
let Some(&(representation, width)) = measured
|
||||
.alternatives
|
||||
.iter()
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
result.items.insert(
|
||||
@@ -255,7 +288,8 @@ pub fn resolve_with_gap(
|
||||
}
|
||||
}
|
||||
result.forced_overflow = ordered.iter().any(|item| {
|
||||
item.overflow == OverflowPolicy::Never && matches!(result.items.get(&item.id), Some(Placement::Overflow))
|
||||
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.
|
||||
@@ -263,7 +297,9 @@ pub fn resolve_with_gap(
|
||||
let Some(measured) = measurements.get(&item.id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(previous @ Placement::Visible { representation, .. }) = result.items.get(&item.id).cloned() else {
|
||||
let Some(previous @ Placement::Visible { representation, .. }) =
|
||||
result.items.get(&item.id).cloned()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for &(candidate, width) in measured
|
||||
@@ -288,7 +324,10 @@ pub fn resolve_with_gap(
|
||||
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 {
|
||||
let Some(measured) = measurements
|
||||
.get(&item.id)
|
||||
.filter(|measured| measured.minimum.is_some())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(Placement::Visible {
|
||||
@@ -309,9 +348,16 @@ pub fn resolve_with_gap(
|
||||
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.items.get_mut(&measured.id).unwrap() = Placement::Visible {
|
||||
representation,
|
||||
width,
|
||||
};
|
||||
result.update_widths(panel, overflow_width);
|
||||
if fits(&result) { low = width } else { high = width }
|
||||
if fits(&result) {
|
||||
low = width
|
||||
} else {
|
||||
high = width
|
||||
}
|
||||
}
|
||||
*result.items.get_mut(&measured.id).unwrap() = Placement::Visible {
|
||||
representation,
|
||||
@@ -333,7 +379,11 @@ mod tests {
|
||||
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] {
|
||||
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;
|
||||
@@ -360,7 +410,10 @@ mod tests {
|
||||
let resolution = resolve(&panel, &measurements, 300., 32.);
|
||||
assert!(resolution.fits(300.));
|
||||
assert_eq!(
|
||||
matches!(resolution.items[&ItemId("small".into())], Placement::Visible { .. }),
|
||||
matches!(
|
||||
resolution.items[&ItemId("small".into())],
|
||||
Placement::Visible { .. }
|
||||
),
|
||||
policy != OverflowPolicy::Always
|
||||
);
|
||||
assert!(resolution.overflow.contains(&ItemId("large".into())));
|
||||
@@ -381,10 +434,18 @@ mod tests {
|
||||
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 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 })],
|
||||
vec![Item::new(
|
||||
"trailing",
|
||||
ItemKind::Battery { percentage: false },
|
||||
)],
|
||||
[0, 2],
|
||||
5.,
|
||||
);
|
||||
@@ -630,9 +691,15 @@ mod tests {
|
||||
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);
|
||||
assert_eq!(
|
||||
resolve_with_gap(&panel, &measured, width, 32., 16.),
|
||||
expected
|
||||
);
|
||||
measured.reverse();
|
||||
assert_eq!(resolve_with_gap(&panel, &measured, width, 32., 16.), expected);
|
||||
assert_eq!(
|
||||
resolve_with_gap(&panel, &measured, width, 32., 16.),
|
||||
expected
|
||||
);
|
||||
measured.reverse();
|
||||
}
|
||||
}
|
||||
@@ -700,7 +767,10 @@ mod tests {
|
||||
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.overflow.is_empty(),
|
||||
"output {output_width}, margin {margin}"
|
||||
);
|
||||
assert!(result.fits(available));
|
||||
assert!(margin <= requested);
|
||||
}
|
||||
@@ -714,7 +784,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn margin_limit_includes_group_padding_and_authored_overflow_without_dependence_on_current_surface_width() {
|
||||
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);
|
||||
@@ -722,7 +793,12 @@ mod tests {
|
||||
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.);
|
||||
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;
|
||||
@@ -794,7 +870,10 @@ mod tests {
|
||||
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");
|
||||
assert_eq!(
|
||||
panel, initial,
|
||||
"resolution must not rewrite configuration intent"
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
@@ -804,8 +883,15 @@ mod tests {
|
||||
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();
|
||||
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;
|
||||
|
||||
@@ -79,7 +79,8 @@ impl std::str::FromStr for CornerRadii {
|
||||
let number = if part == "0" {
|
||||
part
|
||||
} else {
|
||||
part.strip_suffix("px").ok_or("use pixel values such as 4px")?
|
||||
part.strip_suffix("px")
|
||||
.ok_or("use pixel values such as 4px")?
|
||||
};
|
||||
number.parse::<f32>().map_err(|_| "invalid corner radius")
|
||||
})
|
||||
@@ -152,7 +153,10 @@ mod tests {
|
||||
let radii = CornerRadii([4., 8., 12., 16.]);
|
||||
assert_eq!(radii.at_top_edge(true).0, [0., 0., 12., 16.]);
|
||||
assert_eq!(radii.at_top_edge(false), radii);
|
||||
assert_eq!(radii.at_edge(super::super::Edge::Bottom, true).0, [4., 8., 0., 0.]);
|
||||
assert_eq!(
|
||||
radii.at_edge(super::super::Edge::Bottom, true).0,
|
||||
[4., 8., 0., 0.]
|
||||
);
|
||||
assert_eq!(radii.at_edge(super::super::Edge::Bottom, false), radii);
|
||||
assert_eq!(radii.0, [4., 8., 12., 16.]);
|
||||
}
|
||||
@@ -171,6 +175,9 @@ mod tests {
|
||||
] {
|
||||
assert!(source.parse::<CornerRadii>().is_err(), "{source}");
|
||||
}
|
||||
assert_eq!(CornerRadii([4., 40., 0., 18.]).clamped(100., 28.).0, [4., 14., 0., 14.]);
|
||||
assert_eq!(
|
||||
CornerRadii([4., 40., 0., 18.]).clamped(100., 28.).0,
|
||||
[4., 14., 0., 14.]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,12 @@ pub struct Primitive {
|
||||
|
||||
impl Primitive {
|
||||
pub fn packet(self) -> [f32; 10] {
|
||||
let Rect { x, y, width, height } = self.bounds;
|
||||
let Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
} = self.bounds;
|
||||
let [tl, tr, br, bl] = self.radii.0;
|
||||
[
|
||||
x as f32,
|
||||
@@ -34,7 +39,9 @@ impl Primitive {
|
||||
}
|
||||
|
||||
pub fn from_packet(values: [f32; 10]) -> Result<Self, &'static str> {
|
||||
if !values.into_iter().all(|v| v.is_finite() && v.abs() <= 32768.)
|
||||
if !values
|
||||
.into_iter()
|
||||
.all(|v| v.is_finite() && v.abs() <= 32768.)
|
||||
|| values[2] <= 0.
|
||||
|| values[3] <= 0.
|
||||
|| values[4..8].iter().any(|v| *v < 0.)
|
||||
@@ -46,8 +53,14 @@ impl Primitive {
|
||||
return Err("invalid panel shape primitive");
|
||||
}
|
||||
Ok(Self {
|
||||
bounds: Rect::new(values[0].into(), values[1].into(), values[2].into(), values[3].into()),
|
||||
radii: CornerRadii([values[4], values[5], values[6], values[7]]).normalized(values[2], values[3]),
|
||||
bounds: Rect::new(
|
||||
values[0].into(),
|
||||
values[1].into(),
|
||||
values[2].into(),
|
||||
values[3].into(),
|
||||
),
|
||||
radii: CornerRadii([values[4], values[5], values[6], values[7]])
|
||||
.normalized(values[2], values[3]),
|
||||
operation: values[8] as u8,
|
||||
blend_zone: values[9] as u8,
|
||||
})
|
||||
@@ -63,7 +76,8 @@ impl Primitive {
|
||||
4 => [b.x + b.width, b.y],
|
||||
_ => [b.x, b.y],
|
||||
};
|
||||
return rect.max(b.width.min(b.height) - (point[0] - center[0]).hypot(point[1] - center[1]));
|
||||
return rect
|
||||
.max(b.width.min(b.height) - (point[0] - center[0]).hypot(point[1] - center[1]));
|
||||
}
|
||||
let b = self.bounds;
|
||||
let (x, y) = (point[0] - b.x, point[1] - b.y);
|
||||
@@ -150,12 +164,21 @@ impl Scene {
|
||||
let bottom = ((b.y + b.height) * scale).ceil() as i32;
|
||||
for y in top..bottom {
|
||||
let mut first = left;
|
||||
while first < right && !shape.contains([(f64::from(first) + 0.5) / scale, (f64::from(y) + 0.5) / scale])
|
||||
while first < right
|
||||
&& !shape.contains([
|
||||
(f64::from(first) + 0.5) / scale,
|
||||
(f64::from(y) + 0.5) / scale,
|
||||
])
|
||||
{
|
||||
first += 1;
|
||||
}
|
||||
let mut last = right;
|
||||
while last > first && !shape.contains([(f64::from(last) - 0.5) / scale, (f64::from(y) + 0.5) / scale]) {
|
||||
while last > first
|
||||
&& !shape.contains([
|
||||
(f64::from(last) - 0.5) / scale,
|
||||
(f64::from(y) + 0.5) / scale,
|
||||
])
|
||||
{
|
||||
last -= 1;
|
||||
}
|
||||
if first == last {
|
||||
@@ -328,8 +351,18 @@ pub fn derive(
|
||||
Style::CornerWrap => {
|
||||
let h = parameters.height.min(bounds.height);
|
||||
let w = parameters.wrap_width.min(bounds.width);
|
||||
let horizontal = Rect::new(bounds.x, bounds.y, parameters.wrap_size[0].min(bounds.width), h);
|
||||
let vertical = Rect::new(bounds.x, bounds.y, w, parameters.wrap_size[1].min(bounds.height));
|
||||
let horizontal = Rect::new(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
parameters.wrap_size[0].min(bounds.width),
|
||||
h,
|
||||
);
|
||||
let vertical = Rect::new(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
w,
|
||||
parameters.wrap_size[1].min(bounds.height),
|
||||
);
|
||||
add(
|
||||
&mut scene,
|
||||
grid,
|
||||
@@ -338,7 +371,14 @@ pub fn derive(
|
||||
true,
|
||||
0,
|
||||
)?;
|
||||
add(&mut scene, grid, vertical, CornerRadii([0., 0., 14., 0.]), true, 0)?;
|
||||
add(
|
||||
&mut scene,
|
||||
grid,
|
||||
vertical,
|
||||
CornerRadii([0., 0., 14., 0.]),
|
||||
true,
|
||||
0,
|
||||
)?;
|
||||
// Capture the union once: overlapping translucent captures darken the elbow.
|
||||
scene.damage = vec![
|
||||
horizontal,
|
||||
@@ -388,8 +428,12 @@ pub fn derive(
|
||||
// One material capture owns the body and both shoulders. Separate
|
||||
// captures overlap after logical rounding at fractional scales,
|
||||
// painting translucent joins twice as the spring settles.
|
||||
scene.damage =
|
||||
vec![grid.rect(Rect::new(bounds.x - r, bounds.y, bounds.width + 2. * r, bounds.height))?];
|
||||
scene.damage = vec![grid.rect(Rect::new(
|
||||
bounds.x - r,
|
||||
bounds.y,
|
||||
bounds.width + 2. * r,
|
||||
bounds.height,
|
||||
))?];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -406,7 +450,10 @@ pub fn derive(
|
||||
continue;
|
||||
}
|
||||
if style.connected() {
|
||||
let x = occupied.iter().map(|g| g.bounds.x).fold(f64::INFINITY, f64::min);
|
||||
let x = occupied
|
||||
.iter()
|
||||
.map(|g| g.bounds.x)
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
let end = occupied
|
||||
.iter()
|
||||
.map(|g| g.bounds.x + g.bounds.width)
|
||||
@@ -440,9 +487,12 @@ pub fn derive(
|
||||
add(&mut scene, grid, body, radii, true, 0)?;
|
||||
for group in occupied.iter().take(occupied.len() - 1) {
|
||||
let seam = grid.snap(group.bounds.x + group.bounds.width);
|
||||
scene
|
||||
.dividers
|
||||
.push([seam, bounds.y + 8., seam, bounds.y + bounds.height - 8.]);
|
||||
scene.dividers.push([
|
||||
seam,
|
||||
bounds.y + 8.,
|
||||
seam,
|
||||
bounds.y + bounds.height - 8.,
|
||||
]);
|
||||
}
|
||||
if style == Style::Notch && zone == ZonePosition::Center {
|
||||
let r = parameters
|
||||
@@ -469,7 +519,8 @@ pub fn derive(
|
||||
}
|
||||
} else {
|
||||
for group in occupied {
|
||||
let body = Rect::new(group.bounds.x, bounds.y, group.bounds.width, bounds.height);
|
||||
let body =
|
||||
Rect::new(group.bounds.x, bounds.y, group.bounds.width, bounds.height);
|
||||
let radii = parameters
|
||||
.radius
|
||||
.map_or(CornerRadii::uniform((bounds.height / 2.) as f32), |r| {
|
||||
@@ -500,7 +551,8 @@ pub fn derive(
|
||||
if let Some(first) = shapes.next() {
|
||||
let mut region = first.bounds;
|
||||
for shape in shapes {
|
||||
let right = (region.x + region.width).max(shape.bounds.x + shape.bounds.width);
|
||||
let right =
|
||||
(region.x + region.width).max(shape.bounds.x + shape.bounds.width);
|
||||
region.x = region.x.min(shape.bounds.x);
|
||||
region.width = right - region.x;
|
||||
}
|
||||
@@ -510,7 +562,9 @@ pub fn derive(
|
||||
}
|
||||
}
|
||||
}
|
||||
if parameters.edge == Edge::Bottom && matches!(parameters.style, Style::Frame | Style::CornerWrap) {
|
||||
if parameters.edge == Edge::Bottom
|
||||
&& matches!(parameters.style, Style::Frame | Style::CornerWrap)
|
||||
{
|
||||
let mirror = |mut r: Rect| {
|
||||
r.y = 2. * bounds.y + bounds.height - r.y - r.height;
|
||||
r
|
||||
@@ -529,7 +583,8 @@ pub fn derive(
|
||||
}
|
||||
for input in &mut scene.input {
|
||||
let [tl, tr, br, bl] = input.radii().0;
|
||||
*input = RoundedSurface::new(mirror(input.bounds()), CornerRadii([bl, br, tr, tl]), grid)?;
|
||||
*input =
|
||||
RoundedSurface::new(mirror(input.bounds()), CornerRadii([bl, br, tr, tl]), grid)?;
|
||||
}
|
||||
for damage in &mut scene.damage {
|
||||
*damage = mirror(*damage);
|
||||
@@ -629,7 +684,12 @@ mod tests {
|
||||
assert!(!scene.contains([38., 38.])); // the decorative elbow is not input
|
||||
for (i, a) in scene.damage.iter().enumerate() {
|
||||
for b in &scene.damage[i + 1..] {
|
||||
assert!(a.x + a.width <= b.x || b.x + b.width <= a.x || a.y + a.height <= b.y || b.y + b.height <= a.y);
|
||||
assert!(
|
||||
a.x + a.width <= b.x
|
||||
|| b.x + b.width <= a.x
|
||||
|| a.y + a.height <= b.y
|
||||
|| b.y + b.height <= a.y
|
||||
);
|
||||
}
|
||||
}
|
||||
for x in (0..800).step_by(3) {
|
||||
|
||||
@@ -77,7 +77,12 @@ impl Style {
|
||||
pub const fn detached(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Floating | Self::Capsules | Self::Fused | Self::Morph | Self::Adaptive | Self::WindowHug
|
||||
Self::Floating
|
||||
| Self::Capsules
|
||||
| Self::Fused
|
||||
| Self::Morph
|
||||
| Self::Adaptive
|
||||
| Self::WindowHug
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -129,7 +134,10 @@ pub fn effective_radii(
|
||||
index: usize,
|
||||
count: usize,
|
||||
) -> Result<CornerRadii, &'static str> {
|
||||
if !size.into_iter().all(|value| value.is_finite() && value >= 0.) {
|
||||
if !size
|
||||
.into_iter()
|
||||
.all(|value| value.is_finite() && value >= 0.)
|
||||
{
|
||||
return Err("panel dimensions must be finite and nonnegative");
|
||||
}
|
||||
if index >= count {
|
||||
@@ -137,9 +145,12 @@ pub fn effective_radii(
|
||||
}
|
||||
let [width, height] = size;
|
||||
let base = AnchoredRadii(match style {
|
||||
Style::Floating | Style::Capsules | Style::Fused | Style::Morph | Style::Adaptive | Style::WindowHug => {
|
||||
[height / 2.; 4]
|
||||
}
|
||||
Style::Floating
|
||||
| Style::Capsules
|
||||
| Style::Fused
|
||||
| Style::Morph
|
||||
| Style::Adaptive
|
||||
| Style::WindowHug => [height / 2.; 4],
|
||||
Style::Notch => match zone {
|
||||
ZonePosition::Start => [0., 0., 14., 0.],
|
||||
ZonePosition::Center => [0., 0., 20., 20.],
|
||||
@@ -189,7 +200,12 @@ impl PixelGrid {
|
||||
if ![left, top, right, bottom].into_iter().all(f64::is_finite) {
|
||||
return Err("surface bounds exceed the physical pixel grid");
|
||||
}
|
||||
Ok(Rect::new(left, top, (right - left).max(0.), (bottom - top).max(0.)))
|
||||
Ok(Rect::new(
|
||||
left,
|
||||
top,
|
||||
(right - left).max(0.),
|
||||
(bottom - top).max(0.),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +282,17 @@ mod tests {
|
||||
assert_eq!(radius(1, 2).0, [0., 18., 18., 0.]);
|
||||
assert_eq!(radius(0, 1).0, [18.; 4]);
|
||||
assert_eq!(radius(0, 2).0, [18., 0., 0., 18.]);
|
||||
assert!(effective_radii(Style::Capsules, ZonePosition::Start, Edge::Top, [100., 36.], 0, 0).is_err());
|
||||
assert!(
|
||||
effective_radii(
|
||||
Style::Capsules,
|
||||
ZonePosition::Start,
|
||||
Edge::Top,
|
||||
[100., 36.],
|
||||
0,
|
||||
0
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -288,14 +314,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn asymmetric_corners_preserve_notch_shape_and_scale_proportionally() {
|
||||
let notch = effective_radii(Style::Notch, ZonePosition::Center, Edge::Top, [100., 28.], 0, 1).unwrap();
|
||||
let notch = effective_radii(
|
||||
Style::Notch,
|
||||
ZonePosition::Center,
|
||||
Edge::Top,
|
||||
[100., 28.],
|
||||
0,
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(notch.0, [0., 0., 20., 20.]);
|
||||
assert_eq!(notch.normalized(30., 28.).0, [0., 0., 15., 15.]);
|
||||
assert_eq!(
|
||||
CornerRadii([10., 20., 40., 30.]).normalized(35., 25.).0,
|
||||
[25. / 6., 25. / 3., 50. / 3., 12.5]
|
||||
);
|
||||
assert_eq!(CornerRadii::uniform(f32::MAX).normalized(40., 28.).0, [14.; 4]);
|
||||
assert_eq!(
|
||||
CornerRadii::uniform(f32::MAX).normalized(40., 28.).0,
|
||||
[14.; 4]
|
||||
);
|
||||
assert_eq!(notch.normalized(0., 28.).0, [0.; 4]);
|
||||
}
|
||||
|
||||
@@ -317,9 +354,25 @@ mod tests {
|
||||
#[test]
|
||||
fn floating_gaps_and_round_corners_do_not_capture_input() {
|
||||
let grid = PixelGrid::new(1.25).unwrap();
|
||||
let left = RoundedSurface::new(Rect::new(8., 8., 100., 36.), CornerRadii::uniform(18.), grid).unwrap();
|
||||
let right = RoundedSurface::new(Rect::new(116., 8., 100., 36.), CornerRadii::uniform(18.), grid).unwrap();
|
||||
for point in [[0., 0.], [8., 8.], [112., 26.], [216., 26.], [f64::NAN, 26.]] {
|
||||
let left = RoundedSurface::new(
|
||||
Rect::new(8., 8., 100., 36.),
|
||||
CornerRadii::uniform(18.),
|
||||
grid,
|
||||
)
|
||||
.unwrap();
|
||||
let right = RoundedSurface::new(
|
||||
Rect::new(116., 8., 100., 36.),
|
||||
CornerRadii::uniform(18.),
|
||||
grid,
|
||||
)
|
||||
.unwrap();
|
||||
for point in [
|
||||
[0., 0.],
|
||||
[8., 8.],
|
||||
[112., 26.],
|
||||
[216., 26.],
|
||||
[f64::NAN, 26.],
|
||||
] {
|
||||
assert!(!left.contains(point) && !right.contains(point), "{point:?}");
|
||||
}
|
||||
assert!(left.contains([30., 26.]));
|
||||
@@ -330,7 +383,12 @@ mod tests {
|
||||
fn asymmetric_hit_testing_matches_a_vertically_mirrored_surface() {
|
||||
let grid = PixelGrid::new(1.).unwrap();
|
||||
let bounds = Rect::new(0., 0., 100., 28.);
|
||||
let top = RoundedSurface::new(bounds, AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Top), grid).unwrap();
|
||||
let top = RoundedSurface::new(
|
||||
bounds,
|
||||
AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Top),
|
||||
grid,
|
||||
)
|
||||
.unwrap();
|
||||
let bottom = RoundedSurface::new(
|
||||
bounds,
|
||||
AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Bottom),
|
||||
@@ -352,8 +410,18 @@ mod tests {
|
||||
#[test]
|
||||
fn a_connected_seam_has_exactly_one_input_owner() {
|
||||
let grid = PixelGrid::new(1.5).unwrap();
|
||||
let left = RoundedSurface::new(Rect::new(0., 0., 30., 36.), CornerRadii([18., 0., 0., 18.]), grid).unwrap();
|
||||
let right = RoundedSurface::new(Rect::new(30., 0., 40., 36.), CornerRadii([0., 18., 18., 0.]), grid).unwrap();
|
||||
let left = RoundedSurface::new(
|
||||
Rect::new(0., 0., 30., 36.),
|
||||
CornerRadii([18., 0., 0., 18.]),
|
||||
grid,
|
||||
)
|
||||
.unwrap();
|
||||
let right = RoundedSurface::new(
|
||||
Rect::new(30., 0., 40., 36.),
|
||||
CornerRadii([0., 18., 18., 0.]),
|
||||
grid,
|
||||
)
|
||||
.unwrap();
|
||||
for y in [0., 18., 35.9] {
|
||||
assert!(!left.contains([30., y]));
|
||||
assert!(right.contains([30., y]));
|
||||
@@ -373,7 +441,20 @@ mod tests {
|
||||
] {
|
||||
assert!(grid.rect(bounds).is_err());
|
||||
}
|
||||
assert!(RoundedSurface::new(Rect::new(0., 0., 10., 28.), CornerRadii::uniform(-1.), grid).is_err());
|
||||
assert!(effective_radii(Style::Floating, ZonePosition::Start, Edge::Top, [100., f32::NAN], 0, 1).is_err());
|
||||
assert!(
|
||||
RoundedSurface::new(Rect::new(0., 0., 10., 28.), CornerRadii::uniform(-1.), grid)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
effective_radii(
|
||||
Style::Floating,
|
||||
ZonePosition::Start,
|
||||
Edge::Top,
|
||||
[100., f32::NAN],
|
||||
0,
|
||||
1
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user