Clarify constructor failures and borrowed API contracts

This commit is contained in:
Samuel Onoja committed 2026-10-22 01:28:03 +01:00
1 parent 23e280a6b4
commit a7fcd69897
55 files changed
+325 -95

No files matched your search

+5 -5
View File
@@ -51,7 +51,7 @@ impl Default for Engine {
), ),
rules: Arc::new(RwLock::new(Arc::new(Rules { rules: Arc::new(RwLock::new(Arc::new(Rules {
enabled: false, enabled: false,
matcher: ferese_config::activities::Matcher::new(&settings) matcher: ferese_config::activities::Matcher::try_new(&settings)
.expect("built-in rules"), .expect("built-in rules"),
}))), }))),
monitor: None, monitor: None,
@@ -691,7 +691,7 @@ mod tests {
.spawn() .spawn()
.unwrap(); .unwrap();
let (owner, fd, _) = crate::ipc::activity_owner::inspect(child.id()).unwrap(); let (owner, fd, _) = crate::ipc::activity_owner::inspect(child.id()).unwrap();
let matched = ferese_config::activities::Matcher::new(&Default::default()) let matched = ferese_config::activities::Matcher::try_new(&Default::default())
.unwrap() .unwrap()
.matches("cargo test", "/tmp", &[]) .matches("cargo test", "/tmp", &[])
.unwrap(); .unwrap();
@@ -707,7 +707,7 @@ mod tests {
let mut engine = Engine::default(); let mut engine = Engine::default();
let (owner, fd, _) = crate::ipc::activity_owner::inspect(std::process::id()).unwrap(); let (owner, fd, _) = crate::ipc::activity_owner::inspect(std::process::id()).unwrap();
let settings = ferese_config::activities::Settings::default(); let settings = ferese_config::activities::Settings::default();
let matched = ferese_config::activities::Matcher::new(&settings) let matched = ferese_config::activities::Matcher::try_new(&settings)
.unwrap() .unwrap()
.matches("cargo test", "/tmp/test", &[]) .matches("cargo test", "/tmp/test", &[])
.unwrap(); .unwrap();
@@ -859,7 +859,7 @@ mod tests {
#[test] #[test]
fn rule_is_pinned_across_atomic_reload_and_no_raw_metadata_is_published() { fn rule_is_pinned_across_atomic_reload_and_no_raw_metadata_is_published() {
let settings = ferese_config::activities::Settings::default(); let settings = ferese_config::activities::Settings::default();
let matched = ferese_config::activities::Matcher::new(&settings) let matched = ferese_config::activities::Matcher::try_new(&settings)
.unwrap() .unwrap()
.matches("cargo test --secret", "/secret/cwd", &[]) .matches("cargo test --secret", "/secret/cwd", &[])
.unwrap(); .unwrap();
@@ -883,7 +883,7 @@ mod tests {
replacement.rules[0].title = "Replacement".into(); replacement.rules[0].title = "Replacement".into();
engine.configure( engine.configure(
&replacement, &replacement,
ferese_config::activities::Matcher::new(&replacement).unwrap(), ferese_config::activities::Matcher::try_new(&replacement).unwrap(),
); );
let after = engine.store.entries().next().unwrap(); let after = engine.store.entries().next().unwrap();
assert_eq!(after.data.title, "cargo test"); assert_eq!(after.data.title, "cargo test");
+13 -17
View File
@@ -442,12 +442,11 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
if let Some(token) = backend.power_retry.take() { if let Some(token) = backend.power_retry.take() {
handle.remove(token); handle.remove(token);
} }
backend for clock in backend.presentation.values_mut() {
.presentation clock.reset_timing();
.values_mut() }
.for_each(PresentationClock::reset_timing); for device in backend.devices.values_mut() {
backend.devices.values_mut().for_each(|device| { for output in device.outputs.values_mut() {
device.outputs.values_mut().for_each(|output| {
output.gamma.suspend(); output.gamma.suspend();
cancel_output_timers(&handle, output); cancel_output_timers(&handle, output);
output.surface.reset_buffers(); output.surface.reset_buffers();
@@ -456,9 +455,9 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
output.capture_texture = None; output.capture_texture = None;
output.frame_pending = false; output.frame_pending = false;
output.power.suspended(); output.power.suspended();
}); }
device.drm.pause(); device.drm.pause();
}); }
} }
state.display_presentation.clear(); state.display_presentation.clear();
@@ -2178,30 +2177,27 @@ fn send_frame_callbacks(state: &mut Ferese, output: &Output) {
state.lock_frame_callbacks(output); state.lock_frame_callbacks(output);
return; return;
} }
state let visible_windows = state.space.elements().filter(|window| {
.space
.elements()
.filter(|window| {
state state
.windows .windows
.ids() .ids()
.get(*window) .get(*window)
.is_some_and(|id| state.window_belongs_to_output(*id, output)) .is_some_and(|id| state.window_belongs_to_output(*id, output))
}) });
.for_each(|window| { for window in visible_windows {
window.send_frame(output, state.start_time.elapsed(), None, |surface, _| { window.send_frame(output, state.start_time.elapsed(), None, |surface, _| {
eligible.get(&surface.into()).cloned() eligible.get(&surface.into()).cloned()
}); });
}); }
let layers = layer_map_for_output(output) let layers = layer_map_for_output(output)
.layers() .layers()
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
layers.iter().for_each(|layer| { for layer in &layers {
layer.send_frame(output, state.start_time.elapsed(), None, |surface, _| { layer.send_frame(output, state.start_time.elapsed(), None, |surface, _| {
eligible.get(&surface.into()).cloned() eligible.get(&surface.into()).cloned()
}); });
}); }
state.send_cursor_frame(output); state.send_cursor_frame(output);
state.space.refresh(); state.space.refresh();
state.popups.cleanup(); state.popups.cleanup();
+8 -9
View File
@@ -2,11 +2,11 @@
use std::path::PathBuf; use std::path::PathBuf;
use super::{ use super::{
DrmNode, Duration, Ferese, HashMap, HashSet, Instant, OutputMode, Path, PresentationClock, DrmNode, Duration, Ferese, HashMap, HashSet, Instant, OutputMode, Path, RegistrationToken,
RegistrationToken, Scale, Session, TimeoutAction, Timer, all_gpus, cancel_output_timers, Scale, Session, TimeoutAction, Timer, all_gpus, cancel_output_timers, drm_device_key, lid,
drm_device_key, lid, monotonic_now, open_device, output_transform, refresh_connected_info, monotonic_now, open_device, output_transform, refresh_connected_info, refresh_monitor_policy,
refresh_monitor_policy, remember_applied_configuration, remove_device, rescan_device, remember_applied_configuration, remove_device, rescan_device, sync_battery_timer, transaction,
sync_battery_timer, transaction, validate_desired_outputs, validate_desired_outputs,
}; };
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -203,10 +203,9 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
backend.topology.dirty = false; backend.topology.dirty = false;
backend.output_error = None; backend.output_error = None;
if reactivate { if reactivate {
backend for clock in backend.presentation.values_mut() {
.presentation clock.reset_timing();
.values_mut() }
.for_each(PresentationClock::reset_timing);
} }
if let Some(token) = backend.topology.retry_timer.take() { if let Some(token) = backend.topology.retry_timer.take() {
state.loop_handle.remove(token); state.loop_handle.remove(token);
+1 -1
View File
@@ -149,7 +149,7 @@ impl Config {
let binding_modes = self.binding_modes(&input_settings, &bindings)?; let binding_modes = self.binding_modes(&input_settings, &bindings)?;
Ok(crate::RuntimeConfig { Ok(crate::RuntimeConfig {
activity_settings: self.activities.clone(), activity_settings: self.activities.clone(),
activity_matcher: ferese_config::activities::Matcher::new(&self.activities) activity_matcher: ferese_config::activities::Matcher::try_new(&self.activities)
.map_err(ConfigError::Binding)?, .map_err(ConfigError::Binding)?,
lock_idle: self.lock_screen.validate()?, lock_idle: self.lock_screen.validate()?,
night_light: self.night_light.validate().map_err(ConfigError::Binding)?, night_light: self.night_light.validate().map_err(ConfigError::Binding)?,
+1 -1
View File
@@ -344,7 +344,7 @@ fn panel_shader_matches_core_outlines_and_resets_for_popovers() {
}, },
bounds, bounds,
&groups, &groups,
PixelGrid::new(scale).unwrap(), PixelGrid::try_new(scale).unwrap(),
) )
.unwrap(), .unwrap(),
); );
+3 -3
View File
@@ -896,7 +896,7 @@ mod region_geometry_tests {
}, },
ferese_layout::Rect::new((320. - width) / 2., 0., width, 44.), ferese_layout::Rect::new((320. - width) / 2., 0., width, 44.),
&[], &[],
PixelGrid::new(scale).unwrap(), PixelGrid::try_new(scale).unwrap(),
) )
.unwrap(); .unwrap();
let presentation = crate::effects::PanelPresentation { let presentation = crate::effects::PanelPresentation {
@@ -947,7 +947,7 @@ mod region_geometry_tests {
use ferese_panel::shape::{AnchoredRadii, PixelGrid}; use ferese_panel::shape::{AnchoredRadii, PixelGrid};
for scale in [1., 1.25, 1.5, 2.] { for scale in [1., 1.25, 1.5, 2.] {
for edge in [ferese_panel::Edge::Top, ferese_panel::Edge::Bottom] { for edge in [ferese_panel::Edge::Top, ferese_panel::Edge::Bottom] {
let mut hub = Hub::new(Profile::default(), 34.).unwrap(); let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
for mode in [Mode::Peek, Mode::Idle] { for mode in [Mode::Peek, Mode::Idle] {
hub.retarget(mode, [400., 300.]).unwrap(); hub.retarget(mode, [400., 300.]).unwrap();
for frame in 0..160 { for frame in 0..160 {
@@ -977,7 +977,7 @@ mod region_geometry_tests {
}, },
bounds, bounds,
&[], &[],
PixelGrid::new(scale).unwrap(), PixelGrid::try_new(scale).unwrap(),
) )
.unwrap(); .unwrap();
// Reproduce the f32 transport and logical capture bounds used by native materials. // Reproduce the f32 transport and logical capture bounds used by native materials.
+2
View File
@@ -54,6 +54,7 @@ pub enum Phase {
LongBreak, LongBreak,
} }
impl Phase { impl Phase {
#[must_use]
pub fn label(self) -> &'static str { pub fn label(self) -> &'static str {
match self { match self {
Self::Work => "Work", Self::Work => "Work",
@@ -97,6 +98,7 @@ impl Session {
deadline: Some(now.saturating_add(remaining_ms)), deadline: Some(now.saturating_add(remaining_ms)),
}) })
} }
#[must_use]
pub fn remaining(&self, now: Time) -> u64 { pub fn remaining(&self, now: Time) -> u64 {
self.deadline self.deadline
.map_or(self.remaining_ms, |deadline| deadline.saturating_sub(now)) .map_or(self.remaining_ms, |deadline| deadline.saturating_sub(now))
+7
View File
@@ -39,6 +39,7 @@ pub enum Value {
} }
impl Value { impl Value {
#[must_use]
pub fn remaining_ms(&self, now: Time) -> Option<u64> { pub fn remaining_ms(&self, now: Time) -> Option<u64> {
match self { match self {
Self::Countdown { Self::Countdown {
@@ -245,6 +246,7 @@ pub struct Activity {
} }
impl Activity { impl Activity {
#[must_use]
pub fn eligible(&self, now: Time, dnd: bool) -> bool { pub fn eligible(&self, now: Time, dnd: bool) -> bool {
now >= self.eligible_at now >= self.eligible_at
&& match self.state { && match self.state {
@@ -256,6 +258,7 @@ impl Activity {
} }
} }
#[must_use]
pub fn score(&self, now: Time, ceiling: u8) -> u16 { pub fn score(&self, now: Time, ceiling: u8) -> u16 {
u16::from(self.data.priority.min(ceiling)) u16::from(self.data.priority.min(ceiling))
+ if now < self.eligible_at.saturating_add(BOOST_MS) { + if now < self.eligible_at.saturating_add(BOOST_MS) {
@@ -280,6 +283,7 @@ pub struct Store {
} }
impl Store { impl Store {
#[must_use]
pub fn new(session: u64) -> Self { pub fn new(session: u64) -> Self {
Self { Self {
limits: Limits::default(), limits: Limits::default(),
@@ -289,6 +293,7 @@ impl Store {
} }
} }
#[must_use]
pub fn session(&self) -> u64 { pub fn session(&self) -> u64 {
self.session self.session
} }
@@ -297,6 +302,7 @@ impl Store {
self.entries.values() self.entries.values()
} }
#[must_use]
pub fn resolve(&self, owner: Owner, local: &str) -> Option<&Id> { pub fn resolve(&self, owner: Owner, local: &str) -> Option<&Id> {
self.entries self.entries
.keys() .keys()
@@ -477,6 +483,7 @@ impl Store {
before != self.entries.len() before != self.entries.len()
} }
#[must_use]
pub fn next_deadline(&self, now: Time) -> Option<Time> { pub fn next_deadline(&self, now: Time) -> Option<Time> {
self.entries self.entries
.values() .values()
+3
View File
@@ -30,6 +30,7 @@ impl VelocityTracker {
} }
} }
#[must_use]
pub fn velocity(&self, now: Duration) -> f64 { pub fn velocity(&self, now: Duration) -> f64 {
let Some(&(last, origin)) = self.samples.back() else { let Some(&(last, origin)) = self.samples.back() else {
return 0.0; return 0.0;
@@ -87,6 +88,7 @@ impl VelocityTracker {
} }
} }
#[must_use]
pub fn project(position: f64, velocity: f64, rate: f64) -> f64 { pub fn project(position: f64, velocity: f64, rate: f64) -> f64 {
if !velocity.is_finite() || !(0.0..1.0).contains(&rate) || rate == 0.0 { if !velocity.is_finite() || !(0.0..1.0).contains(&rate) || rate == 0.0 {
return position; return position;
@@ -96,6 +98,7 @@ pub fn project(position: f64, velocity: f64, rate: f64) -> f64 {
/// Nonlinear resistance, bounded by `extent`; the derivative transfers the /// Nonlinear resistance, bounded by `extent`; the derivative transfers the
/// velocity of the displayed (resisted) position into the release spring. /// velocity of the displayed (resisted) position into the release spring.
#[must_use]
pub fn rubber_band(distance: f64, extent: f64, resistance: f64) -> (f64, f64) { pub fn rubber_band(distance: f64, extent: f64, resistance: f64) -> (f64, f64) {
if !distance.is_finite() if !distance.is_finite()
|| !extent.is_finite() || !extent.is_finite()
+2
View File
@@ -24,6 +24,7 @@ impl Default for MotionSettings {
} }
impl MotionSettings { impl MotionSettings {
#[must_use]
pub fn motion_enabled(self) -> bool { pub fn motion_enabled(self) -> bool {
self.enabled && !self.reduced_motion self.enabled && !self.reduced_motion
} }
@@ -53,6 +54,7 @@ impl MotionSettings {
} }
/// Only small opacity changes should use a timed duration. /// Only small opacity changes should use a timed duration.
#[must_use]
pub fn duration(self, milliseconds: f64) -> std::time::Duration { pub fn duration(self, milliseconds: f64) -> std::time::Duration {
if !self.motion_enabled() { if !self.motion_enabled() {
return std::time::Duration::ZERO; return std::time::Duration::ZERO;
@@ -74,14 +74,17 @@ pub struct AnimatedValue {
} }
impl AnimatedValue { impl AnimatedValue {
#[must_use]
pub fn needs_update(&self, target: f64) -> bool { pub fn needs_update(&self, target: f64) -> bool {
self.target != target || self.is_animating() self.target != target || self.is_animating()
} }
#[must_use]
pub fn is_animating(&self) -> bool { pub fn is_animating(&self) -> bool {
self.current != self.target || self.velocity != 0.0 self.current != self.target || self.velocity != 0.0
} }
#[must_use]
pub fn new(value: f64) -> Self { pub fn new(value: f64) -> Self {
let value = finite_or_zero(value); let value = finite_or_zero(value);
@@ -148,6 +151,7 @@ impl AnimatedValue {
} }
impl AnimatedRect { impl AnimatedRect {
#[must_use]
pub fn new(rect: Rect) -> Self { pub fn new(rect: Rect) -> Self {
Self { Self {
current: rect, current: rect,
@@ -228,6 +232,7 @@ impl AnimatedRect {
} }
} }
#[must_use]
pub fn is_settled(&self, config: SpringConfig) -> bool { pub fn is_settled(&self, config: SpringConfig) -> bool {
let config = config.normalized(); let config = config.normalized();
rect_distance(self.current, self.target) <= config.position_tolerance rect_distance(self.current, self.target) <= config.position_tolerance
@@ -242,6 +247,7 @@ pub struct ClientSize {
} }
impl ClientSize { impl ClientSize {
#[must_use]
pub fn from_rect(rect: Rect) -> Self { pub fn from_rect(rect: Rect) -> Self {
Self { Self {
width: (rect.width.round() as i32).max(1), width: (rect.width.round() as i32).max(1),
@@ -279,16 +285,19 @@ impl ClientGeometry {
matches_target matches_target
} }
#[must_use]
pub fn waiting_for_commit(&self) -> bool { pub fn waiting_for_commit(&self) -> bool {
self.waiting_since.is_some() self.waiting_since.is_some()
} }
/// The hard bound for the current uninterrupted client-buffer wait. /// The hard bound for the current uninterrupted client-buffer wait.
#[must_use]
pub fn commit_deadline(&self) -> Option<Duration> { pub fn commit_deadline(&self) -> Option<Duration> {
self.waiting_since self.waiting_since
.map(|started| started + CLIENT_COMMIT_TIMEOUT) .map(|started| started + CLIENT_COMMIT_TIMEOUT)
} }
#[must_use]
pub fn timed_out(&self, now: Duration) -> bool { pub fn timed_out(&self, now: Duration) -> bool {
self.waiting_since self.waiting_since
.is_some_and(|started| now.saturating_sub(started) >= CLIENT_COMMIT_TIMEOUT) .is_some_and(|started| now.saturating_sub(started) >= CLIENT_COMMIT_TIMEOUT)
@@ -329,6 +338,7 @@ struct ZoomTransition {
} }
impl WindowGeometry { impl WindowGeometry {
#[must_use]
pub fn new(rect: Rect, committed_size: Option<ClientSize>) -> Self { pub fn new(rect: Rect, committed_size: Option<ClientSize>) -> Self {
let rect = normalized_rect(rect); let rect = normalized_rect(rect);
@@ -414,10 +424,12 @@ impl WindowGeometry {
self.client.request_size(size, now).then_some(size) self.client.request_size(size, now).then_some(size)
} }
#[must_use]
pub fn is_zooming(&self) -> bool { pub fn is_zooming(&self) -> bool {
self.zoom.is_some() self.zoom.is_some()
} }
#[must_use]
pub fn is_fullscreen(&self) -> bool { pub fn is_fullscreen(&self) -> bool {
self.mode == PresentationMode::Fullscreen self.mode == PresentationMode::Fullscreen
} }
@@ -486,6 +498,7 @@ impl WindowGeometry {
geometry_active geometry_active
} }
#[must_use]
pub fn inverse_visual_point(&self, x: f64, y: f64) -> Option<(f64, f64)> { pub fn inverse_visual_point(&self, x: f64, y: f64) -> Option<(f64, f64)> {
let visual = self.visual.current; let visual = self.visual.current;
let (scale_x, scale_y) = self.visual_scale()?; let (scale_x, scale_y) = self.visual_scale()?;
@@ -493,6 +506,7 @@ impl WindowGeometry {
Some(((x - visual.x) / scale_x, (y - visual.y) / scale_y)) Some(((x - visual.x) / scale_x, (y - visual.y) / scale_y))
} }
#[must_use]
pub fn visual_scale(&self) -> Option<(f64, f64)> { pub fn visual_scale(&self) -> Option<(f64, f64)> {
let source = self.client.committed_size?; let source = self.client.committed_size?;
let visual = self.visual.current; let visual = self.visual.current;
+4 -3
View File
@@ -77,7 +77,7 @@ pub struct Matched {
pub group: Option<String>, pub group: Option<String>,
} }
impl Matcher { impl Matcher {
pub fn new(settings: &Settings) -> Result<Self, String> { pub fn try_new(settings: &Settings) -> Result<Self, String> {
let mut rules = Vec::new(); let mut rules = Vec::new();
for rule in &settings.rules { for rule in &settings.rules {
let regex = regex::RegexBuilder::new(&rule.pattern) let regex = regex::RegexBuilder::new(&rule.pattern)
@@ -167,6 +167,7 @@ fn validate_rule(rule: &Rule, regex: &Regex) -> Result<(), String> {
Ok(()) Ok(())
} }
impl Settings { impl Settings {
#[must_use]
pub fn limits(&self) -> ferese_activities::Limits { pub fn limits(&self) -> ferese_activities::Limits {
ferese_activities::Limits { ferese_activities::Limits {
max_visible: self.max_visible, max_visible: self.max_visible,
@@ -191,7 +192,7 @@ impl Settings {
if self.rules.iter().any(|rule| !names.insert(&rule.name)) { if self.rules.iter().any(|rule| !names.insert(&rule.name)) {
return Err("duplicate activity rule".into()); return Err("duplicate activity rule".into());
} }
Matcher::new(self).map(|_| ()) Matcher::try_new(self).map(|_| ())
} }
} }
@@ -397,7 +398,7 @@ mod tests {
let settings = Settings::default(); let settings = Settings::default();
assert!(!settings.sources.shell_hook); assert!(!settings.sources.shell_hook);
assert!(!settings.sources.process_watch); assert!(!settings.sources.process_watch);
let matcher = Matcher::new(&settings).unwrap(); let matcher = Matcher::try_new(&settings).unwrap();
assert!(matcher.matches("secret command", "/tmp", &[]).is_none()); assert!(matcher.matches("secret command", "/tmp", &[]).is_none());
let matched = matcher.matches("cargo test", "/tmp", &[]).unwrap(); let matched = matcher.matches("cargo test", "/tmp", &[]).unwrap();
assert_eq!(matched.title, "cargo test"); assert_eq!(matched.title, "cargo test");
+5
View File
@@ -33,10 +33,12 @@ pub fn config_path() -> Option<PathBuf> {
.map(|p| p.join("ferese/config.kdl")) .map(|p| p.join("ferese/config.kdl"))
} }
#[must_use]
pub fn default_wallpaper() -> &'static str { pub fn default_wallpaper() -> &'static str {
default_wallpaper_for(theme::Appearance::Dark) default_wallpaper_for(theme::Appearance::Dark)
} }
#[must_use]
pub fn default_wallpaper_for(appearance: theme::Appearance) -> &'static str { pub fn default_wallpaper_for(appearance: theme::Appearance) -> &'static str {
static LIGHT: std::sync::OnceLock<String> = std::sync::OnceLock::new(); static LIGHT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static DARK: std::sync::OnceLock<String> = std::sync::OnceLock::new(); static DARK: std::sync::OnceLock<String> = std::sync::OnceLock::new();
@@ -292,11 +294,13 @@ impl Document {
Ok(Self { doc, value }) Ok(Self { doc, value })
} }
#[must_use]
pub fn value(&self) -> &Value { pub fn value(&self) -> &Value {
&self.value &self.value
} }
/// A runtime view; the original KDL remains available for comment-preserving edits. /// A runtime view; the original KDL remains available for comment-preserving edits.
#[must_use]
pub fn with_theme(&self, theme: &theme::ResolvedTheme) -> Self { pub fn with_theme(&self, theme: &theme::ResolvedTheme) -> Self {
let mut document = self.clone(); let mut document = self.clone();
document.value["theme"] = document.value["theme"] =
@@ -305,6 +309,7 @@ impl Document {
document document
} }
#[must_use]
pub fn get(&self, path: &str) -> Option<&Value> { pub fn get(&self, path: &str) -> Option<&Value> {
let mut value = &self.value; let mut value = &self.value;
+3
View File
@@ -30,6 +30,7 @@ pub const BUILTINS: [(&str, &str, &str, &str); 6] = [
), ),
]; ];
#[must_use]
pub fn family_id(preset: &str) -> &str { pub fn family_id(preset: &str) -> &str {
BUILTINS BUILTINS
.iter() .iter()
@@ -37,6 +38,7 @@ pub fn family_id(preset: &str) -> &str {
.map_or(preset, |row| row.0) .map_or(preset, |row| row.0)
} }
#[must_use]
pub fn variant(id: &str, appearance: Appearance) -> Option<&'static str> { pub fn variant(id: &str, appearance: Appearance) -> Option<&'static str> {
if let Some(row) = BUILTINS.iter().find(|row| row.0 == id) { if let Some(row) = BUILTINS.iter().find(|row| row.0 == id) {
return Some(if appearance == Appearance::Light { return Some(if appearance == Appearance::Light {
@@ -54,6 +56,7 @@ pub fn variant(id: &str, appearance: Appearance) -> Option<&'static str> {
} }
} }
#[must_use]
pub fn builtins() -> Vec<Family> { pub fn builtins() -> Vec<Family> {
BUILTINS BUILTINS
.iter() .iter()
+1
View File
@@ -58,6 +58,7 @@ impl ConfigFile {
Ok(file) Ok(file)
} }
#[must_use]
pub fn source(&self) -> &str { pub fn source(&self) -> &str {
&self.source &self.source
} }
+4
View File
@@ -208,6 +208,7 @@ impl Default for Policy {
} }
} }
#[must_use]
pub fn default_tokens() -> Tokens { pub fn default_tokens() -> Tokens {
serde_json::from_value(json!({ serde_json::from_value(json!({
"colors": {"surface_base":"#111821", "surface_raised":"#1E2530", "application_background":"#171E27", "text_primary":"#F4F7FB", "text_muted":"#8793A2", "colors": {"surface_base":"#111821", "surface_raised":"#1E2530", "application_background":"#171E27", "text_primary":"#F4F7FB", "text_muted":"#8793A2",
@@ -224,6 +225,7 @@ pub fn default_tokens() -> Tokens {
.expect("valid built-in theme") .expect("valid built-in theme")
} }
#[must_use]
pub fn default_theme() -> ResolvedTheme { pub fn default_theme() -> ResolvedTheme {
let mut tokens = default_tokens(); let mut tokens = default_tokens();
resolve_gradients(&mut tokens, None); resolve_gradients(&mut tokens, None);
@@ -527,6 +529,7 @@ pub fn resolve_with_context(
}) })
} }
#[must_use]
pub fn theme_path(directory: &Path, path: &Path) -> PathBuf { pub fn theme_path(directory: &Path, path: &Path) -> PathBuf {
let path = if let Some(tail) = path.to_str().and_then(|path| path.strip_prefix("~/")) { let path = if let Some(tail) = path.to_str().and_then(|path| path.strip_prefix("~/")) {
std::env::var_os("HOME") std::env::var_os("HOME")
@@ -690,6 +693,7 @@ pub fn preset(name: &str, appearance: Appearance) -> Result<Tokens, String> {
Ok(t) Ok(t)
} }
#[must_use]
pub fn authored_warnings(tokens: &Tokens) -> Vec<String> { pub fn authored_warnings(tokens: &Tokens) -> Vec<String> {
if let Err(error) = validate(tokens) { if let Err(error) = validate(tokens) {
return vec![error]; return vec![error];
+2
View File
@@ -50,11 +50,13 @@ pub struct FocusCycle {
} }
impl FocusCycle { impl FocusCycle {
#[must_use]
pub fn new(windows: Vec<WindowId>, current: Option<WindowId>) -> Self { pub fn new(windows: Vec<WindowId>, current: Option<WindowId>) -> Self {
let selected = current.filter(|id| windows.contains(id)); let selected = current.filter(|id| windows.contains(id));
Self { windows, selected } Self { windows, selected }
} }
#[must_use]
pub fn windows(&self) -> &[WindowId] { pub fn windows(&self) -> &[WindowId] {
&self.windows &self.windows
} }
+8
View File
@@ -16,6 +16,7 @@ pub struct OutputGeometry {
} }
impl OutputGeometry { impl OutputGeometry {
#[must_use]
pub const fn new(x: i32, y: i32, width: i32, height: i32) -> Self { pub const fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
Self { Self {
x, x,
@@ -223,6 +224,7 @@ impl OutputWorkspaceMap {
}) })
} }
#[must_use]
pub fn assigned_workspaces( pub fn assigned_workspaces(
&self, &self,
output: OutputId, output: OutputId,
@@ -233,22 +235,27 @@ impl OutputWorkspaceMap {
.flat_map(|state| state.workspaces.iter().copied()) .flat_map(|state| state.workspaces.iter().copied())
} }
#[must_use]
pub fn focused_output(&self) -> Option<OutputId> { pub fn focused_output(&self) -> Option<OutputId> {
self.focused self.focused
} }
#[must_use]
pub fn active_workspace(&self, output: OutputId) -> Option<WorkspaceId> { pub fn active_workspace(&self, output: OutputId) -> Option<WorkspaceId> {
self.outputs.get(&output).map(|state| state.active) self.outputs.get(&output).map(|state| state.active)
} }
#[must_use]
pub fn previous_workspace(&self, output: OutputId) -> Option<WorkspaceId> { pub fn previous_workspace(&self, output: OutputId) -> Option<WorkspaceId> {
self.outputs.get(&output).and_then(|state| state.previous) self.outputs.get(&output).and_then(|state| state.previous)
} }
#[must_use]
pub fn output_for_workspace(&self, workspace: WorkspaceId) -> Option<OutputId> { pub fn output_for_workspace(&self, workspace: WorkspaceId) -> Option<OutputId> {
self.assignments.get(&workspace).copied() self.assignments.get(&workspace).copied()
} }
#[must_use]
pub fn home_output(&self, workspace: WorkspaceId) -> Option<OutputId> { pub fn home_output(&self, workspace: WorkspaceId) -> Option<OutputId> {
self.homes.get(&workspace).copied() self.homes.get(&workspace).copied()
} }
@@ -295,6 +302,7 @@ impl OutputWorkspaceMap {
self.outputs.keys().copied() self.outputs.keys().copied()
} }
#[must_use]
pub fn geometry(&self, output: OutputId) -> Option<OutputGeometry> { pub fn geometry(&self, output: OutputId) -> Option<OutputGeometry> {
self.outputs.get(&output).map(|state| state.geometry) self.outputs.get(&output).map(|state| state.geometry)
} }
+16
View File
@@ -39,6 +39,7 @@ impl Default for WorkspaceLayout {
} }
impl WorkspaceLayout { impl WorkspaceLayout {
#[must_use]
pub fn new( pub fn new(
mode: LayoutMode, mode: LayoutMode,
default_column_width: ColumnWidth, default_column_width: ColumnWidth,
@@ -61,6 +62,7 @@ impl WorkspaceLayout {
self self
} }
#[must_use]
pub fn mode(&self) -> LayoutMode { pub fn mode(&self) -> LayoutMode {
match self { match self {
Self::Scrolling(_) => LayoutMode::Scrolling, Self::Scrolling(_) => LayoutMode::Scrolling,
@@ -68,6 +70,7 @@ impl WorkspaceLayout {
} }
} }
#[must_use]
pub fn viewport_x(&self) -> Option<f64> { pub fn viewport_x(&self) -> Option<f64> {
match self { match self {
Self::Scrolling(layout) => Some(layout.viewport_x()), Self::Scrolling(layout) => Some(layout.viewport_x()),
@@ -75,6 +78,7 @@ impl WorkspaceLayout {
} }
} }
#[must_use]
pub fn preferred_window(&self) -> Option<WindowId> { pub fn preferred_window(&self) -> Option<WindowId> {
match self { match self {
Self::Scrolling(layout) => layout.active_window(), Self::Scrolling(layout) => layout.active_window(),
@@ -82,6 +86,7 @@ impl WorkspaceLayout {
} }
} }
#[must_use]
pub fn contains(&self, window: WindowId) -> bool { pub fn contains(&self, window: WindowId) -> bool {
match self { match self {
Self::Scrolling(layout) => layout.contains(window), Self::Scrolling(layout) => layout.contains(window),
@@ -125,6 +130,7 @@ impl WorkspaceLayout {
} }
} }
#[must_use]
pub fn tab_group(&self, window: WindowId) -> Option<ferese_layout::TabGroup> { pub fn tab_group(&self, window: WindowId) -> Option<ferese_layout::TabGroup> {
match self { match self {
Self::Tree(tree) => tree.tab_group(window), Self::Tree(tree) => tree.tab_group(window),
@@ -132,6 +138,7 @@ impl WorkspaceLayout {
} }
} }
#[must_use]
pub fn adjacent_tab(&self, window: WindowId, previous: bool) -> Option<WindowId> { pub fn adjacent_tab(&self, window: WindowId, previous: bool) -> Option<WindowId> {
match self { match self {
Self::Tree(tree) => tree.adjacent_tab(window, previous), Self::Tree(tree) => tree.adjacent_tab(window, previous),
@@ -445,6 +452,7 @@ impl Default for WorkspaceSet {
} }
impl WorkspaceSet { impl WorkspaceSet {
#[must_use]
pub fn new( pub fn new(
default_layout_mode: LayoutMode, default_layout_mode: LayoutMode,
default_column_width: ColumnWidth, default_column_width: ColumnWidth,
@@ -562,10 +570,12 @@ impl WorkspaceSet {
} }
} }
#[must_use]
pub fn active_id(&self) -> WorkspaceId { pub fn active_id(&self) -> WorkspaceId {
self.active self.active
} }
#[must_use]
pub fn active(&self) -> &Workspace { pub fn active(&self) -> &Workspace {
self.workspaces self.workspaces
.get(&self.active) .get(&self.active)
@@ -596,6 +606,7 @@ impl WorkspaceSet {
Ok(visible_floating.or(self.active().fullscreen).or(focused)) Ok(visible_floating.or(self.active().fullscreen).or(focused))
} }
#[must_use]
pub fn workspace(&self, id: WorkspaceId) -> Option<&Workspace> { pub fn workspace(&self, id: WorkspaceId) -> Option<&Workspace> {
self.workspaces.get(&id) self.workspaces.get(&id)
} }
@@ -608,10 +619,12 @@ impl WorkspaceSet {
self.workspaces.values() self.workspaces.values()
} }
#[must_use]
pub fn workspace_for_window(&self, window: WindowId) -> Option<WorkspaceId> { pub fn workspace_for_window(&self, window: WindowId) -> Option<WorkspaceId> {
self.window_workspaces.get(&window).copied() self.window_workspaces.get(&window).copied()
} }
#[must_use]
pub fn placement(&self, window: WindowId) -> Option<WindowPlacement> { pub fn placement(&self, window: WindowId) -> Option<WindowPlacement> {
self.placements.get(&window).copied() self.placements.get(&window).copied()
} }
@@ -823,6 +836,7 @@ impl WorkspaceSet {
} }
/// Reserve workspace identities in a pure topology plan without creating them. /// Reserve workspace identities in a pure topology plan without creating them.
#[must_use]
pub fn next_workspace_id(&self) -> WorkspaceId { pub fn next_workspace_id(&self) -> WorkspaceId {
WorkspaceId(self.next_id) WorkspaceId(self.next_id)
} }
@@ -1210,10 +1224,12 @@ fn finite_or_zero(value: f64) -> f64 {
} }
impl Workspace { impl Workspace {
#[must_use]
pub fn window_count(&self) -> usize { pub fn window_count(&self) -> usize {
self.layout.window_ids().count() + self.floating.len() self.layout.window_ids().count() + self.floating.len()
} }
#[must_use]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.layout.window_ids().next().is_none() && self.floating.is_empty() self.layout.window_ids().next().is_none() && self.floating.is_empty()
} }
@@ -14,6 +14,7 @@ pub struct WorkspaceView {
} }
impl OutputWorkspaceMap { impl OutputWorkspaceMap {
#[must_use]
pub fn workspace_views(&self, workspaces: &WorkspaceSet) -> Vec<WorkspaceView> { pub fn workspace_views(&self, workspaces: &WorkspaceSet) -> Vec<WorkspaceView> {
self.workspace_views_iter(workspaces).collect() self.workspace_views_iter(workspaces).collect()
} }
@@ -47,6 +48,7 @@ impl OutputWorkspaceMap {
}) })
} }
#[must_use]
pub fn ordered_workspace_ids<'a>( pub fn ordered_workspace_ids<'a>(
&'a self, &'a self,
workspaces: &'a WorkspaceSet, workspaces: &'a WorkspaceSet,
@@ -56,6 +58,7 @@ impl OutputWorkspaceMap {
.filter(|id| workspaces.workspace(*id).is_some()) .filter(|id| workspaces.workspace(*id).is_some())
} }
#[must_use]
pub fn ordered_workspaces( pub fn ordered_workspaces(
&self, &self,
workspaces: &WorkspaceSet, workspaces: &WorkspaceSet,
@@ -66,6 +69,7 @@ impl OutputWorkspaceMap {
/// Out-of-range numeric selection resolves to the trailing spare. Looking /// Out-of-range numeric selection resolves to the trailing spare. Looking
/// up a gesture target never creates workspaces or changes history. /// up a gesture target never creates workspaces or changes history.
#[must_use]
pub fn workspace_at( pub fn workspace_at(
&self, &self,
workspaces: &WorkspaceSet, workspaces: &WorkspaceSet,
+1
View File
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
pub const VERSION: u32 = 1; pub const VERSION: u32 = 1;
#[must_use]
pub fn now_ms() -> u64 { pub fn now_ms() -> u64 {
let mut time = libc::timespec { let mut time = libc::timespec {
tv_sec: 0, tv_sec: 0,
+1
View File
@@ -63,6 +63,7 @@ impl Event {
} }
impl Snapshot { impl Snapshot {
#[must_use]
pub fn changes_since(&self, previous: &Self) -> Vec<Change> { pub fn changes_since(&self, previous: &Self) -> Vec<Change> {
let mut changes = Vec::new(); let mut changes = Vec::new();
if self.outputs != previous.outputs { if self.outputs != previous.outputs {
+1
View File
@@ -36,6 +36,7 @@ pub struct ResponseError {
} }
impl Response { impl Response {
#[must_use]
pub fn success(id: u64, result: Value) -> Self { pub fn success(id: u64, result: Value) -> Self {
Self { Self {
version: VERSION, version: VERSION,
+4
View File
@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[must_use]
pub fn now_us() -> u64 { pub fn now_us() -> u64 {
let mut time = libc::timespec { let mut time = libc::timespec {
tv_sec: 0, tv_sec: 0,
@@ -79,6 +80,7 @@ impl Default for Player {
} }
impl Player { impl Player {
#[must_use]
pub fn label(&self) -> &str { pub fn label(&self) -> &str {
if self.title.is_empty() { if self.title.is_empty() {
&self.identity &self.identity
@@ -87,6 +89,7 @@ impl Player {
} }
} }
#[must_use]
pub fn can_toggle(&self) -> bool { pub fn can_toggle(&self) -> bool {
self.can_control self.can_control
&& if self.status == Playback::Playing { && if self.status == Playback::Playing {
@@ -96,6 +99,7 @@ impl Player {
} }
} }
#[must_use]
pub fn position_at(&self, now_us: u64) -> Option<u64> { pub fn position_at(&self, now_us: u64) -> Option<u64> {
let mut position = self.position_us? as f64; let mut position = self.position_us? as f64;
if self.status == Playback::Playing && self.rate.is_finite() { if self.status == Playback::Playing && self.rate.is_finite() {
+2
View File
@@ -17,6 +17,7 @@ pub enum State {
} }
impl State { impl State {
#[must_use]
pub fn is_inactive(self) -> bool { pub fn is_inactive(self) -> bool {
matches!(self, Self::Disabled | Self::Idle | Self::Stopped) matches!(self, Self::Disabled | Self::Idle | Self::Stopped)
} }
@@ -51,6 +52,7 @@ impl Status {
serde_json::from_value(value) serde_json::from_value(value)
} }
#[must_use]
pub fn to_value(&self) -> Value { pub fn to_value(&self) -> Value {
match serde_json::to_value(self) { match serde_json::to_value(self) {
Ok(value) => value, Ok(value) => value,
+9
View File
@@ -63,6 +63,7 @@ pub struct Rect {
} }
impl Rect { impl Rect {
#[must_use]
pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self { pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
Self { Self {
x, x,
@@ -165,14 +166,17 @@ pub struct LayoutTree {
} }
impl LayoutTree { impl LayoutTree {
#[must_use]
pub fn root(&self) -> Option<NodeId> { pub fn root(&self) -> Option<NodeId> {
self.root self.root
} }
#[must_use]
pub fn node(&self, id: NodeId) -> Option<&Node> { pub fn node(&self, id: NodeId) -> Option<&Node> {
self.nodes.get(&id) self.nodes.get(&id)
} }
#[must_use]
pub fn contains(&self, window: WindowId) -> bool { pub fn contains(&self, window: WindowId) -> bool {
self.windows.contains_key(&window) self.windows.contains_key(&window)
} }
@@ -181,6 +185,7 @@ impl LayoutTree {
self.windows.keys().copied() self.windows.keys().copied()
} }
#[must_use]
pub fn visible_window_count(&self) -> usize { pub fn visible_window_count(&self) -> usize {
fn count(tree: &LayoutTree, node: NodeId) -> usize { fn count(tree: &LayoutTree, node: NodeId) -> usize {
match tree.nodes.get(&node) { match tree.nodes.get(&node) {
@@ -197,6 +202,7 @@ impl LayoutTree {
self.root.map_or(0, |root| count(self, root)) self.root.map_or(0, |root| count(self, root))
} }
#[must_use]
pub fn preferred_window(&self) -> Option<WindowId> { pub fn preferred_window(&self) -> Option<WindowId> {
let leaf = self.first_window(self.root?).ok()?; let leaf = self.first_window(self.root?).ok()?;
match self.nodes.get(&leaf)? { match self.nodes.get(&leaf)? {
@@ -424,10 +430,12 @@ impl LayoutTree {
}) })
} }
#[must_use]
pub fn tab_group(&self, window: WindowId) -> Option<TabGroup> { pub fn tab_group(&self, window: WindowId) -> Option<TabGroup> {
self.group_at(*self.parents.get(self.windows.get(&window)?)?) self.group_at(*self.parents.get(self.windows.get(&window)?)?)
} }
#[must_use]
pub fn tab_groups(&self) -> Vec<TabGroup> { pub fn tab_groups(&self) -> Vec<TabGroup> {
let mut groups: Vec<_> = self let mut groups: Vec<_> = self
.nodes .nodes
@@ -438,6 +446,7 @@ impl LayoutTree {
groups groups
} }
#[must_use]
pub fn adjacent_tab(&self, window: WindowId, previous: bool) -> Option<WindowId> { pub fn adjacent_tab(&self, window: WindowId, previous: bool) -> Option<WindowId> {
let group = self.tab_group(window)?; let group = self.tab_group(window)?;
let index = group.windows.iter().position(|id| *id == window)?; let index = group.windows.iter().position(|id| *id == window)?;
+20 -3
View File
@@ -90,13 +90,15 @@ impl Column {
if self.heights.len() != self.windows.len() { if self.heights.len() != self.windows.len() {
self.heights.resize(self.windows.len(), 1.0); self.heights.resize(self.windows.len(), 1.0);
} }
self.heights.iter_mut().for_each(|height| { for height in &mut self.heights {
if !height.is_finite() || *height <= 0.0 { if !height.is_finite() || *height <= 0.0 {
*height = 1.0; *height = 1.0;
} }
}); }
let total = self.heights.iter().sum::<f64>(); let total = self.heights.iter().sum::<f64>();
self.heights.iter_mut().for_each(|height| *height /= total); for height in &mut self.heights {
*height /= total;
}
self.active = self.active.min(self.windows.len() - 1); self.active = self.active.min(self.windows.len() - 1);
} }
} }
@@ -166,6 +168,7 @@ impl ViewportTarget {
/// Re-evaluate the same target with pending allocated widths held back. /// Re-evaluate the same target with pending allocated widths held back.
/// A start-relative request can also use the previous held target. /// A start-relative request can also use the previous held target.
#[must_use]
pub fn with_held_widths(&self, widths: &[(WindowId, f64, f64)], start: Option<f64>) -> f64 { pub fn with_held_widths(&self, widths: &[(WindowId, f64, f64)], start: Option<f64>) -> f64 {
let replacements = widths let replacements = widths
.iter() .iter()
@@ -181,6 +184,7 @@ impl ViewportTarget {
self.target(&replacements, start) self.target(&replacements, start)
} }
#[must_use]
pub fn uses_previous_target(&self) -> bool { pub fn uses_previous_target(&self) -> bool {
matches!(self.request, ViewportRequest::Slide(..)) matches!(self.request, ViewportRequest::Slide(..))
|| (matches!(self.request, ViewportRequest::Reveal(..)) || (matches!(self.request, ViewportRequest::Reveal(..))
@@ -290,6 +294,7 @@ impl ScrollingLayout {
Ok(()) Ok(())
} }
#[must_use]
pub fn tab_group(&self, window: WindowId) -> Option<crate::TabGroup> { pub fn tab_group(&self, window: WindowId) -> Option<crate::TabGroup> {
let (index, _) = self.window_location(window)?; let (index, _) = self.window_location(window)?;
let column = &self.columns[index]; let column = &self.columns[index];
@@ -301,6 +306,7 @@ impl ScrollingLayout {
}) })
} }
#[must_use]
pub fn adjacent_tab(&self, window: WindowId, previous: bool) -> Option<WindowId> { pub fn adjacent_tab(&self, window: WindowId, previous: bool) -> Option<WindowId> {
let group = self.tab_group(window)?; let group = self.tab_group(window)?;
let index = group.windows.iter().position(|id| *id == window)?; let index = group.windows.iter().position(|id| *id == window)?;
@@ -308,6 +314,7 @@ impl ScrollingLayout {
Some(group.windows[(index + step) % group.windows.len()]) Some(group.windows[(index + step) % group.windows.len()])
} }
#[must_use]
pub fn visible_window_count(&self) -> usize { pub fn visible_window_count(&self) -> usize {
self.columns self.columns
.iter() .iter()
@@ -325,6 +332,7 @@ impl ScrollingLayout {
self.default_width = normalized_width(width); self.default_width = normalized_width(width);
} }
#[must_use]
pub fn with_default_width(default_width: ColumnWidth) -> Self { pub fn with_default_width(default_width: ColumnWidth) -> Self {
Self { Self {
default_width: normalized_width(default_width), default_width: normalized_width(default_width),
@@ -337,33 +345,40 @@ impl ScrollingLayout {
self.request_focus(self.active_window().map(PendingFocusRequest::Reveal)); self.request_focus(self.active_window().map(PendingFocusRequest::Reveal));
} }
#[must_use]
pub fn default_width(&self) -> ColumnWidth { pub fn default_width(&self) -> ColumnWidth {
self.default_width self.default_width
} }
#[must_use]
pub fn columns(&self) -> &[Column] { pub fn columns(&self) -> &[Column] {
&self.columns &self.columns
} }
#[must_use]
pub fn active_column(&self) -> Option<usize> { pub fn active_column(&self) -> Option<usize> {
self.active_column self.active_column
} }
#[must_use]
pub fn active_window(&self) -> Option<WindowId> { pub fn active_window(&self) -> Option<WindowId> {
let column = self.columns.get(self.active_column?)?; let column = self.columns.get(self.active_column?)?;
column.windows.get(column.active).copied() column.windows.get(column.active).copied()
} }
#[must_use]
pub fn viewport_x(&self) -> f64 { pub fn viewport_x(&self) -> f64 {
self.viewport_x self.viewport_x
} }
#[must_use]
pub fn allocated_column_width(&self, window: WindowId) -> Option<f64> { pub fn allocated_column_width(&self, window: WindowId) -> Option<f64> {
self.allocated_widths.get(&window).copied() self.allocated_widths.get(&window).copied()
} }
/// Whether this target changes when the pending column width is held back. /// Whether this target changes when the pending column width is held back.
/// Allocated widths, rather than cell-grid/native buffer sizes, own the strip. /// Allocated widths, rather than cell-grid/native buffer sizes, own the strip.
#[must_use]
pub fn viewport_depends_on_width(&self, window: WindowId, source: f64, target: f64) -> bool { pub fn viewport_depends_on_width(&self, window: WindowId, source: f64, target: f64) -> bool {
(source - target).abs() > 0.001 (source - target).abs() > 0.001
&& self && self
@@ -373,6 +388,7 @@ impl ScrollingLayout {
.is_some_and(|held| (held - self.viewport_x).abs() > 0.001) .is_some_and(|held| (held - self.viewport_x).abs() > 0.001)
} }
#[must_use]
pub fn viewport_target(&self) -> Option<&ViewportTarget> { pub fn viewport_target(&self) -> Option<&ViewportTarget> {
self.viewport_basis.as_ref() self.viewport_basis.as_ref()
} }
@@ -400,6 +416,7 @@ impl ScrollingLayout {
self.viewport_basis = Some(next); self.viewport_basis = Some(next);
} }
#[must_use]
pub fn contains(&self, window: WindowId) -> bool { pub fn contains(&self, window: WindowId) -> bool {
self.window_location(window).is_some() self.window_location(window).is_some()
} }
+4
View File
@@ -18,6 +18,7 @@ pub enum Presentation {
} }
impl Presentation { impl Presentation {
#[must_use]
pub fn style(self) -> Option<PanelStyle> { pub fn style(self) -> Option<PanelStyle> {
match self { match self {
Self::Floating => Some(PanelStyle::Floating), Self::Floating => Some(PanelStyle::Floating),
@@ -48,6 +49,7 @@ pub struct Adaptive {
} }
impl Adaptive { impl Adaptive {
#[must_use]
pub fn new(state: WindowState) -> Self { pub fn new(state: WindowState) -> Self {
Self { Self {
current: state.into(), current: state.into(),
@@ -55,6 +57,7 @@ impl Adaptive {
} }
} }
#[must_use]
pub fn current(&self) -> Presentation { pub fn current(&self) -> Presentation {
self.current self.current
} }
@@ -87,6 +90,7 @@ impl Adaptive {
} }
} }
#[must_use]
pub fn deadline(&self) -> Option<Duration> { pub fn deadline(&self) -> Option<Duration> {
self.pending self.pending
.map(|(_, since)| since.saturating_add(HYSTERESIS)) .map(|(_, since)| since.saturating_add(HYSTERESIS))
+1
View File
@@ -9,6 +9,7 @@ pub mod shape;
pub use shape::Style as PanelStyle; pub use shape::Style as PanelStyle;
#[must_use]
pub const fn default_island_padding() -> f32 { pub const fn default_island_padding() -> f32 {
4.0 4.0
} }
+17 -10
View File
@@ -75,6 +75,7 @@ impl Profile {
} }
} }
#[must_use]
pub fn size(self, mode: Mode, collapsed_height: f64) -> [f64; 2] { pub fn size(self, mode: Mode, collapsed_height: f64) -> [f64; 2] {
match mode { match mode {
Mode::Idle => [self.idle_width, collapsed_height], Mode::Idle => [self.idle_width, collapsed_height],
@@ -114,6 +115,7 @@ pub enum Mode {
} }
impl Mode { impl Mode {
#[must_use]
pub fn expanded(self) -> bool { pub fn expanded(self) -> bool {
matches!(self, Self::Expanded(_)) matches!(self, Self::Expanded(_))
} }
@@ -145,7 +147,7 @@ pub struct Hub {
} }
impl 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()?; profile.validate()?;
if !collapsed_height.is_finite() || !(24. ..=128.).contains(&collapsed_height) { if !collapsed_height.is_finite() || !(24. ..=128.).contains(&collapsed_height) {
return Err("hub height must be 24..128"); return Err("hub height must be 24..128");
@@ -196,6 +198,7 @@ impl Hub {
self.motion.retarget(target) self.motion.retarget(target)
} }
#[must_use]
pub fn mode(&self) -> Mode { pub fn mode(&self) -> Mode {
self.mode self.mode
} }
@@ -301,10 +304,12 @@ impl Hub {
self.active() self.active()
} }
#[must_use]
pub fn active(&self) -> bool { pub fn active(&self) -> bool {
self.motion.active() || self.sample().previous_opacity > 0. || self.sample().opacity < 1. self.motion.active() || self.sample().previous_opacity > 0. || self.sample().opacity < 1.
} }
#[must_use]
pub fn sample(&self) -> Sample { pub fn sample(&self) -> Sample {
let fade = |elapsed: Duration| (elapsed.as_secs_f32() / FADE.as_secs_f32()).clamp(0., 1.); let fade = |elapsed: Duration| (elapsed.as_secs_f32() / FADE.as_secs_f32()).clamp(0., 1.);
let opacity = self.initial_opacity let opacity = self.initial_opacity
@@ -324,11 +329,13 @@ impl Hub {
} }
} }
#[must_use]
pub fn allocation_size(&self) -> [f64; 2] { pub fn allocation_size(&self) -> [f64; 2] {
let maximum = self.motion.maximum_size(); let maximum = self.motion.maximum_size();
std::array::from_fn(|i| maximum[i].min(self.available[i])) std::array::from_fn(|i| maximum[i].min(self.available[i]))
} }
#[must_use]
pub fn target_size(&self) -> [f64; 2] { pub fn target_size(&self) -> [f64; 2] {
let target = self.motion.target().bounds; let target = self.motion.target().bounds;
[target.width, target.height] [target.width, target.height]
@@ -341,7 +348,7 @@ mod tests {
#[test] #[test]
fn workspace_width_changes_preserve_content_and_motion() { 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.retarget_idle(270., [800., 600.]).unwrap();
hub.advance(Duration::from_millis(60), false); hub.advance(Duration::from_millis(60), false);
let before = hub.sample(); let before = hub.sample();
@@ -358,7 +365,7 @@ mod tests {
#[test] #[test]
fn measured_height_preserves_fade_and_motion_and_caps_long_content() { 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.]) hub.retarget(Mode::Expanded(Content::Controls), [800., 600.])
.unwrap(); .unwrap();
hub.advance(Duration::from_millis(180), false); hub.advance(Duration::from_millis(180), false);
@@ -382,7 +389,7 @@ mod tests {
#[test] #[test]
fn a_content_swap_fades_without_restarting_the_geometry() { 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.]) hub.retarget(Mode::Expanded(Content::Controls), [800., 600.])
.unwrap(); .unwrap();
hub.advance(Duration::from_secs(2), false); hub.advance(Duration::from_secs(2), false);
@@ -404,7 +411,7 @@ mod tests {
#[test] #[test]
fn content_swap_uses_a_fixed_delay_instead_of_waiting_for_spring_settlement() { 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()); assert!(!hub.active());
hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.]) hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.])
.unwrap(); .unwrap();
@@ -430,7 +437,7 @@ mod tests {
#[test] #[test]
fn an_interrupted_content_delay_never_reveals_the_cancelled_panel() { 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.]) hub.retarget(Mode::Expanded(Content::Media), [800., 600.])
.unwrap(); .unwrap();
hub.advance(Duration::from_millis(80), false); hub.advance(Duration::from_millis(80), false);
@@ -448,7 +455,7 @@ mod tests {
#[test] #[test]
fn spring_overshoots_then_settles_within_its_reserved_surface() { 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.]) hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.])
.unwrap(); .unwrap();
let allocation = hub.allocation_size(); let allocation = hub.allocation_size();
@@ -468,7 +475,7 @@ mod tests {
#[test] #[test]
fn overshoot_is_clipped_to_small_outputs_and_reduced_motion_removes_all_fades() { 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.]) hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.])
.unwrap(); .unwrap();
for _ in 0..60 { for _ in 0..60 {
@@ -486,7 +493,7 @@ mod tests {
#[test] #[test]
fn interrupted_expansion_keeps_the_presented_shape() { 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.]) hub.retarget(Mode::Expanded(Content::Media), [800., 600.])
.unwrap(); .unwrap();
hub.advance(Duration::from_millis(40), false); hub.advance(Duration::from_millis(40), false);
@@ -502,7 +509,7 @@ mod tests {
#[test] #[test]
fn narrow_outputs_and_reduced_motion_keep_the_hub_bounded() { 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.]) hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.])
.unwrap(); .unwrap();
hub.advance(Duration::ZERO, true); hub.advance(Duration::ZERO, true);
+8 -5
View File
@@ -18,6 +18,7 @@ pub struct Geometry {
const COUNT: usize = 18; const COUNT: usize = 18;
impl Geometry { impl Geometry {
#[must_use]
pub fn rounded(bounds: Rect, radius: f64) -> Self { pub fn rounded(bounds: Rect, radius: f64) -> Self {
Self { Self {
bounds, bounds,
@@ -75,7 +76,7 @@ pub struct Motion {
} }
impl 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( Self::with_spring(
geometry, geometry,
SpringConfig { SpringConfig {
@@ -132,10 +133,12 @@ impl Motion {
}) })
} }
#[must_use]
pub fn current(&self) -> Geometry { pub fn current(&self) -> Geometry {
Geometry::from_values(self.values.map(|value| value.current)) Geometry::from_values(self.values.map(|value| value.current))
} }
#[must_use]
pub fn target(&self) -> Geometry { pub fn target(&self) -> Geometry {
Geometry::from_values(self.values.map(|value| value.target)) Geometry::from_values(self.values.map(|value| value.target))
} }
@@ -180,7 +183,7 @@ mod tests {
#[test] #[test]
fn interruption_keeps_the_current_geometry_and_velocity() { 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.retarget(expanded()).unwrap();
motion.advance(Duration::from_millis(16), false); motion.advance(Duration::from_millis(16), false);
let before = motion.current(); let before = motion.current();
@@ -197,7 +200,7 @@ mod tests {
#[test] #[test]
fn dimensions_share_one_trajectory_and_stop_requesting_frames() { 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(); motion.retarget(expanded()).unwrap();
for _ in 0..12 { for _ in 0..12 {
motion.advance(Duration::from_millis(16), false); motion.advance(Duration::from_millis(16), false);
@@ -218,7 +221,7 @@ mod tests {
#[test] #[test]
fn reduced_motion_snaps_an_inflight_animation() { 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.retarget(expanded()).unwrap();
motion.advance(Duration::from_millis(16), false); motion.advance(Duration::from_millis(16), false);
assert!(!motion.advance(Duration::ZERO, true)); assert!(!motion.advance(Duration::ZERO, true));
@@ -263,7 +266,7 @@ mod tests {
#[test] #[test]
fn invalid_retarget_leaves_motion_untouched() { 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(); let mut invalid = expanded();
invalid.bounds.height = f64::NAN; invalid.bounds.height = f64::NAN;
assert!(motion.retarget(invalid).is_err()); assert!(motion.retarget(invalid).is_err());
+3 -3
View File
@@ -269,7 +269,7 @@ mod tests {
zone: ZonePosition::Center, zone: ZonePosition::Center,
bounds: Rect::new(70.3, 0., 160.4, 44.), bounds: Rect::new(70.3, 0., 160.4, 44.),
}], }],
PixelGrid::new(scale).unwrap(), PixelGrid::try_new(scale).unwrap(),
) )
.unwrap(); .unwrap();
let paths = notch_border(&scene, 0.1 / scale, edge).unwrap(); let paths = notch_border(&scene, 0.1 / scale, edge).unwrap();
@@ -325,7 +325,7 @@ mod tests {
}, },
Rect::new(30.3, 0., width, height), Rect::new(30.3, 0., width, height),
&[], &[],
PixelGrid::new(scale).unwrap(), PixelGrid::try_new(scale).unwrap(),
) )
.unwrap(); .unwrap();
let body = scene.paint[0]; let body = scene.paint[0];
@@ -420,7 +420,7 @@ mod tests {
}, },
Rect::new(0., 0., 96., 64.), Rect::new(0., 0., 96., 64.),
&groups, &groups,
PixelGrid::new(1.25).unwrap(), PixelGrid::try_new(1.25).unwrap(),
) )
.unwrap(); .unwrap();
let contours = contours(&scene, 0.1).unwrap(); let contours = contours(&scene, 0.1).unwrap();
@@ -33,6 +33,7 @@ pub enum Edge {
} }
impl Edge { impl Edge {
#[must_use]
pub fn key(self) -> &'static str { pub fn key(self) -> &'static str {
match self { match self {
Self::Top => "top", Self::Top => "top",
@@ -86,6 +87,7 @@ impl WorkspaceStyle {
Self::AppIcons, Self::AppIcons,
]; ];
#[must_use]
pub fn key(self) -> &'static str { pub fn key(self) -> &'static str {
match self { match self {
Self::Numbers => "numbers", Self::Numbers => "numbers",
@@ -96,6 +98,7 @@ impl WorkspaceStyle {
} }
} }
#[must_use]
pub fn label(self) -> &'static str { pub fn label(self) -> &'static str {
match self { match self {
Self::Numbers => "Numbers", Self::Numbers => "Numbers",
@@ -135,6 +138,7 @@ pub enum ItemKind {
impl ItemKind { impl ItemKind {
/// Smallest useful allocation before a flexible control moves into overflow. /// Smallest useful allocation before a flexible control moves into overflow.
#[must_use]
pub fn minimum_width(self) -> Option<f32> { pub fn minimum_width(self) -> Option<f32> {
match self { match self {
Self::FocusedWindow => Some(48.), Self::FocusedWindow => Some(48.),
@@ -144,6 +148,7 @@ impl ItemKind {
} }
} }
#[must_use]
pub fn label(self) -> &'static str { pub fn label(self) -> &'static str {
match self { match self {
Self::Overview => "Overview", Self::Overview => "Overview",
@@ -163,6 +168,7 @@ impl ItemKind {
Self::Overflow => "Overflow", Self::Overflow => "Overflow",
} }
} }
#[must_use]
pub fn representations(self) -> &'static [Representation] { pub fn representations(self) -> &'static [Representation] {
use Representation::*; use Representation::*;
match self { match self {
@@ -265,6 +271,7 @@ impl<'de> Deserialize<'de> for Item {
impl Item { impl Item {
/// Start at the configured preference and retain supported smaller forms. /// Start at the configured preference and retain supported smaller forms.
#[must_use]
pub fn representations(&self) -> Vec<Representation> { pub fn representations(&self) -> Vec<Representation> {
let supported = self.kind.representations(); let supported = self.kind.representations();
let preferred = self.representation.unwrap_or(supported[0]); let preferred = self.representation.unwrap_or(supported[0]);
@@ -279,6 +286,7 @@ impl Item {
alternatives alternatives
} }
} }
#[must_use]
pub fn new(id: &str, kind: ItemKind) -> Self { pub fn new(id: &str, kind: ItemKind) -> Self {
Self { Self {
id: ItemId(id.into()), id: ItemId(id.into()),
@@ -305,6 +313,7 @@ impl Item {
} }
} }
#[must_use]
pub fn available(&self, availability: Availability) -> bool { pub fn available(&self, availability: Availability) -> bool {
if !self.visible { if !self.visible {
return false; return false;
@@ -351,6 +360,7 @@ pub enum GroupSurface {
} }
impl GroupSurface { impl GroupSurface {
#[must_use]
pub fn key(self) -> &'static str { pub fn key(self) -> &'static str {
match self { match self {
Self::None => "none", Self::None => "none",
@@ -519,6 +529,7 @@ impl Default for Panel {
} }
impl Panel { impl Panel {
#[must_use]
pub fn preset(&self) -> Option<PanelStyle> { pub fn preset(&self) -> Option<PanelStyle> {
Some(self.style) Some(self.style)
} }
@@ -527,6 +538,7 @@ impl Panel {
self.style = style; self.style = style;
} }
#[must_use]
pub fn surface(&self) -> PanelSurface { pub fn surface(&self) -> PanelSurface {
if matches!(self.style, PanelStyle::Flat | PanelStyle::Frame) { if matches!(self.style, PanelStyle::Flat | PanelStyle::Frame) {
PanelSurface::Solid PanelSurface::Solid
@@ -535,6 +547,7 @@ impl Panel {
} }
} }
#[must_use]
pub fn group_surface(&self) -> GroupSurface { pub fn group_surface(&self) -> GroupSurface {
if self.surface() == PanelSurface::Solid { if self.surface() == PanelSurface::Solid {
GroupSurface::None GroupSurface::None
@@ -543,6 +556,7 @@ impl Panel {
} }
} }
#[must_use]
pub fn zone_spacing(&self, zone: &Zone) -> f32 { pub fn zone_spacing(&self, zone: &Zone) -> f32 {
if self.style.connected() { if self.style.connected() {
0. 0.
@@ -551,6 +565,7 @@ impl Panel {
} }
} }
#[must_use]
pub fn shape_parameters(&self) -> crate::scene::Parameters { pub fn shape_parameters(&self) -> crate::scene::Parameters {
crate::scene::Parameters { crate::scene::Parameters {
style: self.style, style: self.style,
@@ -567,6 +582,7 @@ impl Panel {
} }
} }
#[must_use]
pub fn resolved_geometry(&self) -> PanelGeometry { pub fn resolved_geometry(&self) -> PanelGeometry {
let mut geometry = self.geometry.resolve(self.style); let mut geometry = self.geometry.resolve(self.style);
if self.style == PanelStyle::Morph && self.morph.attached { if self.style == PanelStyle::Morph && self.morph.attached {
@@ -575,6 +591,7 @@ impl Panel {
geometry geometry
} }
#[must_use]
pub fn exclusive_zone(&self, hidden: bool) -> i32 { pub fn exclusive_zone(&self, hidden: bool) -> i32 {
if hidden { if hidden {
0 0
@@ -584,6 +601,7 @@ impl Panel {
} }
} }
#[must_use]
pub fn group_count(&self) -> usize { pub fn group_count(&self) -> usize {
[&self.start, &self.center, &self.end] [&self.start, &self.center, &self.end]
.into_iter() .into_iter()
@@ -592,6 +610,7 @@ impl Panel {
} }
/// Appearance changes do not replace item ownership or measured allocation. /// Appearance changes do not replace item ownership or measured allocation.
#[must_use]
pub fn same_composition(&self, other: &Self) -> bool { pub fn same_composition(&self, other: &Self) -> bool {
let same_zone = |left: &Zone, right: &Zone| { let same_zone = |left: &Zone, right: &Zone| {
left.spacing == right.spacing left.spacing == right.spacing
@@ -615,12 +634,14 @@ impl Panel {
&& same_zone(&self.end, &other.end) && same_zone(&self.end, &other.end)
} }
#[must_use]
pub fn resolved_radius(&self, shell_radius: f32) -> CornerRadii { pub fn resolved_radius(&self, shell_radius: f32) -> CornerRadii {
self.corner_radius self.corner_radius
.unwrap_or_else(|| CornerRadii::uniform(shell_radius)) .unwrap_or_else(|| CornerRadii::uniform(shell_radius))
} }
/// The overflow trigger is generated, never persisted as a configured item. /// The overflow trigger is generated, never persisted as a configured item.
#[must_use]
pub fn overflow_group(&self) -> Group { pub fn overflow_group(&self) -> Group {
Group { Group {
id: GroupId("_overflow".into()), id: GroupId("_overflow".into()),
@@ -646,6 +667,7 @@ impl Panel {
} }
} }
#[must_use]
pub fn item(&self, id: &ItemId) -> Option<&Item> { pub fn item(&self, id: &ItemId) -> Option<&Item> {
[&self.start, &self.center, &self.end] [&self.start, &self.center, &self.end]
.into_iter() .into_iter()
@@ -655,6 +677,7 @@ impl Panel {
} }
/// Build a stable initial composition; presets are an editor convenience. /// Build a stable initial composition; presets are an editor convenience.
#[must_use]
pub fn from_preset(preset: PanelStyle) -> Self { pub fn from_preset(preset: PanelStyle) -> Self {
use ItemKind::*; use ItemKind::*;
@@ -16,6 +16,7 @@ pub struct GeometryIntent {
} }
impl GeometryIntent { impl GeometryIntent {
#[must_use]
pub fn resolve(self, style: crate::PanelStyle) -> PanelGeometry { pub fn resolve(self, style: crate::PanelStyle) -> PanelGeometry {
use crate::PanelStyle::*; use crate::PanelStyle::*;
let flush = matches!(style, Notch | Flat | Frame | CornerWrap); let flush = matches!(style, Notch | Flat | Frame | CornerWrap);
+4
View File
@@ -92,12 +92,14 @@ impl Resolution {
} }
} }
#[must_use]
pub fn fits(&self, available: f32) -> bool { pub fn fits(&self, available: f32) -> bool {
self.required_width() <= available self.required_width() <= available
} }
/// Clamp a requested inset independently for each output. Content may still /// Clamp a requested inset independently for each output. Content may still
/// overflow on an output too small to fit it even without an inset. /// 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 { 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.) let maximum = ((output_width as f32 - 2. * padding - self.minimum_width) / 2.)
.floor() .floor()
@@ -106,6 +108,7 @@ impl Resolution {
} }
} }
#[must_use]
pub fn resolve( pub fn resolve(
panel: &Panel, panel: &Panel,
measurements: &[Measurement], measurements: &[Measurement],
@@ -116,6 +119,7 @@ pub fn resolve(
} }
/// Reserve separation between zones while keeping the center on the panel midpoint. /// Reserve separation between zones while keeping the center on the panel midpoint.
#[must_use]
pub fn resolve_with_gap( pub fn resolve_with_gap(
panel: &Panel, panel: &Panel,
measurements: &[Measurement], measurements: &[Measurement],
+5
View File
@@ -5,6 +5,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub struct CornerRadii(pub [f32; 4]); pub struct CornerRadii(pub [f32; 4]);
impl CornerRadii { impl CornerRadii {
#[must_use]
pub fn uniform(radius: f32) -> Self { pub fn uniform(radius: f32) -> Self {
Self([radius; 4]) Self([radius; 4])
} }
@@ -18,6 +19,7 @@ impl CornerRadii {
} }
/// A panel touching the top edge must cover its top corners. /// A panel touching the top edge must cover its top corners.
#[must_use]
pub fn at_top_edge(mut self, touching: bool) -> Self { pub fn at_top_edge(mut self, touching: bool) -> Self {
if touching { if touching {
self.0[0] = 0.; self.0[0] = 0.;
@@ -26,6 +28,7 @@ impl CornerRadii {
self self
} }
#[must_use]
pub fn at_edge(mut self, edge: super::Edge, touching: bool) -> Self { pub fn at_edge(mut self, edge: super::Edge, touching: bool) -> Self {
if touching { if touching {
match edge { match edge {
@@ -44,6 +47,7 @@ impl CornerRadii {
} }
/// Keep every corner within half the shorter surface dimension. /// Keep every corner within half the shorter surface dimension.
#[must_use]
pub fn clamped(self, width: f32, height: f32) -> Self { pub fn clamped(self, width: f32, height: f32) -> Self {
let limit = width.min(height).max(0.) / 2.; let limit = width.min(height).max(0.) / 2.;
Self(self.0.map(|value| value.min(limit))) 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. /// Scale all corners together when their sums exceed an adjacent edge.
/// Unlike the symmetric round-box clamp, this permits a large free-side /// Unlike the symmetric round-box clamp, this permits a large free-side
/// corner when the corner on the anchored side is square. /// corner when the corner on the anchored side is square.
#[must_use]
pub fn normalized(self, width: f32, height: f32) -> Self { pub fn normalized(self, width: f32, height: f32) -> Self {
let [tl, tr, br, bl] = self.0; let [tl, tr, br, bl] = self.0;
let mut factor = 1.0_f64; let mut factor = 1.0_f64;
+17 -8
View File
@@ -16,6 +16,7 @@ pub struct Primitive {
} }
impl Primitive { impl Primitive {
#[must_use]
pub fn packet(self) -> [f32; 10] { pub fn packet(self) -> [f32; 10] {
let Rect { let Rect {
x, x,
@@ -66,6 +67,7 @@ impl Primitive {
}) })
} }
#[must_use]
pub fn distance(self, point: [f64; 2]) -> f64 { pub fn distance(self, point: [f64; 2]) -> f64 {
let rect = rectangle_distance(point, self.bounds); let rect = rectangle_distance(point, self.bounds);
if self.operation >= 2 { 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.) 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 { pub fn smooth_min(a: f64, b: f64, strength: f64) -> f64 {
if strength <= 0. { if strength <= 0. {
return a.min(b); return a.min(b);
@@ -141,16 +144,19 @@ impl Scene {
zones.into_iter().fold(f64::INFINITY, f64::min).max(-cutout) zones.into_iter().fold(f64::INFINITY, f64::min).max(-cutout)
} }
#[must_use]
pub fn contains(&self, point: [f64; 2]) -> bool { pub fn contains(&self, point: [f64; 2]) -> bool {
self.input.iter().any(|shape| shape.contains(point)) self.input.iter().any(|shape| shape.contains(point))
} }
#[must_use]
pub fn packets(&self) -> Vec<[f32; 10]> { pub fn packets(&self) -> Vec<[f32; 10]> {
self.paint.iter().map(|p| p.packet()).collect() self.paint.iter().map(|p| p.packet()).collect()
} }
/// Horizontal runs at physical-pixel centers. Corners and animated gaps /// Horizontal runs at physical-pixel centers. Corners and animated gaps
/// stay click-through; adjacent equal runs are coalesced into one rectangle. /// stay click-through; adjacent equal runs are coalesced into one rectangle.
#[must_use]
pub fn input_rectangles(&self, scale: f64) -> Vec<[i32; 4]> { pub fn input_rectangles(&self, scale: f64) -> Vec<[i32; 4]> {
if !scale.is_finite() || scale <= 0. { if !scale.is_finite() || scale <= 0. {
return Vec::new(); return Vec::new();
@@ -246,7 +252,7 @@ fn add(
input: bool, input: bool,
zone: u8, zone: u8,
) -> Result<(), &'static str> { ) -> 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. { if rounded.bounds().width == 0. || rounded.bounds().height == 0. {
return Ok(()); return Ok(());
} }
@@ -320,7 +326,7 @@ pub fn derive(
operation: 1, operation: 1,
blend_zone: 0, blend_zone: 0,
}); });
scene.input.push(RoundedSurface::new( scene.input.push(RoundedSurface::try_new(
Rect::new(bounds.x, bounds.y, bounds.width, top), Rect::new(bounds.x, bounds.y, bounds.width, top),
CornerRadii::uniform(0.), CornerRadii::uniform(0.),
grid, grid,
@@ -583,8 +589,11 @@ pub fn derive(
} }
for input in &mut scene.input { for input in &mut scene.input {
let [tl, tr, br, bl] = input.radii().0; let [tl, tr, br, bl] = input.radii().0;
*input = *input = RoundedSurface::try_new(
RoundedSurface::new(mirror(input.bounds()), CornerRadii([bl, br, tr, tl]), grid)?; mirror(input.bounds()),
CornerRadii([bl, br, tr, tl]),
grid,
)?;
} }
for damage in &mut scene.damage { for damage in &mut scene.damage {
*damage = mirror(*damage); *damage = mirror(*damage);
@@ -624,7 +633,7 @@ mod tests {
}, },
Rect::new(0., 0., 800., 36.), Rect::new(0., 0., 800., 36.),
&groups(), &groups(),
PixelGrid::new(1.25).unwrap(), PixelGrid::try_new(1.25).unwrap(),
) )
.unwrap() .unwrap()
} }
@@ -677,7 +686,7 @@ mod tests {
}, },
Rect::new(0., 0., 800., 600.), Rect::new(0., 0., 800., 600.),
&[], &[],
PixelGrid::new(1.25).unwrap(), PixelGrid::try_new(1.25).unwrap(),
) )
.unwrap(); .unwrap();
assert!(scene.contains([18., 200.])); assert!(scene.contains([18., 200.]));
@@ -714,7 +723,7 @@ mod tests {
}, },
Rect::new(0., 0., 1600., 1000.), Rect::new(0., 0., 1600., 1000.),
&[], &[],
PixelGrid::new(1.5).unwrap(), PixelGrid::try_new(1.5).unwrap(),
) )
.unwrap(); .unwrap();
assert!(scene.distance([800., 20.]) < 0. && scene.distance([800., 500.]) > 0.); 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() { fn attached_styles_mirror_the_whole_outline_at_the_bottom() {
for style in [Style::Notch, Style::Frame, Style::CornerWrap] { for style in [Style::Notch, Style::Frame, Style::CornerWrap] {
let bounds = Rect::new(0., 0., 800., if style == Style::Notch { 36. } else { 600. }); 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( let top = derive(
Parameters { Parameters {
style, style,
+28 -15
View File
@@ -43,6 +43,7 @@ impl Style {
Self::WindowHug, Self::WindowHug,
]; ];
#[must_use]
pub const fn key(self) -> &'static str { pub const fn key(self) -> &'static str {
match self { match self {
Self::Floating => "floating", Self::Floating => "floating",
@@ -57,6 +58,7 @@ impl Style {
Self::WindowHug => "window-hug", Self::WindowHug => "window-hug",
} }
} }
#[must_use]
pub const fn label(self) -> &'static str { pub const fn label(self) -> &'static str {
match self { match self {
Self::Floating => "Floating", Self::Floating => "Floating",
@@ -71,9 +73,11 @@ impl Style {
Self::WindowHug => "Window hug", Self::WindowHug => "Window hug",
} }
} }
#[must_use]
pub const fn connected(self) -> bool { pub const fn connected(self) -> bool {
matches!(self, Self::Capsules | Self::Notch) matches!(self, Self::Capsules | Self::Notch)
} }
#[must_use]
pub const fn detached(self) -> bool { pub const fn detached(self) -> bool {
matches!( matches!(
self, self,
@@ -100,6 +104,7 @@ pub enum ZonePosition {
pub struct AnchoredRadii(pub [f32; 4]); pub struct AnchoredRadii(pub [f32; 4]);
impl AnchoredRadii { impl AnchoredRadii {
#[must_use]
pub fn screen_space(self, edge: Edge) -> CornerRadii { pub fn screen_space(self, edge: Edge) -> CornerRadii {
let [flat_left, flat_right, free_right, free_left] = self.0; let [flat_left, flat_right, free_right, free_left] = self.0;
CornerRadii(match edge { CornerRadii(match edge {
@@ -173,13 +178,14 @@ pub struct PixelGrid {
} }
impl 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. { if !scale.is_finite() || scale <= 0. {
return Err("output scale must be finite and positive"); return Err("output scale must be finite and positive");
} }
Ok(Self { scale }) Ok(Self { scale })
} }
#[must_use]
pub fn snap(self, position: f64) -> f64 { pub fn snap(self, position: f64) -> f64 {
(position * self.scale).round() / self.scale (position * self.scale).round() / self.scale
} }
@@ -218,7 +224,11 @@ pub struct RoundedSurface {
} }
impl 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() { if radii.validate().is_err() {
return Err("surface radii must be finite and nonnegative"); return Err("surface radii must be finite and nonnegative");
} }
@@ -230,16 +240,19 @@ impl RoundedSurface {
Ok(Self { bounds, radii }) Ok(Self { bounds, radii })
} }
#[must_use]
pub fn bounds(self) -> Rect { pub fn bounds(self) -> Rect {
self.bounds self.bounds
} }
#[must_use]
pub fn radii(self) -> CornerRadii { pub fn radii(self) -> CornerRadii {
self.radii self.radii
} }
/// Right and bottom boundaries are excluded so a shared seam belongs to /// Right and bottom boundaries are excluded so a shared seam belongs to
/// exactly one surface, rather than both neighbors. /// exactly one surface, rather than both neighbors.
#[must_use]
pub fn contains(self, point: [f64; 2]) -> bool { pub fn contains(self, point: [f64; 2]) -> bool {
let [x, y] = [point[0] - self.bounds.x, point[1] - self.bounds.y]; let [x, y] = [point[0] - self.bounds.x, point[1] - self.bounds.y];
let (width, height) = (self.bounds.width, self.bounds.height); let (width, height) = (self.bounds.width, self.bounds.height);
@@ -339,7 +352,7 @@ mod tests {
#[test] #[test]
fn shared_boundaries_snap_without_seams_at_fractional_scales() { fn shared_boundaries_snap_without_seams_at_fractional_scales() {
for scale in [1., 1.25, 1.5, 2.] { 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] { for origin in [-1800.3, -0.4, 0., 23.3] {
let left = grid.rect(Rect::new(origin, 0., 33.3, 36.)).unwrap(); 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(); let right = grid.rect(Rect::new(origin + 33.3, 0., 42.7, 36.)).unwrap();
@@ -353,14 +366,14 @@ mod tests {
#[test] #[test]
fn floating_gaps_and_round_corners_do_not_capture_input() { fn floating_gaps_and_round_corners_do_not_capture_input() {
let grid = PixelGrid::new(1.25).unwrap(); let grid = PixelGrid::try_new(1.25).unwrap();
let left = RoundedSurface::new( let left = RoundedSurface::try_new(
Rect::new(8., 8., 100., 36.), Rect::new(8., 8., 100., 36.),
CornerRadii::uniform(18.), CornerRadii::uniform(18.),
grid, grid,
) )
.unwrap(); .unwrap();
let right = RoundedSurface::new( let right = RoundedSurface::try_new(
Rect::new(116., 8., 100., 36.), Rect::new(116., 8., 100., 36.),
CornerRadii::uniform(18.), CornerRadii::uniform(18.),
grid, grid,
@@ -381,15 +394,15 @@ mod tests {
#[test] #[test]
fn asymmetric_hit_testing_matches_a_vertically_mirrored_surface() { 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 bounds = Rect::new(0., 0., 100., 28.);
let top = RoundedSurface::new( let top = RoundedSurface::try_new(
bounds, bounds,
AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Top), AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Top),
grid, grid,
) )
.unwrap(); .unwrap();
let bottom = RoundedSurface::new( let bottom = RoundedSurface::try_new(
bounds, bounds,
AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Bottom), AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Bottom),
grid, grid,
@@ -409,14 +422,14 @@ mod tests {
#[test] #[test]
fn a_connected_seam_has_exactly_one_input_owner() { fn a_connected_seam_has_exactly_one_input_owner() {
let grid = PixelGrid::new(1.5).unwrap(); let grid = PixelGrid::try_new(1.5).unwrap();
let left = RoundedSurface::new( let left = RoundedSurface::try_new(
Rect::new(0., 0., 30., 36.), Rect::new(0., 0., 30., 36.),
CornerRadii([18., 0., 0., 18.]), CornerRadii([18., 0., 0., 18.]),
grid, grid,
) )
.unwrap(); .unwrap();
let right = RoundedSurface::new( let right = RoundedSurface::try_new(
Rect::new(30., 0., 40., 36.), Rect::new(30., 0., 40., 36.),
CornerRadii([0., 18., 18., 0.]), CornerRadii([0., 18., 18., 0.]),
grid, grid,
@@ -431,9 +444,9 @@ mod tests {
#[test] #[test]
fn invalid_runtime_geometry_is_rejected() { fn invalid_runtime_geometry_is_rejected() {
for scale in [0., -1., f64::NAN, f64::INFINITY] { 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 [ for bounds in [
Rect::new(0., 0., -1., 28.), Rect::new(0., 0., -1., 28.),
Rect::new(f64::NAN, 0., 10., 28.), Rect::new(f64::NAN, 0., 10., 28.),
@@ -442,7 +455,7 @@ mod tests {
assert!(grid.rect(bounds).is_err()); assert!(grid.rect(bounds).is_err());
} }
assert!( 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() .is_err()
); );
assert!( assert!(
@@ -9,6 +9,7 @@ pub fn current() -> Snapshot {
} }
/// Standalone appearance; it does not represent compositor acceptance. /// Standalone appearance; it does not represent compositor acceptance.
#[must_use]
pub fn fallback() -> Snapshot { pub fn fallback() -> Snapshot {
let theme = ferese_config::theme::default_theme(); let theme = ferese_config::theme::default_theme();
Snapshot { Snapshot {
+8
View File
@@ -17,6 +17,7 @@ pub fn rgba(s: &str) -> Result<[f64; 4], String> {
Ok(out) Ok(out)
} }
#[must_use]
pub fn hex(c: [f64; 4]) -> String { pub fn hex(c: [f64; 4]) -> String {
format!( format!(
"#{:02X}{:02X}{:02X}{:02X}", "#{:02X}{:02X}{:02X}{:02X}",
@@ -27,6 +28,7 @@ pub fn hex(c: [f64; 4]) -> String {
) )
} }
#[must_use]
pub fn luminance(c: [f64; 4]) -> f64 { pub fn luminance(c: [f64; 4]) -> f64 {
let linear = |v: f64| { let linear = |v: f64| {
if v <= 0.04045 { if v <= 0.04045 {
@@ -38,11 +40,13 @@ pub fn luminance(c: [f64; 4]) -> f64 {
0.2126 * linear(c[0]) + 0.7152 * linear(c[1]) + 0.0722 * linear(c[2]) 0.2126 * linear(c[0]) + 0.7152 * linear(c[1]) + 0.0722 * linear(c[2])
} }
#[must_use]
pub fn contrast(a: [f64; 4], b: [f64; 4]) -> f64 { pub fn contrast(a: [f64; 4], b: [f64; 4]) -> f64 {
let (a, b) = (luminance(a), luminance(b)); let (a, b) = (luminance(a), luminance(b));
(a.max(b) + 0.05) / (a.min(b) + 0.05) (a.max(b) + 0.05) / (a.min(b) + 0.05)
} }
#[must_use]
pub fn composite(fg: [f64; 4], bg: [f64; 4]) -> [f64; 4] { pub fn composite(fg: [f64; 4], bg: [f64; 4]) -> [f64; 4] {
let mut c = [0.; 4]; let mut c = [0.; 4];
for i in 0..3 { for i in 0..3 {
@@ -52,10 +56,12 @@ pub fn composite(fg: [f64; 4], bg: [f64; 4]) -> [f64; 4] {
c c
} }
#[must_use]
pub fn blend(a: [f64; 4], b: [f64; 4], p: f64) -> [f64; 4] { pub fn blend(a: [f64; 4], b: [f64; 4], p: f64) -> [f64; 4] {
std::array::from_fn(|i| a[i] + (b[i] - a[i]) * p) std::array::from_fn(|i| a[i] + (b[i] - a[i]) * p)
} }
#[must_use]
pub fn readable(preferred: [f64; 4], background: [f64; 4], minimum: f64) -> [f64; 4] { pub fn readable(preferred: [f64; 4], background: [f64; 4], minimum: f64) -> [f64; 4] {
let mut preferred = preferred; let mut preferred = preferred;
preferred[3] = 1.; preferred[3] = 1.;
@@ -78,6 +84,7 @@ pub fn readable(preferred: [f64; 4], background: [f64; 4], minimum: f64) -> [f64
target target
} }
#[must_use]
pub fn readable_across( pub fn readable_across(
mut preferred: [f64; 4], mut preferred: [f64; 4],
backgrounds: &[[f64; 4]], backgrounds: &[[f64; 4]],
@@ -109,6 +116,7 @@ pub fn readable_across(
target target
} }
#[must_use]
pub fn material_bounds(mut surface: [f64; 4], opacity: f64) -> [[f64; 4]; 2] { pub fn material_bounds(mut surface: [f64; 4], opacity: f64) -> [[f64; 4]; 2] {
surface[3] = opacity; surface[3] = opacity;
[ [
@@ -34,6 +34,7 @@ pub struct Family {
} }
impl Family { impl Family {
#[must_use]
pub fn palette(&self, appearance: Appearance) -> Option<&Palette> { pub fn palette(&self, appearance: Appearance) -> Option<&Palette> {
match appearance { match appearance {
Appearance::Light => self.light.as_ref(), Appearance::Light => self.light.as_ref(),
@@ -41,6 +42,7 @@ impl Family {
} }
} }
#[must_use]
pub fn availability(&self) -> &'static str { pub fn availability(&self) -> &'static str {
match (self.light.is_some(), self.dark.is_some()) { match (self.light.is_some(), self.dark.is_some()) {
(true, true) => "light and dark", (true, true) => "light and dark",
@@ -121,6 +121,7 @@ pub struct Paint {
} }
impl Paint { impl Paint {
#[must_use]
pub fn effective_gradient(&self) -> Option<&Gradient> { pub fn effective_gradient(&self) -> Option<&Gradient> {
if self.style == PaintStyle::Solid { if self.style == PaintStyle::Solid {
None None
@@ -153,6 +154,7 @@ pub struct ResolvedTheme {
} }
/// Accessibility takes precedence over a panel's authored opacity. /// Accessibility takes precedence over a panel's authored opacity.
#[must_use]
pub fn effective_panel_opacity( pub fn effective_panel_opacity(
inherited: f32, inherited: f32,
custom: Option<f32>, custom: Option<f32>,
@@ -166,6 +168,7 @@ pub fn effective_panel_opacity(
} }
impl ResolvedTheme { impl ResolvedTheme {
#[must_use]
pub fn panel_opacity(&self, custom: Option<f32>) -> f32 { pub fn panel_opacity(&self, custom: Option<f32>) -> f32 {
let inherited = if self.tokens.material.style == "translucent" { let inherited = if self.tokens.material.style == "translucent" {
self.tokens.material.opacity as f32 self.tokens.material.opacity as f32
+1
View File
@@ -45,6 +45,7 @@ pub(crate) fn accent_button_style(
} }
/// Filled accent buttons and selected rows share this per-state contrast policy. /// Filled accent buttons and selected rows share this per-state contrast policy.
#[must_use]
pub fn accent_button() -> theme::Button { pub fn accent_button() -> theme::Button {
theme::Button::Custom { theme::Button::Custom {
active: Box::new(|focused, theme| accent_button_style(theme, State::Active, focused)), active: Box::new(|focused, theme| accent_button_style(theme, State::Active, focused)),
+4
View File
@@ -5,6 +5,7 @@ use cosmic::widget::{column, container, row};
use cosmic::{Element, theme}; use cosmic::{Element, theme};
use jiff::Zoned; use jiff::Zoned;
#[must_use]
pub fn format_bar_time(now: &Zoned) -> String { pub fn format_bar_time(now: &Zoned) -> String {
let months = [ let months = [
"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sept", "oct", "nov", "dec", "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sept", "oct", "nov", "dec",
@@ -20,6 +21,7 @@ pub fn format_bar_time(now: &Zoned) -> String {
if hour < 12 { "am" } else { "pm" } if hour < 12 { "am" } else { "pm" }
) )
} }
#[must_use]
pub fn month(today: jiff::civil::Date, offset: i32) -> (jiff::civil::Date, usize, usize) { pub fn month(today: jiff::civil::Date, offset: i32) -> (jiff::civil::Date, usize, usize) {
let month_index = i32::from(today.year()) * 12 + i32::from(today.month()) - 1 + offset; let month_index = i32::from(today.year()) * 12 + i32::from(today.month()) - 1 + offset;
let year = month_index.div_euclid(12) as i16; let year = month_index.div_euclid(12) as i16;
@@ -30,6 +32,7 @@ pub fn month(today: jiff::civil::Date, offset: i32) -> (jiff::civil::Date, usize
(first, start, days) (first, start, days)
} }
#[must_use]
pub fn grid<'a, M: Clone + 'a>( pub fn grid<'a, M: Clone + 'a>(
today: jiff::civil::Date, today: jiff::civil::Date,
offset: i32, offset: i32,
@@ -41,6 +44,7 @@ pub fn grid<'a, M: Clone + 'a>(
grid_with_density(today, offset, palette, font, opacity, navigation, false) grid_with_density(today, offset, palette, font, opacity, navigation, false)
} }
#[must_use]
pub fn compact_grid<'a, M: Clone + 'a>( pub fn compact_grid<'a, M: Clone + 'a>(
today: jiff::civil::Date, today: jiff::civil::Date,
offset: i32, offset: i32,
+6
View File
@@ -1,5 +1,6 @@
use cosmic::iced::Color; use cosmic::iced::Color;
#[must_use]
pub fn luminance(color: Color) -> f32 { pub fn luminance(color: Color) -> f32 {
let linear = |v: f32| { let linear = |v: f32| {
if v <= 0.04045 { if v <= 0.04045 {
@@ -11,6 +12,7 @@ pub fn luminance(color: Color) -> f32 {
0.2126 * linear(color.r) + 0.7152 * linear(color.g) + 0.0722 * linear(color.b) 0.2126 * linear(color.r) + 0.7152 * linear(color.g) + 0.0722 * linear(color.b)
} }
#[must_use]
pub fn contrast(a: Color, b: Color) -> f32 { pub fn contrast(a: Color, b: Color) -> f32 {
let (a, b) = (luminance(a), luminance(b)); let (a, b) = (luminance(a), luminance(b));
(a.max(b) + 0.05) / (a.min(b) + 0.05) (a.max(b) + 0.05) / (a.min(b) + 0.05)
@@ -19,6 +21,7 @@ pub fn contrast(a: Color, b: Color) -> f32 {
/// Resolve a control fill against its surface before selecting its foreground. /// Resolve a control fill against its surface before selecting its foreground.
/// The returned fill is opaque, so wallpaper and translucent panels cannot change /// The returned fill is opaque, so wallpaper and translucent panels cannot change
/// the contrast of a small, filled control. /// the contrast of a small, filled control.
#[must_use]
pub fn composite(fill: Color, surface: Color) -> Color { pub fn composite(fill: Color, surface: Color) -> Color {
let a = fill.a.clamp(0., 1.); let a = fill.a.clamp(0., 1.);
Color::from_rgb( Color::from_rgb(
@@ -30,6 +33,7 @@ pub fn composite(fill: Color, surface: Color) -> Color {
/// Preserve the user's preferred color when it passes normal-text contrast. /// Preserve the user's preferred color when it passes normal-text contrast.
/// Otherwise black or white always provides at least 4.5:1 on an opaque sRGB fill. /// Otherwise black or white always provides at least 4.5:1 on an opaque sRGB fill.
#[must_use]
pub fn foreground(background: Color, preferred: Color) -> Color { pub fn foreground(background: Color, preferred: Color) -> Color {
let preferred = composite(preferred, background); let preferred = composite(preferred, background);
if contrast(background, preferred) >= 4.5 { if contrast(background, preferred) >= 4.5 {
@@ -44,6 +48,7 @@ pub fn foreground(background: Color, preferred: Color) -> Color {
/// Prefer the theme's foreground for filled controls by adjusting the accent's /// Prefer the theme's foreground for filled controls by adjusting the accent's
/// brightness until the text has enough contrast. /// brightness until the text has enough contrast.
#[must_use]
pub fn accent_pair(background: Color, preferred: Color) -> (Color, Color) { pub fn accent_pair(background: Color, preferred: Color) -> (Color, Color) {
let on = composite(preferred, background); let on = composite(preferred, background);
let shade = if luminance(on) > luminance(background) { let shade = if luminance(on) > luminance(background) {
@@ -67,6 +72,7 @@ pub fn accent_pair(background: Color, preferred: Color) -> (Color, Color) {
} }
/// Resolve configured accent transparency before passing an opaque color to a toolkit. /// Resolve configured accent transparency before passing an opaque color to a toolkit.
#[must_use]
pub fn accent_color(accent: Color, surface: Color) -> cosmic::cosmic_theme::palette::Srgb { pub fn accent_color(accent: Color, surface: Color) -> cosmic::cosmic_theme::palette::Srgb {
rgba(composite(accent, surface)).color rgba(composite(accent, surface)).color
} }
+13
View File
@@ -5,23 +5,28 @@ use cosmic::{theme, widget};
use crate::{Palette, mix}; use crate::{Palette, mix};
#[must_use]
pub fn surface(background: Color, radius: f32) -> theme::Container<'static> { pub fn surface(background: Color, radius: f32) -> theme::Container<'static> {
theme::Container::custom(move |_| surface_appearance(background, radius)) theme::Container::custom(move |_| surface_appearance(background, radius))
} }
#[must_use]
pub fn button_style(p: Palette, selected: bool) -> theme::Button { pub fn button_style(p: Palette, selected: bool) -> theme::Button {
button_style_with_focus(p, selected, true) button_style_with_focus(p, selected, true)
} }
#[must_use]
pub fn button_style_with_focus(p: Palette, selected: bool, focus_visible: bool) -> theme::Button { pub fn button_style_with_focus(p: Palette, selected: bool, focus_visible: bool) -> theme::Button {
styled_button(p, selected, false, 1.0, focus_visible) styled_button(p, selected, false, 1.0, focus_visible)
} }
/// Selection cards share native button states while revealing the modal material. /// Selection cards share native button states while revealing the modal material.
#[must_use]
pub fn material_button_style(p: Palette, selected: bool, material_opacity: f32) -> theme::Button { pub fn material_button_style(p: Palette, selected: bool, material_opacity: f32) -> theme::Button {
material_button_style_with_focus(p, selected, material_opacity, true) material_button_style_with_focus(p, selected, material_opacity, true)
} }
#[must_use]
pub fn material_button_style_with_focus( pub fn material_button_style_with_focus(
p: Palette, p: Palette,
selected: bool, selected: bool,
@@ -101,10 +106,12 @@ pub(crate) fn switch_colors(palette: Palette, enabled: bool, hovered: bool) -> (
(track, thumb) (track, thumb)
} }
#[must_use]
pub fn navigation_style(p: Palette, selected: bool) -> theme::Button { pub fn navigation_style(p: Palette, selected: bool) -> theme::Button {
styled_button(p, selected, true, 1.0, true) styled_button(p, selected, true, 1.0, true)
} }
#[must_use]
pub fn settings_input(p: Palette) -> theme::TextInput { pub fn settings_input(p: Palette) -> theme::TextInput {
let appearance = move |focused: bool, hovered: bool| cosmic::widget::text_input::Appearance { let appearance = move |focused: bool, hovered: bool| cosmic::widget::text_input::Appearance {
shape: Some(BorderShape::Continuous), shape: Some(BorderShape::Continuous),
@@ -229,6 +236,7 @@ fn styled_button(
/// Authentication actions use normal text while enabled and muted text while disabled. /// Authentication actions use normal text while enabled and muted text while disabled.
/// Keep the disabled fill neutral so contrast correction cannot make it look active. /// Keep the disabled fill neutral so contrast correction cannot make it look active.
#[must_use]
pub fn authentication_button(palette: Palette) -> theme::Button { pub fn authentication_button(palette: Palette) -> theme::Button {
let paint = move |enabled: bool, interaction: f32, focused: bool| { let paint = move |enabled: bool, interaction: f32, focused: bool| {
let (fill, foreground) = if enabled { let (fill, foreground) = if enabled {
@@ -260,6 +268,7 @@ pub fn authentication_button(palette: Palette) -> theme::Button {
} }
} }
#[must_use]
pub fn authentication_input(palette: Palette) -> theme::TextInput { pub fn authentication_input(palette: Palette) -> theme::TextInput {
let text = palette.text; let text = palette.text;
let muted = palette.muted; let muted = palette.muted;
@@ -293,6 +302,7 @@ pub fn authentication_input(palette: Palette) -> theme::TextInput {
} }
} }
#[must_use]
pub fn lock_input( pub fn lock_input(
radius: f32, radius: f32,
accent: cosmic::iced::Color, accent: cosmic::iced::Color,
@@ -361,6 +371,7 @@ pub fn shell_button(
} }
} }
#[must_use]
pub fn surface_appearance(background: Color, radius: f32) -> container::Style { pub fn surface_appearance(background: Color, radius: f32) -> container::Style {
container::Style { container::Style {
background: Some(Background::Color(background)), background: Some(Background::Color(background)),
@@ -373,6 +384,7 @@ pub fn surface_appearance(background: Color, radius: f32) -> container::Style {
} }
} }
#[must_use]
pub fn notification_button( pub fn notification_button(
foreground: Color, foreground: Color,
hover: Color, hover: Color,
@@ -396,6 +408,7 @@ pub fn notification_button(
} }
} }
#[must_use]
pub fn filled_button(fill: Color, foreground: Color, radius: f32, opacity: f32) -> theme::Button { pub fn filled_button(fill: Color, foreground: Color, radius: f32, opacity: f32) -> theme::Button {
let paint = move |outline| button::Style { let paint = move |outline| button::Style {
shape: Some(BorderShape::Continuous), shape: Some(BorderShape::Continuous),
+2
View File
@@ -174,6 +174,7 @@ fn desktop(p: &Palette) -> String {
) )
} }
#[must_use]
pub fn svg( pub fn svg(
family: &Family, family: &Family,
variant: Option<Appearance>, variant: Option<Appearance>,
@@ -274,6 +275,7 @@ fn svg_with_radius(
result result
} }
#[must_use]
pub fn neighbor(index: usize, count: usize, key: &keyboard::key::Named) -> Option<usize> { pub fn neighbor(index: usize, count: usize, key: &keyboard::key::Named) -> Option<usize> {
use keyboard::key::Named::*; use keyboard::key::Named::*;
if count == 0 { if count == 0 {
+1
View File
@@ -1,4 +1,5 @@
/// Radius of a circular inset. Squircle insets retain an `Outline` and its distance offset. /// Radius of a circular inset. Squircle insets retain an `Outline` and its distance offset.
#[must_use]
pub fn inner_radius(outer: f32, padding: f32) -> f32 { pub fn inner_radius(outer: f32, padding: f32) -> f32 {
(outer - padding).max(0.0) (outer - padding).max(0.0)
} }
+1
View File
@@ -186,6 +186,7 @@ pub fn accented(source: &'static [u8], size: u16, foreground: Color, accent: Col
})) }))
} }
#[must_use]
pub fn tint_svg(source: &[u8], foreground: Color, accent: Color) -> Vec<u8> { pub fn tint_svg(source: &[u8], foreground: Color, accent: Color) -> Vec<u8> {
let rgba = |foreground: Color| { let rgba = |foreground: Color| {
format!( format!(
+5
View File
@@ -37,6 +37,7 @@ pub fn heading<'a, M: 'a>(
.align_y(Alignment::Center) .align_y(Alignment::Center)
} }
#[must_use]
pub fn badge<'a, M: 'a>( pub fn badge<'a, M: 'a>(
content: Element<'a, M>, content: Element<'a, M>,
accent: Color, accent: Color,
@@ -55,6 +56,7 @@ pub fn badge<'a, M: 'a>(
.into() .into()
} }
#[must_use]
pub fn section<'a, M: 'a>( pub fn section<'a, M: 'a>(
content: Element<'a, M>, content: Element<'a, M>,
foreground: Color, foreground: Color,
@@ -84,6 +86,7 @@ pub fn section_label<'a>(
.class(cosmic::theme::Text::Color(muted)) .class(cosmic::theme::Text::Color(muted))
} }
#[must_use]
pub fn separator<'a, M: 'a>(foreground: Color, opacity: f32) -> Element<'a, M> { pub fn separator<'a, M: 'a>(foreground: Color, opacity: f32) -> Element<'a, M> {
container(cosmic::widget::Space::new().width(Length::Fill).height(1)) container(cosmic::widget::Space::new().width(Length::Fill).height(1))
.class(crate::controls::surface( .class(crate::controls::surface(
@@ -93,6 +96,7 @@ pub fn separator<'a, M: 'a>(foreground: Color, opacity: f32) -> Element<'a, M> {
.into() .into()
} }
#[must_use]
pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) -> Element<'a, M> { pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) -> Element<'a, M> {
let (track, thumb) = crate::controls::switch_colors(palette, enabled, false); let (track, thumb) = crate::controls::switch_colors(palette, enabled, false);
let track_radius = 10.; let track_radius = 10.;
@@ -123,6 +127,7 @@ pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) -
.into() .into()
} }
#[must_use]
pub fn slider(foreground: Color, opacity: f32) -> cosmic::theme::iced::Slider { pub fn slider(foreground: Color, opacity: f32) -> cosmic::theme::iced::Slider {
let style = std::rc::Rc::new(move |_: &cosmic::Theme| { let style = std::rc::Rc::new(move |_: &cosmic::Theme| {
use cosmic::iced::widget::slider::{Breakpoint, Handle, HandleShape, Rail, Style}; use cosmic::iced::widget::slider::{Breakpoint, Handle, HandleShape, Rail, Style};
+7
View File
@@ -19,6 +19,7 @@ pub struct Palette {
} }
impl Palette { impl Palette {
#[must_use]
pub fn from_resolved(theme: &ferese_theme_model::ResolvedTheme) -> Self { pub fn from_resolved(theme: &ferese_theme_model::ResolvedTheme) -> Self {
let color = |value: &str| parse_color(value).expect("validated resolved theme color"); let color = |value: &str| parse_color(value).expect("validated resolved theme color");
Self { Self {
@@ -50,6 +51,7 @@ impl Palette {
} }
} }
#[must_use]
pub fn flat(mut self) -> Self { pub fn flat(mut self) -> Self {
self.background = self.sidebar; self.background = self.sidebar;
self.card = self.sidebar; self.card = self.sidebar;
@@ -138,6 +140,7 @@ impl Palette {
cosmic::Theme::custom(std::sync::Arc::new(native)).corner_shape(BorderShape::Continuous) cosmic::Theme::custom(std::sync::Arc::new(native)).corner_shape(BorderShape::Continuous)
} }
#[must_use]
pub fn application_style(self, background: Color) -> cosmic::iced::theme::Style { pub fn application_style(self, background: Color) -> cosmic::iced::theme::Style {
cosmic::iced::theme::Style { cosmic::iced::theme::Style {
background_color: background, background_color: background,
@@ -147,6 +150,7 @@ impl Palette {
} }
} }
#[must_use]
pub fn parse_color(value: &str) -> Option<Color> { pub fn parse_color(value: &str) -> Option<Color> {
let hex = value.strip_prefix('#')?; let hex = value.strip_prefix('#')?;
let packed = u32::from_str_radix(hex, 16).ok()?; let packed = u32::from_str_radix(hex, 16).ok()?;
@@ -163,6 +167,7 @@ pub fn parse_color(value: &str) -> Option<Color> {
)) ))
} }
#[must_use]
pub fn mix(a: Color, b: Color, t: f32) -> Color { pub fn mix(a: Color, b: Color, t: f32) -> Color {
Color::from_rgb( Color::from_rgb(
a.r + (b.r - a.r) * t, a.r + (b.r - a.r) * t,
@@ -171,6 +176,7 @@ pub fn mix(a: Color, b: Color, t: f32) -> Color {
) )
} }
#[must_use]
pub fn surface_shade(base: Color) -> Color { pub fn surface_shade(base: Color) -> Color {
if crate::luminance(base) > 0.5 { if crate::luminance(base) > 0.5 {
Color::BLACK Color::BLACK
@@ -179,6 +185,7 @@ pub fn surface_shade(base: Color) -> Color {
} }
} }
#[must_use]
pub fn material_opacity(theme: &ferese_theme_model::ResolvedTheme) -> f32 { pub fn material_opacity(theme: &ferese_theme_model::ResolvedTheme) -> f32 {
if theme.tokens.material.style == "translucent" { if theme.tokens.material.style == "translucent" {
theme.tokens.material.opacity as f32 theme.tokens.material.opacity as f32
+1 -1
View File
@@ -107,7 +107,7 @@ impl<M> Surface<'_, M> {
&mut collector, &mut collector,
); );
let size = layout.bounds().size(); let size = layout.bounds().size();
let scene = PixelGrid::new(self.options.scale) let scene = PixelGrid::try_new(self.options.scale)
.and_then(|grid| { .and_then(|grid| {
ferese_panel::scene::derive( ferese_panel::scene::derive(
self.options.parameters, self.options.parameters,
+4
View File
@@ -16,6 +16,7 @@ pub struct Workspace<'a> {
pub visible_elsewhere: bool, pub visible_elsewhere: bool,
} }
#[must_use]
pub fn width(style: WorkspaceStyle) -> f32 { pub fn width(style: WorkspaceStyle) -> f32 {
match style { match style {
WorkspaceStyle::Numbers | WorkspaceStyle::Dots => 24., WorkspaceStyle::Numbers | WorkspaceStyle::Dots => 24.,
@@ -25,6 +26,7 @@ pub fn width(style: WorkspaceStyle) -> f32 {
} }
} }
#[must_use]
pub fn tooltip_label(workspace: &Workspace<'_>) -> String { pub fn tooltip_label(workspace: &Workspace<'_>) -> String {
let name = if workspace.name.is_empty() || workspace.name == workspace.index.to_string() { let name = if workspace.name.is_empty() || workspace.name == workspace.index.to_string() {
String::new() String::new()
@@ -44,6 +46,7 @@ pub fn tooltip_label(workspace: &Workspace<'_>) -> String {
) )
} }
#[must_use]
pub fn indicator<'a, M: 'a>( pub fn indicator<'a, M: 'a>(
style: WorkspaceStyle, style: WorkspaceStyle,
workspace: Workspace<'a>, workspace: Workspace<'a>,
@@ -193,6 +196,7 @@ pub fn indicator<'a, M: 'a>(
.into() .into()
} }
#[must_use]
pub fn sample<'a, M: 'a>( pub fn sample<'a, M: 'a>(
style: WorkspaceStyle, style: WorkspaceStyle,
palette: Palette, palette: Palette,
+9 -9
View File
@@ -87,15 +87,14 @@ pub enum ResultKind {
Fail, Fail,
Cancelled, Cancelled,
} }
fn handle(value: &Option<String>) -> Result<Option<Id>, Box<dyn Error>> { fn handle(value: Option<&str>) -> Result<Option<Id>, Box<dyn Error>> {
value value
.as_ref()
.map(|value| serde_json::from_str(value).map_err(Into::into)) .map(|value| serde_json::from_str(value).map_err(Into::into))
.transpose() .transpose()
} }
fn value( fn value(
progress: Option<f32>, progress: Option<f32>,
message: &Option<String>, message: Option<&str>,
remaining_ms: Option<u64>, remaining_ms: Option<u64>,
) -> Result<Option<Value>, Box<dyn Error>> { ) -> Result<Option<Value>, Box<dyn Error>> {
if usize::from(progress.is_some()) if usize::from(progress.is_some())
@@ -107,7 +106,7 @@ fn value(
} }
Ok(progress Ok(progress
.map(Value::Progress) .map(Value::Progress)
.or_else(|| message.clone().map(Value::Text)) .or_else(|| message.map(|message| Value::Text(message.to_owned())))
.or_else(|| { .or_else(|| {
remaining_ms.map(|remaining_ms| Value::Countdown { remaining_ms.map(|remaining_ms| Value::Countdown {
remaining_ms, remaining_ms,
@@ -156,7 +155,8 @@ pub fn command(action: &Action, socket: &Path, json_output: bool) -> Result<(),
priority, priority,
lease_ms, lease_ms,
} => { } => {
let value = value(*progress, message, *remaining_ms)?.unwrap_or(Value::Indeterminate); let value = value(*progress, message.as_deref(), *remaining_ms)?
.unwrap_or(Value::Indeterminate);
let data = Data { let data = Data {
source: if let Some(source) = source { source: if let Some(source) = source {
serde_json::from_value(json!(source))? serde_json::from_value(json!(source))?
@@ -196,8 +196,8 @@ pub fn command(action: &Action, socket: &Path, json_output: bool) -> Result<(),
handle: token, handle: token,
} => Publish::Update { } => Publish::Update {
local: local.clone(), local: local.clone(),
handle: handle(token)?, handle: handle(token.as_deref())?,
value: value(*progress, message, *remaining_ms)?, value: value(*progress, message.as_deref(), *remaining_ms)?,
lease_ms: *lease_ms, lease_ms: *lease_ms,
}, },
Action::End { Action::End {
@@ -207,7 +207,7 @@ pub fn command(action: &Action, socket: &Path, json_output: bool) -> Result<(),
handle: token, handle: token,
} => Publish::End { } => Publish::End {
local: local.clone(), local: local.clone(),
handle: handle(token)?, handle: handle(token.as_deref())?,
result: match result { result: match result {
ResultKind::Ok => State::Done, ResultKind::Ok => State::Done,
ResultKind::Fail => State::Failed, ResultKind::Fail => State::Failed,
@@ -220,7 +220,7 @@ pub fn command(action: &Action, socket: &Path, json_output: bool) -> Result<(),
handle: token, handle: token,
} => Publish::Cancel { } => Publish::Cancel {
local: local.clone(), local: local.clone(),
handle: handle(token)?, handle: handle(token.as_deref())?,
}, },
Action::Hook { Action::Hook {
local, local,
+2 -2
View File
@@ -93,7 +93,7 @@ pub(super) struct Runtime {
impl Runtime { impl Runtime {
pub(super) fn new(panel: &Panel) -> Self { pub(super) fn new(panel: &Panel) -> Self {
let mut hub = Hub::new(panel.morph, panel.resolved_geometry().height.into()) let mut hub = Hub::try_new(panel.morph, panel.resolved_geometry().height.into())
.expect("validated panel hub"); .expect("validated panel hub");
hub.pin_radii(panel.corner_radius.map(|r| r.0.map(f64::from))) hub.pin_radii(panel.corner_radius.map(|r| r.0.map(f64::from)))
.expect("validated panel radii"); .expect("validated panel radii");
@@ -3539,7 +3539,7 @@ mod tests {
bounds.height, bounds.height,
), ),
&[], &[],
ferese_panel::shape::PixelGrid::new(1.5).unwrap(), ferese_panel::shape::PixelGrid::try_new(1.5).unwrap(),
) )
.unwrap(); .unwrap();
assert_eq!(scene.paint.len(), 3); assert_eq!(scene.paint.len(), 3);