Clarify constructor failures and borrowed API contracts
This commit is contained in:
1 parent
23e280a6b4
commit
a7fcd69897
55 files changed
+332
-102
No files matched your search
@@ -18,6 +18,7 @@ pub enum Presentation {
|
||||
}
|
||||
|
||||
impl Presentation {
|
||||
#[must_use]
|
||||
pub fn style(self) -> Option<PanelStyle> {
|
||||
match self {
|
||||
Self::Floating => Some(PanelStyle::Floating),
|
||||
@@ -48,6 +49,7 @@ pub struct Adaptive {
|
||||
}
|
||||
|
||||
impl Adaptive {
|
||||
#[must_use]
|
||||
pub fn new(state: WindowState) -> Self {
|
||||
Self {
|
||||
current: state.into(),
|
||||
@@ -55,6 +57,7 @@ impl Adaptive {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current(&self) -> Presentation {
|
||||
self.current
|
||||
}
|
||||
@@ -87,6 +90,7 @@ impl Adaptive {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn deadline(&self) -> Option<Duration> {
|
||||
self.pending
|
||||
.map(|(_, since)| since.saturating_add(HYSTERESIS))
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod shape;
|
||||
|
||||
pub use shape::Style as PanelStyle;
|
||||
|
||||
#[must_use]
|
||||
pub const fn default_island_padding() -> f32 {
|
||||
4.0
|
||||
}
|
||||
@@ -75,6 +75,7 @@ impl Profile {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn size(self, mode: Mode, collapsed_height: f64) -> [f64; 2] {
|
||||
match mode {
|
||||
Mode::Idle => [self.idle_width, collapsed_height],
|
||||
@@ -114,6 +115,7 @@ pub enum Mode {
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
#[must_use]
|
||||
pub fn expanded(self) -> bool {
|
||||
matches!(self, Self::Expanded(_))
|
||||
}
|
||||
@@ -145,7 +147,7 @@ pub struct Hub {
|
||||
}
|
||||
|
||||
impl Hub {
|
||||
pub fn new(profile: Profile, collapsed_height: f64) -> Result<Self, &'static str> {
|
||||
pub fn try_new(profile: Profile, collapsed_height: f64) -> Result<Self, &'static str> {
|
||||
profile.validate()?;
|
||||
if !collapsed_height.is_finite() || !(24. ..=128.).contains(&collapsed_height) {
|
||||
return Err("hub height must be 24..128");
|
||||
@@ -196,6 +198,7 @@ impl Hub {
|
||||
self.motion.retarget(target)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode
|
||||
}
|
||||
@@ -301,10 +304,12 @@ impl Hub {
|
||||
self.active()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn active(&self) -> bool {
|
||||
self.motion.active() || self.sample().previous_opacity > 0. || self.sample().opacity < 1.
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
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
|
||||
@@ -324,11 +329,13 @@ impl Hub {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn allocation_size(&self) -> [f64; 2] {
|
||||
let maximum = self.motion.maximum_size();
|
||||
std::array::from_fn(|i| maximum[i].min(self.available[i]))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn target_size(&self) -> [f64; 2] {
|
||||
let target = self.motion.target().bounds;
|
||||
[target.width, target.height]
|
||||
@@ -341,7 +348,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn workspace_width_changes_preserve_content_and_motion() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget_idle(270., [800., 600.]).unwrap();
|
||||
hub.advance(Duration::from_millis(60), false);
|
||||
let before = hub.sample();
|
||||
@@ -358,7 +365,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn measured_height_preserves_fade_and_motion_and_caps_long_content() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Controls), [800., 600.])
|
||||
.unwrap();
|
||||
hub.advance(Duration::from_millis(180), false);
|
||||
@@ -382,7 +389,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_content_swap_fades_without_restarting_the_geometry() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Controls), [800., 600.])
|
||||
.unwrap();
|
||||
hub.advance(Duration::from_secs(2), false);
|
||||
@@ -404,7 +411,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn content_swap_uses_a_fixed_delay_instead_of_waiting_for_spring_settlement() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
|
||||
assert!(!hub.active());
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.])
|
||||
.unwrap();
|
||||
@@ -430,7 +437,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn an_interrupted_content_delay_never_reveals_the_cancelled_panel() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Media), [800., 600.])
|
||||
.unwrap();
|
||||
hub.advance(Duration::from_millis(80), false);
|
||||
@@ -448,7 +455,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn spring_overshoots_then_settles_within_its_reserved_surface() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.])
|
||||
.unwrap();
|
||||
let allocation = hub.allocation_size();
|
||||
@@ -468,7 +475,7 @@ 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();
|
||||
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.])
|
||||
.unwrap();
|
||||
for _ in 0..60 {
|
||||
@@ -486,7 +493,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn interrupted_expansion_keeps_the_presented_shape() {
|
||||
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
|
||||
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Media), [800., 600.])
|
||||
.unwrap();
|
||||
hub.advance(Duration::from_millis(40), false);
|
||||
@@ -502,7 +509,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn narrow_outputs_and_reduced_motion_keep_the_hub_bounded() {
|
||||
let mut hub = Hub::new(Profile::default(), 30.).unwrap();
|
||||
let mut hub = Hub::try_new(Profile::default(), 30.).unwrap();
|
||||
hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.])
|
||||
.unwrap();
|
||||
hub.advance(Duration::ZERO, true);
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct Geometry {
|
||||
const COUNT: usize = 18;
|
||||
|
||||
impl Geometry {
|
||||
#[must_use]
|
||||
pub fn rounded(bounds: Rect, radius: f64) -> Self {
|
||||
Self {
|
||||
bounds,
|
||||
@@ -75,7 +76,7 @@ pub struct Motion {
|
||||
}
|
||||
|
||||
impl Motion {
|
||||
pub fn new(geometry: Geometry) -> Result<Self, &'static str> {
|
||||
pub fn try_new(geometry: Geometry) -> Result<Self, &'static str> {
|
||||
Self::with_spring(
|
||||
geometry,
|
||||
SpringConfig {
|
||||
@@ -132,10 +133,12 @@ impl Motion {
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current(&self) -> Geometry {
|
||||
Geometry::from_values(self.values.map(|value| value.current))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn target(&self) -> Geometry {
|
||||
Geometry::from_values(self.values.map(|value| value.target))
|
||||
}
|
||||
@@ -180,7 +183,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn interruption_keeps_the_current_geometry_and_velocity() {
|
||||
let mut motion = Motion::new(collapsed()).unwrap();
|
||||
let mut motion = Motion::try_new(collapsed()).unwrap();
|
||||
motion.retarget(expanded()).unwrap();
|
||||
motion.advance(Duration::from_millis(16), false);
|
||||
let before = motion.current();
|
||||
@@ -197,7 +200,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn dimensions_share_one_trajectory_and_stop_requesting_frames() {
|
||||
let mut motion = Motion::new(collapsed()).unwrap();
|
||||
let mut motion = Motion::try_new(collapsed()).unwrap();
|
||||
motion.retarget(expanded()).unwrap();
|
||||
for _ in 0..12 {
|
||||
motion.advance(Duration::from_millis(16), false);
|
||||
@@ -218,7 +221,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn reduced_motion_snaps_an_inflight_animation() {
|
||||
let mut motion = Motion::new(collapsed()).unwrap();
|
||||
let mut motion = Motion::try_new(collapsed()).unwrap();
|
||||
motion.retarget(expanded()).unwrap();
|
||||
motion.advance(Duration::from_millis(16), false);
|
||||
assert!(!motion.advance(Duration::ZERO, true));
|
||||
@@ -263,7 +266,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn invalid_retarget_leaves_motion_untouched() {
|
||||
let mut motion = Motion::new(collapsed()).unwrap();
|
||||
let mut motion = Motion::try_new(collapsed()).unwrap();
|
||||
let mut invalid = expanded();
|
||||
invalid.bounds.height = f64::NAN;
|
||||
assert!(motion.retarget(invalid).is_err());
|
||||
|
||||
@@ -269,7 +269,7 @@ mod tests {
|
||||
zone: ZonePosition::Center,
|
||||
bounds: Rect::new(70.3, 0., 160.4, 44.),
|
||||
}],
|
||||
PixelGrid::new(scale).unwrap(),
|
||||
PixelGrid::try_new(scale).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let paths = notch_border(&scene, 0.1 / scale, edge).unwrap();
|
||||
@@ -325,7 +325,7 @@ mod tests {
|
||||
},
|
||||
Rect::new(30.3, 0., width, height),
|
||||
&[],
|
||||
PixelGrid::new(scale).unwrap(),
|
||||
PixelGrid::try_new(scale).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let body = scene.paint[0];
|
||||
@@ -420,7 +420,7 @@ mod tests {
|
||||
},
|
||||
Rect::new(0., 0., 96., 64.),
|
||||
&groups,
|
||||
PixelGrid::new(1.25).unwrap(),
|
||||
PixelGrid::try_new(1.25).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let contours = contours(&scene, 0.1).unwrap();
|
||||
|
||||
@@ -33,6 +33,7 @@ pub enum Edge {
|
||||
}
|
||||
|
||||
impl Edge {
|
||||
#[must_use]
|
||||
pub fn key(self) -> &'static str {
|
||||
match self {
|
||||
Self::Top => "top",
|
||||
@@ -86,6 +87,7 @@ impl WorkspaceStyle {
|
||||
Self::AppIcons,
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub fn key(self) -> &'static str {
|
||||
match self {
|
||||
Self::Numbers => "numbers",
|
||||
@@ -96,6 +98,7 @@ impl WorkspaceStyle {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Numbers => "Numbers",
|
||||
@@ -135,6 +138,7 @@ pub enum ItemKind {
|
||||
|
||||
impl ItemKind {
|
||||
/// Smallest useful allocation before a flexible control moves into overflow.
|
||||
#[must_use]
|
||||
pub fn minimum_width(self) -> Option<f32> {
|
||||
match self {
|
||||
Self::FocusedWindow => Some(48.),
|
||||
@@ -144,6 +148,7 @@ impl ItemKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Overview => "Overview",
|
||||
@@ -163,6 +168,7 @@ impl ItemKind {
|
||||
Self::Overflow => "Overflow",
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
pub fn representations(self) -> &'static [Representation] {
|
||||
use Representation::*;
|
||||
match self {
|
||||
@@ -265,6 +271,7 @@ impl<'de> Deserialize<'de> for Item {
|
||||
|
||||
impl Item {
|
||||
/// Start at the configured preference and retain supported smaller forms.
|
||||
#[must_use]
|
||||
pub fn representations(&self) -> Vec<Representation> {
|
||||
let supported = self.kind.representations();
|
||||
let preferred = self.representation.unwrap_or(supported[0]);
|
||||
@@ -279,6 +286,7 @@ impl Item {
|
||||
alternatives
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
pub fn new(id: &str, kind: ItemKind) -> Self {
|
||||
Self {
|
||||
id: ItemId(id.into()),
|
||||
@@ -305,6 +313,7 @@ impl Item {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn available(&self, availability: Availability) -> bool {
|
||||
if !self.visible {
|
||||
return false;
|
||||
@@ -351,6 +360,7 @@ pub enum GroupSurface {
|
||||
}
|
||||
|
||||
impl GroupSurface {
|
||||
#[must_use]
|
||||
pub fn key(self) -> &'static str {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
@@ -519,6 +529,7 @@ impl Default for Panel {
|
||||
}
|
||||
|
||||
impl Panel {
|
||||
#[must_use]
|
||||
pub fn preset(&self) -> Option<PanelStyle> {
|
||||
Some(self.style)
|
||||
}
|
||||
@@ -527,6 +538,7 @@ impl Panel {
|
||||
self.style = style;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn surface(&self) -> PanelSurface {
|
||||
if matches!(self.style, PanelStyle::Flat | PanelStyle::Frame) {
|
||||
PanelSurface::Solid
|
||||
@@ -535,6 +547,7 @@ impl Panel {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn group_surface(&self) -> GroupSurface {
|
||||
if self.surface() == PanelSurface::Solid {
|
||||
GroupSurface::None
|
||||
@@ -543,6 +556,7 @@ impl Panel {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn zone_spacing(&self, zone: &Zone) -> f32 {
|
||||
if self.style.connected() {
|
||||
0.
|
||||
@@ -551,6 +565,7 @@ impl Panel {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn shape_parameters(&self) -> crate::scene::Parameters {
|
||||
crate::scene::Parameters {
|
||||
style: self.style,
|
||||
@@ -567,6 +582,7 @@ impl Panel {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn resolved_geometry(&self) -> PanelGeometry {
|
||||
let mut geometry = self.geometry.resolve(self.style);
|
||||
if self.style == PanelStyle::Morph && self.morph.attached {
|
||||
@@ -575,6 +591,7 @@ impl Panel {
|
||||
geometry
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn exclusive_zone(&self, hidden: bool) -> i32 {
|
||||
if hidden {
|
||||
0
|
||||
@@ -584,6 +601,7 @@ impl Panel {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn group_count(&self) -> usize {
|
||||
[&self.start, &self.center, &self.end]
|
||||
.into_iter()
|
||||
@@ -592,6 +610,7 @@ impl Panel {
|
||||
}
|
||||
|
||||
/// Appearance changes do not replace item ownership or measured allocation.
|
||||
#[must_use]
|
||||
pub fn same_composition(&self, other: &Self) -> bool {
|
||||
let same_zone = |left: &Zone, right: &Zone| {
|
||||
left.spacing == right.spacing
|
||||
@@ -615,12 +634,14 @@ impl Panel {
|
||||
&& same_zone(&self.end, &other.end)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn resolved_radius(&self, shell_radius: f32) -> CornerRadii {
|
||||
self.corner_radius
|
||||
.unwrap_or_else(|| CornerRadii::uniform(shell_radius))
|
||||
}
|
||||
|
||||
/// The overflow trigger is generated, never persisted as a configured item.
|
||||
#[must_use]
|
||||
pub fn overflow_group(&self) -> Group {
|
||||
Group {
|
||||
id: GroupId("_overflow".into()),
|
||||
@@ -646,6 +667,7 @@ impl Panel {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn item(&self, id: &ItemId) -> Option<&Item> {
|
||||
[&self.start, &self.center, &self.end]
|
||||
.into_iter()
|
||||
@@ -655,6 +677,7 @@ impl Panel {
|
||||
}
|
||||
|
||||
/// Build a stable initial composition; presets are an editor convenience.
|
||||
#[must_use]
|
||||
pub fn from_preset(preset: PanelStyle) -> Self {
|
||||
use ItemKind::*;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ pub struct GeometryIntent {
|
||||
}
|
||||
|
||||
impl GeometryIntent {
|
||||
#[must_use]
|
||||
pub fn resolve(self, style: crate::PanelStyle) -> PanelGeometry {
|
||||
use crate::PanelStyle::*;
|
||||
let flush = matches!(style, Notch | Flat | Frame | CornerWrap);
|
||||
|
||||
@@ -92,12 +92,14 @@ impl Resolution {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
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.
|
||||
#[must_use]
|
||||
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()
|
||||
@@ -106,6 +108,7 @@ impl Resolution {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn resolve(
|
||||
panel: &Panel,
|
||||
measurements: &[Measurement],
|
||||
@@ -116,6 +119,7 @@ pub fn resolve(
|
||||
}
|
||||
|
||||
/// Reserve separation between zones while keeping the center on the panel midpoint.
|
||||
#[must_use]
|
||||
pub fn resolve_with_gap(
|
||||
panel: &Panel,
|
||||
measurements: &[Measurement],
|
||||
|
||||
@@ -5,6 +5,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
pub struct CornerRadii(pub [f32; 4]);
|
||||
|
||||
impl CornerRadii {
|
||||
#[must_use]
|
||||
pub fn uniform(radius: f32) -> Self {
|
||||
Self([radius; 4])
|
||||
}
|
||||
@@ -18,6 +19,7 @@ impl CornerRadii {
|
||||
}
|
||||
|
||||
/// A panel touching the top edge must cover its top corners.
|
||||
#[must_use]
|
||||
pub fn at_top_edge(mut self, touching: bool) -> Self {
|
||||
if touching {
|
||||
self.0[0] = 0.;
|
||||
@@ -26,6 +28,7 @@ impl CornerRadii {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn at_edge(mut self, edge: super::Edge, touching: bool) -> Self {
|
||||
if touching {
|
||||
match edge {
|
||||
@@ -44,6 +47,7 @@ impl CornerRadii {
|
||||
}
|
||||
|
||||
/// Keep every corner within half the shorter surface dimension.
|
||||
#[must_use]
|
||||
pub fn clamped(self, width: f32, height: f32) -> Self {
|
||||
let limit = width.min(height).max(0.) / 2.;
|
||||
Self(self.0.map(|value| value.min(limit)))
|
||||
@@ -52,6 +56,7 @@ impl CornerRadii {
|
||||
/// Scale all corners together when their sums exceed an adjacent edge.
|
||||
/// Unlike the symmetric round-box clamp, this permits a large free-side
|
||||
/// corner when the corner on the anchored side is square.
|
||||
#[must_use]
|
||||
pub fn normalized(self, width: f32, height: f32) -> Self {
|
||||
let [tl, tr, br, bl] = self.0;
|
||||
let mut factor = 1.0_f64;
|
||||
|
||||
@@ -16,6 +16,7 @@ pub struct Primitive {
|
||||
}
|
||||
|
||||
impl Primitive {
|
||||
#[must_use]
|
||||
pub fn packet(self) -> [f32; 10] {
|
||||
let Rect {
|
||||
x,
|
||||
@@ -66,6 +67,7 @@ impl Primitive {
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn distance(self, point: [f64; 2]) -> f64 {
|
||||
let rect = rectangle_distance(point, self.bounds);
|
||||
if self.operation >= 2 {
|
||||
@@ -103,6 +105,7 @@ fn rectangle_distance(point: [f64; 2], b: Rect) -> f64 {
|
||||
x.max(0.).hypot(y.max(0.)) + x.max(y).min(0.)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn smooth_min(a: f64, b: f64, strength: f64) -> f64 {
|
||||
if strength <= 0. {
|
||||
return a.min(b);
|
||||
@@ -141,16 +144,19 @@ impl Scene {
|
||||
zones.into_iter().fold(f64::INFINITY, f64::min).max(-cutout)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn contains(&self, point: [f64; 2]) -> bool {
|
||||
self.input.iter().any(|shape| shape.contains(point))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn packets(&self) -> Vec<[f32; 10]> {
|
||||
self.paint.iter().map(|p| p.packet()).collect()
|
||||
}
|
||||
|
||||
/// Horizontal runs at physical-pixel centers. Corners and animated gaps
|
||||
/// stay click-through; adjacent equal runs are coalesced into one rectangle.
|
||||
#[must_use]
|
||||
pub fn input_rectangles(&self, scale: f64) -> Vec<[i32; 4]> {
|
||||
if !scale.is_finite() || scale <= 0. {
|
||||
return Vec::new();
|
||||
@@ -246,7 +252,7 @@ fn add(
|
||||
input: bool,
|
||||
zone: u8,
|
||||
) -> Result<(), &'static str> {
|
||||
let rounded = RoundedSurface::new(rect, radii, grid)?;
|
||||
let rounded = RoundedSurface::try_new(rect, radii, grid)?;
|
||||
if rounded.bounds().width == 0. || rounded.bounds().height == 0. {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -320,7 +326,7 @@ pub fn derive(
|
||||
operation: 1,
|
||||
blend_zone: 0,
|
||||
});
|
||||
scene.input.push(RoundedSurface::new(
|
||||
scene.input.push(RoundedSurface::try_new(
|
||||
Rect::new(bounds.x, bounds.y, bounds.width, top),
|
||||
CornerRadii::uniform(0.),
|
||||
grid,
|
||||
@@ -583,8 +589,11 @@ 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::try_new(
|
||||
mirror(input.bounds()),
|
||||
CornerRadii([bl, br, tr, tl]),
|
||||
grid,
|
||||
)?;
|
||||
}
|
||||
for damage in &mut scene.damage {
|
||||
*damage = mirror(*damage);
|
||||
@@ -624,7 +633,7 @@ mod tests {
|
||||
},
|
||||
Rect::new(0., 0., 800., 36.),
|
||||
&groups(),
|
||||
PixelGrid::new(1.25).unwrap(),
|
||||
PixelGrid::try_new(1.25).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
@@ -677,7 +686,7 @@ mod tests {
|
||||
},
|
||||
Rect::new(0., 0., 800., 600.),
|
||||
&[],
|
||||
PixelGrid::new(1.25).unwrap(),
|
||||
PixelGrid::try_new(1.25).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(scene.contains([18., 200.]));
|
||||
@@ -714,7 +723,7 @@ mod tests {
|
||||
},
|
||||
Rect::new(0., 0., 1600., 1000.),
|
||||
&[],
|
||||
PixelGrid::new(1.5).unwrap(),
|
||||
PixelGrid::try_new(1.5).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(scene.distance([800., 20.]) < 0. && scene.distance([800., 500.]) > 0.);
|
||||
@@ -746,7 +755,7 @@ mod tests {
|
||||
fn attached_styles_mirror_the_whole_outline_at_the_bottom() {
|
||||
for style in [Style::Notch, Style::Frame, Style::CornerWrap] {
|
||||
let bounds = Rect::new(0., 0., 800., if style == Style::Notch { 36. } else { 600. });
|
||||
let grid = PixelGrid::new(1.).unwrap();
|
||||
let grid = PixelGrid::try_new(1.).unwrap();
|
||||
let top = derive(
|
||||
Parameters {
|
||||
style,
|
||||
|
||||
@@ -43,6 +43,7 @@ impl Style {
|
||||
Self::WindowHug,
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub const fn key(self) -> &'static str {
|
||||
match self {
|
||||
Self::Floating => "floating",
|
||||
@@ -57,6 +58,7 @@ impl Style {
|
||||
Self::WindowHug => "window-hug",
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Floating => "Floating",
|
||||
@@ -71,9 +73,11 @@ impl Style {
|
||||
Self::WindowHug => "Window hug",
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn connected(self) -> bool {
|
||||
matches!(self, Self::Capsules | Self::Notch)
|
||||
}
|
||||
#[must_use]
|
||||
pub const fn detached(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
@@ -100,6 +104,7 @@ pub enum ZonePosition {
|
||||
pub struct AnchoredRadii(pub [f32; 4]);
|
||||
|
||||
impl AnchoredRadii {
|
||||
#[must_use]
|
||||
pub fn screen_space(self, edge: Edge) -> CornerRadii {
|
||||
let [flat_left, flat_right, free_right, free_left] = self.0;
|
||||
CornerRadii(match edge {
|
||||
@@ -173,13 +178,14 @@ pub struct PixelGrid {
|
||||
}
|
||||
|
||||
impl PixelGrid {
|
||||
pub fn new(scale: f64) -> Result<Self, &'static str> {
|
||||
pub fn try_new(scale: f64) -> Result<Self, &'static str> {
|
||||
if !scale.is_finite() || scale <= 0. {
|
||||
return Err("output scale must be finite and positive");
|
||||
}
|
||||
Ok(Self { scale })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn snap(self, position: f64) -> f64 {
|
||||
(position * self.scale).round() / self.scale
|
||||
}
|
||||
@@ -218,7 +224,11 @@ pub struct RoundedSurface {
|
||||
}
|
||||
|
||||
impl RoundedSurface {
|
||||
pub fn new(bounds: Rect, radii: CornerRadii, grid: PixelGrid) -> Result<Self, &'static str> {
|
||||
pub fn try_new(
|
||||
bounds: Rect,
|
||||
radii: CornerRadii,
|
||||
grid: PixelGrid,
|
||||
) -> Result<Self, &'static str> {
|
||||
if radii.validate().is_err() {
|
||||
return Err("surface radii must be finite and nonnegative");
|
||||
}
|
||||
@@ -230,16 +240,19 @@ impl RoundedSurface {
|
||||
Ok(Self { bounds, radii })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn bounds(self) -> Rect {
|
||||
self.bounds
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn radii(self) -> CornerRadii {
|
||||
self.radii
|
||||
}
|
||||
|
||||
/// Right and bottom boundaries are excluded so a shared seam belongs to
|
||||
/// exactly one surface, rather than both neighbors.
|
||||
#[must_use]
|
||||
pub fn contains(self, point: [f64; 2]) -> bool {
|
||||
let [x, y] = [point[0] - self.bounds.x, point[1] - self.bounds.y];
|
||||
let (width, height) = (self.bounds.width, self.bounds.height);
|
||||
@@ -339,7 +352,7 @@ mod tests {
|
||||
#[test]
|
||||
fn shared_boundaries_snap_without_seams_at_fractional_scales() {
|
||||
for scale in [1., 1.25, 1.5, 2.] {
|
||||
let grid = PixelGrid::new(scale).unwrap();
|
||||
let grid = PixelGrid::try_new(scale).unwrap();
|
||||
for origin in [-1800.3, -0.4, 0., 23.3] {
|
||||
let left = grid.rect(Rect::new(origin, 0., 33.3, 36.)).unwrap();
|
||||
let right = grid.rect(Rect::new(origin + 33.3, 0., 42.7, 36.)).unwrap();
|
||||
@@ -353,14 +366,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn floating_gaps_and_round_corners_do_not_capture_input() {
|
||||
let grid = PixelGrid::new(1.25).unwrap();
|
||||
let left = RoundedSurface::new(
|
||||
let grid = PixelGrid::try_new(1.25).unwrap();
|
||||
let left = RoundedSurface::try_new(
|
||||
Rect::new(8., 8., 100., 36.),
|
||||
CornerRadii::uniform(18.),
|
||||
grid,
|
||||
)
|
||||
.unwrap();
|
||||
let right = RoundedSurface::new(
|
||||
let right = RoundedSurface::try_new(
|
||||
Rect::new(116., 8., 100., 36.),
|
||||
CornerRadii::uniform(18.),
|
||||
grid,
|
||||
@@ -381,15 +394,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn asymmetric_hit_testing_matches_a_vertically_mirrored_surface() {
|
||||
let grid = PixelGrid::new(1.).unwrap();
|
||||
let grid = PixelGrid::try_new(1.).unwrap();
|
||||
let bounds = Rect::new(0., 0., 100., 28.);
|
||||
let top = RoundedSurface::new(
|
||||
let top = RoundedSurface::try_new(
|
||||
bounds,
|
||||
AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Top),
|
||||
grid,
|
||||
)
|
||||
.unwrap();
|
||||
let bottom = RoundedSurface::new(
|
||||
let bottom = RoundedSurface::try_new(
|
||||
bounds,
|
||||
AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Bottom),
|
||||
grid,
|
||||
@@ -409,14 +422,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_connected_seam_has_exactly_one_input_owner() {
|
||||
let grid = PixelGrid::new(1.5).unwrap();
|
||||
let left = RoundedSurface::new(
|
||||
let grid = PixelGrid::try_new(1.5).unwrap();
|
||||
let left = RoundedSurface::try_new(
|
||||
Rect::new(0., 0., 30., 36.),
|
||||
CornerRadii([18., 0., 0., 18.]),
|
||||
grid,
|
||||
)
|
||||
.unwrap();
|
||||
let right = RoundedSurface::new(
|
||||
let right = RoundedSurface::try_new(
|
||||
Rect::new(30., 0., 40., 36.),
|
||||
CornerRadii([0., 18., 18., 0.]),
|
||||
grid,
|
||||
@@ -431,9 +444,9 @@ mod tests {
|
||||
#[test]
|
||||
fn invalid_runtime_geometry_is_rejected() {
|
||||
for scale in [0., -1., f64::NAN, f64::INFINITY] {
|
||||
assert!(PixelGrid::new(scale).is_err());
|
||||
assert!(PixelGrid::try_new(scale).is_err());
|
||||
}
|
||||
let grid = PixelGrid::new(1.).unwrap();
|
||||
let grid = PixelGrid::try_new(1.).unwrap();
|
||||
for bounds in [
|
||||
Rect::new(0., 0., -1., 28.),
|
||||
Rect::new(f64::NAN, 0., 10., 28.),
|
||||
@@ -442,7 +455,7 @@ mod tests {
|
||||
assert!(grid.rect(bounds).is_err());
|
||||
}
|
||||
assert!(
|
||||
RoundedSurface::new(Rect::new(0., 0., 10., 28.), CornerRadii::uniform(-1.), grid)
|
||||
RoundedSurface::try_new(Rect::new(0., 0., 10., 28.), CornerRadii::uniform(-1.), grid)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
|
||||
Reference in new issue
Block a user