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
+332 -102

No files matched your search

+2
View File
@@ -54,6 +54,7 @@ pub enum Phase {
LongBreak,
}
impl Phase {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Work => "Work",
@@ -97,6 +98,7 @@ impl Session {
deadline: Some(now.saturating_add(remaining_ms)),
})
}
#[must_use]
pub fn remaining(&self, now: Time) -> u64 {
self.deadline
.map_or(self.remaining_ms, |deadline| deadline.saturating_sub(now))
+7
View File
@@ -39,6 +39,7 @@ pub enum Value {
}
impl Value {
#[must_use]
pub fn remaining_ms(&self, now: Time) -> Option<u64> {
match self {
Self::Countdown {
@@ -245,6 +246,7 @@ pub struct Activity {
}
impl Activity {
#[must_use]
pub fn eligible(&self, now: Time, dnd: bool) -> bool {
now >= self.eligible_at
&& match self.state {
@@ -256,6 +258,7 @@ impl Activity {
}
}
#[must_use]
pub fn score(&self, now: Time, ceiling: u8) -> u16 {
u16::from(self.data.priority.min(ceiling))
+ if now < self.eligible_at.saturating_add(BOOST_MS) {
@@ -280,6 +283,7 @@ pub struct Store {
}
impl Store {
#[must_use]
pub fn new(session: u64) -> Self {
Self {
limits: Limits::default(),
@@ -289,6 +293,7 @@ impl Store {
}
}
#[must_use]
pub fn session(&self) -> u64 {
self.session
}
@@ -297,6 +302,7 @@ impl Store {
self.entries.values()
}
#[must_use]
pub fn resolve(&self, owner: Owner, local: &str) -> Option<&Id> {
self.entries
.keys()
@@ -477,6 +483,7 @@ impl Store {
before != self.entries.len()
}
#[must_use]
pub fn next_deadline(&self, now: Time) -> Option<Time> {
self.entries
.values()
+3
View File
@@ -30,6 +30,7 @@ impl VelocityTracker {
}
}
#[must_use]
pub fn velocity(&self, now: Duration) -> f64 {
let Some(&(last, origin)) = self.samples.back() else {
return 0.0;
@@ -87,6 +88,7 @@ impl VelocityTracker {
}
}
#[must_use]
pub fn project(position: f64, velocity: f64, rate: f64) -> f64 {
if !velocity.is_finite() || !(0.0..1.0).contains(&rate) || rate == 0.0 {
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
/// velocity of the displayed (resisted) position into the release spring.
#[must_use]
pub fn rubber_band(distance: f64, extent: f64, resistance: f64) -> (f64, f64) {
if !distance.is_finite()
|| !extent.is_finite()
+2
View File
@@ -24,6 +24,7 @@ impl Default for MotionSettings {
}
impl MotionSettings {
#[must_use]
pub fn motion_enabled(self) -> bool {
self.enabled && !self.reduced_motion
}
@@ -53,6 +54,7 @@ impl MotionSettings {
}
/// Only small opacity changes should use a timed duration.
#[must_use]
pub fn duration(self, milliseconds: f64) -> std::time::Duration {
if !self.motion_enabled() {
return std::time::Duration::ZERO;
@@ -74,14 +74,17 @@ pub struct AnimatedValue {
}
impl AnimatedValue {
#[must_use]
pub fn needs_update(&self, target: f64) -> bool {
self.target != target || self.is_animating()
}
#[must_use]
pub fn is_animating(&self) -> bool {
self.current != self.target || self.velocity != 0.0
}
#[must_use]
pub fn new(value: f64) -> Self {
let value = finite_or_zero(value);
@@ -148,6 +151,7 @@ impl AnimatedValue {
}
impl AnimatedRect {
#[must_use]
pub fn new(rect: Rect) -> Self {
Self {
current: rect,
@@ -228,6 +232,7 @@ impl AnimatedRect {
}
}
#[must_use]
pub fn is_settled(&self, config: SpringConfig) -> bool {
let config = config.normalized();
rect_distance(self.current, self.target) <= config.position_tolerance
@@ -242,6 +247,7 @@ pub struct ClientSize {
}
impl ClientSize {
#[must_use]
pub fn from_rect(rect: Rect) -> Self {
Self {
width: (rect.width.round() as i32).max(1),
@@ -279,16 +285,19 @@ impl ClientGeometry {
matches_target
}
#[must_use]
pub fn waiting_for_commit(&self) -> bool {
self.waiting_since.is_some()
}
/// The hard bound for the current uninterrupted client-buffer wait.
#[must_use]
pub fn commit_deadline(&self) -> Option<Duration> {
self.waiting_since
.map(|started| started + CLIENT_COMMIT_TIMEOUT)
}
#[must_use]
pub fn timed_out(&self, now: Duration) -> bool {
self.waiting_since
.is_some_and(|started| now.saturating_sub(started) >= CLIENT_COMMIT_TIMEOUT)
@@ -329,6 +338,7 @@ struct ZoomTransition {
}
impl WindowGeometry {
#[must_use]
pub fn new(rect: Rect, committed_size: Option<ClientSize>) -> Self {
let rect = normalized_rect(rect);
@@ -414,10 +424,12 @@ impl WindowGeometry {
self.client.request_size(size, now).then_some(size)
}
#[must_use]
pub fn is_zooming(&self) -> bool {
self.zoom.is_some()
}
#[must_use]
pub fn is_fullscreen(&self) -> bool {
self.mode == PresentationMode::Fullscreen
}
@@ -486,6 +498,7 @@ impl WindowGeometry {
geometry_active
}
#[must_use]
pub fn inverse_visual_point(&self, x: f64, y: f64) -> Option<(f64, f64)> {
let visual = self.visual.current;
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))
}
#[must_use]
pub fn visual_scale(&self) -> Option<(f64, f64)> {
let source = self.client.committed_size?;
let visual = self.visual.current;
+4 -3
View File
@@ -77,7 +77,7 @@ pub struct Matched {
pub group: Option<String>,
}
impl Matcher {
pub fn new(settings: &Settings) -> Result<Self, String> {
pub fn try_new(settings: &Settings) -> Result<Self, String> {
let mut rules = Vec::new();
for rule in &settings.rules {
let regex = regex::RegexBuilder::new(&rule.pattern)
@@ -167,6 +167,7 @@ fn validate_rule(rule: &Rule, regex: &Regex) -> Result<(), String> {
Ok(())
}
impl Settings {
#[must_use]
pub fn limits(&self) -> ferese_activities::Limits {
ferese_activities::Limits {
max_visible: self.max_visible,
@@ -191,7 +192,7 @@ impl Settings {
if self.rules.iter().any(|rule| !names.insert(&rule.name)) {
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();
assert!(!settings.sources.shell_hook);
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());
let matched = matcher.matches("cargo test", "/tmp", &[]).unwrap();
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"))
}
#[must_use]
pub fn default_wallpaper() -> &'static str {
default_wallpaper_for(theme::Appearance::Dark)
}
#[must_use]
pub fn default_wallpaper_for(appearance: theme::Appearance) -> &'static str {
static LIGHT: 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 })
}
#[must_use]
pub fn value(&self) -> &Value {
&self.value
}
/// A runtime view; the original KDL remains available for comment-preserving edits.
#[must_use]
pub fn with_theme(&self, theme: &theme::ResolvedTheme) -> Self {
let mut document = self.clone();
document.value["theme"] =
@@ -305,6 +309,7 @@ impl Document {
document
}
#[must_use]
pub fn get(&self, path: &str) -> Option<&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 {
BUILTINS
.iter()
@@ -37,6 +38,7 @@ pub fn family_id(preset: &str) -> &str {
.map_or(preset, |row| row.0)
}
#[must_use]
pub fn variant(id: &str, appearance: Appearance) -> Option<&'static str> {
if let Some(row) = BUILTINS.iter().find(|row| row.0 == id) {
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> {
BUILTINS
.iter()
+1
View File
@@ -58,6 +58,7 @@ impl ConfigFile {
Ok(file)
}
#[must_use]
pub fn source(&self) -> &str {
&self.source
}
+4
View File
@@ -208,6 +208,7 @@ impl Default for Policy {
}
}
#[must_use]
pub fn default_tokens() -> Tokens {
serde_json::from_value(json!({
"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")
}
#[must_use]
pub fn default_theme() -> ResolvedTheme {
let mut tokens = default_tokens();
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 {
let path = if let Some(tail) = path.to_str().and_then(|path| path.strip_prefix("~/")) {
std::env::var_os("HOME")
@@ -690,6 +693,7 @@ pub fn preset(name: &str, appearance: Appearance) -> Result<Tokens, String> {
Ok(t)
}
#[must_use]
pub fn authored_warnings(tokens: &Tokens) -> Vec<String> {
if let Err(error) = validate(tokens) {
return vec![error];
+2
View File
@@ -50,11 +50,13 @@ pub struct FocusCycle {
}
impl FocusCycle {
#[must_use]
pub fn new(windows: Vec<WindowId>, current: Option<WindowId>) -> Self {
let selected = current.filter(|id| windows.contains(id));
Self { windows, selected }
}
#[must_use]
pub fn windows(&self) -> &[WindowId] {
&self.windows
}
+8
View File
@@ -16,6 +16,7 @@ pub struct OutputGeometry {
}
impl OutputGeometry {
#[must_use]
pub const fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
Self {
x,
@@ -223,6 +224,7 @@ impl OutputWorkspaceMap {
})
}
#[must_use]
pub fn assigned_workspaces(
&self,
output: OutputId,
@@ -233,22 +235,27 @@ impl OutputWorkspaceMap {
.flat_map(|state| state.workspaces.iter().copied())
}
#[must_use]
pub fn focused_output(&self) -> Option<OutputId> {
self.focused
}
#[must_use]
pub fn active_workspace(&self, output: OutputId) -> Option<WorkspaceId> {
self.outputs.get(&output).map(|state| state.active)
}
#[must_use]
pub fn previous_workspace(&self, output: OutputId) -> Option<WorkspaceId> {
self.outputs.get(&output).and_then(|state| state.previous)
}
#[must_use]
pub fn output_for_workspace(&self, workspace: WorkspaceId) -> Option<OutputId> {
self.assignments.get(&workspace).copied()
}
#[must_use]
pub fn home_output(&self, workspace: WorkspaceId) -> Option<OutputId> {
self.homes.get(&workspace).copied()
}
@@ -295,6 +302,7 @@ impl OutputWorkspaceMap {
self.outputs.keys().copied()
}
#[must_use]
pub fn geometry(&self, output: OutputId) -> Option<OutputGeometry> {
self.outputs.get(&output).map(|state| state.geometry)
}
+16
View File
@@ -39,6 +39,7 @@ impl Default for WorkspaceLayout {
}
impl WorkspaceLayout {
#[must_use]
pub fn new(
mode: LayoutMode,
default_column_width: ColumnWidth,
@@ -61,6 +62,7 @@ impl WorkspaceLayout {
self
}
#[must_use]
pub fn mode(&self) -> LayoutMode {
match self {
Self::Scrolling(_) => LayoutMode::Scrolling,
@@ -68,6 +70,7 @@ impl WorkspaceLayout {
}
}
#[must_use]
pub fn viewport_x(&self) -> Option<f64> {
match self {
Self::Scrolling(layout) => Some(layout.viewport_x()),
@@ -75,6 +78,7 @@ impl WorkspaceLayout {
}
}
#[must_use]
pub fn preferred_window(&self) -> Option<WindowId> {
match self {
Self::Scrolling(layout) => layout.active_window(),
@@ -82,6 +86,7 @@ impl WorkspaceLayout {
}
}
#[must_use]
pub fn contains(&self, window: WindowId) -> bool {
match self {
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> {
match self {
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> {
match self {
Self::Tree(tree) => tree.adjacent_tab(window, previous),
@@ -445,6 +452,7 @@ impl Default for WorkspaceSet {
}
impl WorkspaceSet {
#[must_use]
pub fn new(
default_layout_mode: LayoutMode,
default_column_width: ColumnWidth,
@@ -562,10 +570,12 @@ impl WorkspaceSet {
}
}
#[must_use]
pub fn active_id(&self) -> WorkspaceId {
self.active
}
#[must_use]
pub fn active(&self) -> &Workspace {
self.workspaces
.get(&self.active)
@@ -596,6 +606,7 @@ impl WorkspaceSet {
Ok(visible_floating.or(self.active().fullscreen).or(focused))
}
#[must_use]
pub fn workspace(&self, id: WorkspaceId) -> Option<&Workspace> {
self.workspaces.get(&id)
}
@@ -608,10 +619,12 @@ impl WorkspaceSet {
self.workspaces.values()
}
#[must_use]
pub fn workspace_for_window(&self, window: WindowId) -> Option<WorkspaceId> {
self.window_workspaces.get(&window).copied()
}
#[must_use]
pub fn placement(&self, window: WindowId) -> Option<WindowPlacement> {
self.placements.get(&window).copied()
}
@@ -823,6 +836,7 @@ impl WorkspaceSet {
}
/// Reserve workspace identities in a pure topology plan without creating them.
#[must_use]
pub fn next_workspace_id(&self) -> WorkspaceId {
WorkspaceId(self.next_id)
}
@@ -1210,10 +1224,12 @@ fn finite_or_zero(value: f64) -> f64 {
}
impl Workspace {
#[must_use]
pub fn window_count(&self) -> usize {
self.layout.window_ids().count() + self.floating.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.layout.window_ids().next().is_none() && self.floating.is_empty()
}
@@ -14,6 +14,7 @@ pub struct WorkspaceView {
}
impl OutputWorkspaceMap {
#[must_use]
pub fn workspace_views(&self, workspaces: &WorkspaceSet) -> Vec<WorkspaceView> {
self.workspace_views_iter(workspaces).collect()
}
@@ -47,6 +48,7 @@ impl OutputWorkspaceMap {
})
}
#[must_use]
pub fn ordered_workspace_ids<'a>(
&'a self,
workspaces: &'a WorkspaceSet,
@@ -56,6 +58,7 @@ impl OutputWorkspaceMap {
.filter(|id| workspaces.workspace(*id).is_some())
}
#[must_use]
pub fn ordered_workspaces(
&self,
workspaces: &WorkspaceSet,
@@ -66,6 +69,7 @@ impl OutputWorkspaceMap {
/// Out-of-range numeric selection resolves to the trailing spare. Looking
/// up a gesture target never creates workspaces or changes history.
#[must_use]
pub fn workspace_at(
&self,
workspaces: &WorkspaceSet,
+1
View File
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
pub const VERSION: u32 = 1;
#[must_use]
pub fn now_ms() -> u64 {
let mut time = libc::timespec {
tv_sec: 0,
+1
View File
@@ -63,6 +63,7 @@ impl Event {
}
impl Snapshot {
#[must_use]
pub fn changes_since(&self, previous: &Self) -> Vec<Change> {
let mut changes = Vec::new();
if self.outputs != previous.outputs {
+1
View File
@@ -36,6 +36,7 @@ pub struct ResponseError {
}
impl Response {
#[must_use]
pub fn success(id: u64, result: Value) -> Self {
Self {
version: VERSION,
+4
View File
@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize};
#[must_use]
pub fn now_us() -> u64 {
let mut time = libc::timespec {
tv_sec: 0,
@@ -79,6 +80,7 @@ impl Default for Player {
}
impl Player {
#[must_use]
pub fn label(&self) -> &str {
if self.title.is_empty() {
&self.identity
@@ -87,6 +89,7 @@ impl Player {
}
}
#[must_use]
pub fn can_toggle(&self) -> bool {
self.can_control
&& if self.status == Playback::Playing {
@@ -96,6 +99,7 @@ impl Player {
}
}
#[must_use]
pub fn position_at(&self, now_us: u64) -> Option<u64> {
let mut position = self.position_us? as f64;
if self.status == Playback::Playing && self.rate.is_finite() {
+2
View File
@@ -17,6 +17,7 @@ pub enum State {
}
impl State {
#[must_use]
pub fn is_inactive(self) -> bool {
matches!(self, Self::Disabled | Self::Idle | Self::Stopped)
}
@@ -51,6 +52,7 @@ impl Status {
serde_json::from_value(value)
}
#[must_use]
pub fn to_value(&self) -> Value {
match serde_json::to_value(self) {
Ok(value) => value,
+9
View File
@@ -63,6 +63,7 @@ pub struct Rect {
}
impl Rect {
#[must_use]
pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
Self {
x,
@@ -165,14 +166,17 @@ pub struct LayoutTree {
}
impl LayoutTree {
#[must_use]
pub fn root(&self) -> Option<NodeId> {
self.root
}
#[must_use]
pub fn node(&self, id: NodeId) -> Option<&Node> {
self.nodes.get(&id)
}
#[must_use]
pub fn contains(&self, window: WindowId) -> bool {
self.windows.contains_key(&window)
}
@@ -181,6 +185,7 @@ impl LayoutTree {
self.windows.keys().copied()
}
#[must_use]
pub fn visible_window_count(&self) -> usize {
fn count(tree: &LayoutTree, node: NodeId) -> usize {
match tree.nodes.get(&node) {
@@ -197,6 +202,7 @@ impl LayoutTree {
self.root.map_or(0, |root| count(self, root))
}
#[must_use]
pub fn preferred_window(&self) -> Option<WindowId> {
let leaf = self.first_window(self.root?).ok()?;
match self.nodes.get(&leaf)? {
@@ -424,10 +430,12 @@ impl LayoutTree {
})
}
#[must_use]
pub fn tab_group(&self, window: WindowId) -> Option<TabGroup> {
self.group_at(*self.parents.get(self.windows.get(&window)?)?)
}
#[must_use]
pub fn tab_groups(&self) -> Vec<TabGroup> {
let mut groups: Vec<_> = self
.nodes
@@ -438,6 +446,7 @@ impl LayoutTree {
groups
}
#[must_use]
pub fn adjacent_tab(&self, window: WindowId, previous: bool) -> Option<WindowId> {
let group = self.tab_group(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() {
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 {
*height = 1.0;
}
});
}
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);
}
}
@@ -166,6 +168,7 @@ impl ViewportTarget {
/// Re-evaluate the same target with pending allocated widths held back.
/// 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 {
let replacements = widths
.iter()
@@ -181,6 +184,7 @@ impl ViewportTarget {
self.target(&replacements, start)
}
#[must_use]
pub fn uses_previous_target(&self) -> bool {
matches!(self.request, ViewportRequest::Slide(..))
|| (matches!(self.request, ViewportRequest::Reveal(..))
@@ -290,6 +294,7 @@ impl ScrollingLayout {
Ok(())
}
#[must_use]
pub fn tab_group(&self, window: WindowId) -> Option<crate::TabGroup> {
let (index, _) = self.window_location(window)?;
let column = &self.columns[index];
@@ -301,6 +306,7 @@ impl ScrollingLayout {
})
}
#[must_use]
pub fn adjacent_tab(&self, window: WindowId, previous: bool) -> Option<WindowId> {
let group = self.tab_group(window)?;
let index = group.windows.iter().position(|id| *id == window)?;
@@ -308,6 +314,7 @@ impl ScrollingLayout {
Some(group.windows[(index + step) % group.windows.len()])
}
#[must_use]
pub fn visible_window_count(&self) -> usize {
self.columns
.iter()
@@ -325,6 +332,7 @@ impl ScrollingLayout {
self.default_width = normalized_width(width);
}
#[must_use]
pub fn with_default_width(default_width: ColumnWidth) -> Self {
Self {
default_width: normalized_width(default_width),
@@ -337,33 +345,40 @@ impl ScrollingLayout {
self.request_focus(self.active_window().map(PendingFocusRequest::Reveal));
}
#[must_use]
pub fn default_width(&self) -> ColumnWidth {
self.default_width
}
#[must_use]
pub fn columns(&self) -> &[Column] {
&self.columns
}
#[must_use]
pub fn active_column(&self) -> Option<usize> {
self.active_column
}
#[must_use]
pub fn active_window(&self) -> Option<WindowId> {
let column = self.columns.get(self.active_column?)?;
column.windows.get(column.active).copied()
}
#[must_use]
pub fn viewport_x(&self) -> f64 {
self.viewport_x
}
#[must_use]
pub fn allocated_column_width(&self, window: WindowId) -> Option<f64> {
self.allocated_widths.get(&window).copied()
}
/// Whether this target changes when the pending column width is held back.
/// 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 {
(source - target).abs() > 0.001
&& self
@@ -373,6 +388,7 @@ impl ScrollingLayout {
.is_some_and(|held| (held - self.viewport_x).abs() > 0.001)
}
#[must_use]
pub fn viewport_target(&self) -> Option<&ViewportTarget> {
self.viewport_basis.as_ref()
}
@@ -400,6 +416,7 @@ impl ScrollingLayout {
self.viewport_basis = Some(next);
}
#[must_use]
pub fn contains(&self, window: WindowId) -> bool {
self.window_location(window).is_some()
}
+4
View File
@@ -18,6 +18,7 @@ pub enum Presentation {
}
impl Presentation {
#[must_use]
pub fn style(self) -> Option<PanelStyle> {
match self {
Self::Floating => Some(PanelStyle::Floating),
@@ -48,6 +49,7 @@ pub struct Adaptive {
}
impl Adaptive {
#[must_use]
pub fn new(state: WindowState) -> Self {
Self {
current: state.into(),
@@ -55,6 +57,7 @@ impl Adaptive {
}
}
#[must_use]
pub fn current(&self) -> Presentation {
self.current
}
@@ -87,6 +90,7 @@ impl Adaptive {
}
}
#[must_use]
pub fn deadline(&self) -> Option<Duration> {
self.pending
.map(|(_, since)| since.saturating_add(HYSTERESIS))
+1
View File
@@ -9,6 +9,7 @@ pub mod shape;
pub use shape::Style as PanelStyle;
#[must_use]
pub const fn default_island_padding() -> f32 {
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] {
match mode {
Mode::Idle => [self.idle_width, collapsed_height],
@@ -114,6 +115,7 @@ pub enum Mode {
}
impl Mode {
#[must_use]
pub fn expanded(self) -> bool {
matches!(self, Self::Expanded(_))
}
@@ -145,7 +147,7 @@ pub struct Hub {
}
impl Hub {
pub fn new(profile: Profile, collapsed_height: f64) -> Result<Self, &'static str> {
pub fn try_new(profile: Profile, collapsed_height: f64) -> Result<Self, &'static str> {
profile.validate()?;
if !collapsed_height.is_finite() || !(24. ..=128.).contains(&collapsed_height) {
return Err("hub height must be 24..128");
@@ -196,6 +198,7 @@ impl Hub {
self.motion.retarget(target)
}
#[must_use]
pub fn mode(&self) -> Mode {
self.mode
}
@@ -301,10 +304,12 @@ impl Hub {
self.active()
}
#[must_use]
pub fn active(&self) -> bool {
self.motion.active() || self.sample().previous_opacity > 0. || self.sample().opacity < 1.
}
#[must_use]
pub fn sample(&self) -> Sample {
let fade = |elapsed: Duration| (elapsed.as_secs_f32() / FADE.as_secs_f32()).clamp(0., 1.);
let opacity = self.initial_opacity
@@ -324,11 +329,13 @@ impl Hub {
}
}
#[must_use]
pub fn allocation_size(&self) -> [f64; 2] {
let maximum = self.motion.maximum_size();
std::array::from_fn(|i| maximum[i].min(self.available[i]))
}
#[must_use]
pub fn target_size(&self) -> [f64; 2] {
let target = self.motion.target().bounds;
[target.width, target.height]
@@ -341,7 +348,7 @@ mod tests {
#[test]
fn workspace_width_changes_preserve_content_and_motion() {
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
hub.retarget_idle(270., [800., 600.]).unwrap();
hub.advance(Duration::from_millis(60), false);
let before = hub.sample();
@@ -358,7 +365,7 @@ mod tests {
#[test]
fn measured_height_preserves_fade_and_motion_and_caps_long_content() {
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
hub.retarget(Mode::Expanded(Content::Controls), [800., 600.])
.unwrap();
hub.advance(Duration::from_millis(180), false);
@@ -382,7 +389,7 @@ mod tests {
#[test]
fn a_content_swap_fades_without_restarting_the_geometry() {
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
hub.retarget(Mode::Expanded(Content::Controls), [800., 600.])
.unwrap();
hub.advance(Duration::from_secs(2), false);
@@ -404,7 +411,7 @@ mod tests {
#[test]
fn content_swap_uses_a_fixed_delay_instead_of_waiting_for_spring_settlement() {
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
assert!(!hub.active());
hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.])
.unwrap();
@@ -430,7 +437,7 @@ mod tests {
#[test]
fn an_interrupted_content_delay_never_reveals_the_cancelled_panel() {
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
hub.retarget(Mode::Expanded(Content::Media), [800., 600.])
.unwrap();
hub.advance(Duration::from_millis(80), false);
@@ -448,7 +455,7 @@ mod tests {
#[test]
fn spring_overshoots_then_settles_within_its_reserved_surface() {
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.])
.unwrap();
let allocation = hub.allocation_size();
@@ -468,7 +475,7 @@ mod tests {
#[test]
fn overshoot_is_clipped_to_small_outputs_and_reduced_motion_removes_all_fades() {
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.])
.unwrap();
for _ in 0..60 {
@@ -486,7 +493,7 @@ mod tests {
#[test]
fn interrupted_expansion_keeps_the_presented_shape() {
let mut hub = Hub::new(Profile::default(), 34.).unwrap();
let mut hub = Hub::try_new(Profile::default(), 34.).unwrap();
hub.retarget(Mode::Expanded(Content::Media), [800., 600.])
.unwrap();
hub.advance(Duration::from_millis(40), false);
@@ -502,7 +509,7 @@ mod tests {
#[test]
fn narrow_outputs_and_reduced_motion_keep_the_hub_bounded() {
let mut hub = Hub::new(Profile::default(), 30.).unwrap();
let mut hub = Hub::try_new(Profile::default(), 30.).unwrap();
hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.])
.unwrap();
hub.advance(Duration::ZERO, true);
+8 -5
View File
@@ -18,6 +18,7 @@ pub struct Geometry {
const COUNT: usize = 18;
impl Geometry {
#[must_use]
pub fn rounded(bounds: Rect, radius: f64) -> Self {
Self {
bounds,
@@ -75,7 +76,7 @@ pub struct Motion {
}
impl Motion {
pub fn new(geometry: Geometry) -> Result<Self, &'static str> {
pub fn try_new(geometry: Geometry) -> Result<Self, &'static str> {
Self::with_spring(
geometry,
SpringConfig {
@@ -132,10 +133,12 @@ impl Motion {
})
}
#[must_use]
pub fn current(&self) -> Geometry {
Geometry::from_values(self.values.map(|value| value.current))
}
#[must_use]
pub fn target(&self) -> Geometry {
Geometry::from_values(self.values.map(|value| value.target))
}
@@ -180,7 +183,7 @@ mod tests {
#[test]
fn interruption_keeps_the_current_geometry_and_velocity() {
let mut motion = Motion::new(collapsed()).unwrap();
let mut motion = Motion::try_new(collapsed()).unwrap();
motion.retarget(expanded()).unwrap();
motion.advance(Duration::from_millis(16), false);
let before = motion.current();
@@ -197,7 +200,7 @@ mod tests {
#[test]
fn dimensions_share_one_trajectory_and_stop_requesting_frames() {
let mut motion = Motion::new(collapsed()).unwrap();
let mut motion = Motion::try_new(collapsed()).unwrap();
motion.retarget(expanded()).unwrap();
for _ in 0..12 {
motion.advance(Duration::from_millis(16), false);
@@ -218,7 +221,7 @@ mod tests {
#[test]
fn reduced_motion_snaps_an_inflight_animation() {
let mut motion = Motion::new(collapsed()).unwrap();
let mut motion = Motion::try_new(collapsed()).unwrap();
motion.retarget(expanded()).unwrap();
motion.advance(Duration::from_millis(16), false);
assert!(!motion.advance(Duration::ZERO, true));
@@ -263,7 +266,7 @@ mod tests {
#[test]
fn invalid_retarget_leaves_motion_untouched() {
let mut motion = Motion::new(collapsed()).unwrap();
let mut motion = Motion::try_new(collapsed()).unwrap();
let mut invalid = expanded();
invalid.bounds.height = f64::NAN;
assert!(motion.retarget(invalid).is_err());
+3 -3
View File
@@ -269,7 +269,7 @@ mod tests {
zone: ZonePosition::Center,
bounds: Rect::new(70.3, 0., 160.4, 44.),
}],
PixelGrid::new(scale).unwrap(),
PixelGrid::try_new(scale).unwrap(),
)
.unwrap();
let paths = notch_border(&scene, 0.1 / scale, edge).unwrap();
@@ -325,7 +325,7 @@ mod tests {
},
Rect::new(30.3, 0., width, height),
&[],
PixelGrid::new(scale).unwrap(),
PixelGrid::try_new(scale).unwrap(),
)
.unwrap();
let body = scene.paint[0];
@@ -420,7 +420,7 @@ mod tests {
},
Rect::new(0., 0., 96., 64.),
&groups,
PixelGrid::new(1.25).unwrap(),
PixelGrid::try_new(1.25).unwrap(),
)
.unwrap();
let contours = contours(&scene, 0.1).unwrap();
@@ -33,6 +33,7 @@ pub enum Edge {
}
impl Edge {
#[must_use]
pub fn key(self) -> &'static str {
match self {
Self::Top => "top",
@@ -86,6 +87,7 @@ impl WorkspaceStyle {
Self::AppIcons,
];
#[must_use]
pub fn key(self) -> &'static str {
match self {
Self::Numbers => "numbers",
@@ -96,6 +98,7 @@ impl WorkspaceStyle {
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Numbers => "Numbers",
@@ -135,6 +138,7 @@ pub enum ItemKind {
impl ItemKind {
/// Smallest useful allocation before a flexible control moves into overflow.
#[must_use]
pub fn minimum_width(self) -> Option<f32> {
match self {
Self::FocusedWindow => Some(48.),
@@ -144,6 +148,7 @@ impl ItemKind {
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Overview => "Overview",
@@ -163,6 +168,7 @@ impl ItemKind {
Self::Overflow => "Overflow",
}
}
#[must_use]
pub fn representations(self) -> &'static [Representation] {
use Representation::*;
match self {
@@ -265,6 +271,7 @@ impl<'de> Deserialize<'de> for Item {
impl Item {
/// Start at the configured preference and retain supported smaller forms.
#[must_use]
pub fn representations(&self) -> Vec<Representation> {
let supported = self.kind.representations();
let preferred = self.representation.unwrap_or(supported[0]);
@@ -279,6 +286,7 @@ impl Item {
alternatives
}
}
#[must_use]
pub fn new(id: &str, kind: ItemKind) -> Self {
Self {
id: ItemId(id.into()),
@@ -305,6 +313,7 @@ impl Item {
}
}
#[must_use]
pub fn available(&self, availability: Availability) -> bool {
if !self.visible {
return false;
@@ -351,6 +360,7 @@ pub enum GroupSurface {
}
impl GroupSurface {
#[must_use]
pub fn key(self) -> &'static str {
match self {
Self::None => "none",
@@ -519,6 +529,7 @@ impl Default for Panel {
}
impl Panel {
#[must_use]
pub fn preset(&self) -> Option<PanelStyle> {
Some(self.style)
}
@@ -527,6 +538,7 @@ impl Panel {
self.style = style;
}
#[must_use]
pub fn surface(&self) -> PanelSurface {
if matches!(self.style, PanelStyle::Flat | PanelStyle::Frame) {
PanelSurface::Solid
@@ -535,6 +547,7 @@ impl Panel {
}
}
#[must_use]
pub fn group_surface(&self) -> GroupSurface {
if self.surface() == PanelSurface::Solid {
GroupSurface::None
@@ -543,6 +556,7 @@ impl Panel {
}
}
#[must_use]
pub fn zone_spacing(&self, zone: &Zone) -> f32 {
if self.style.connected() {
0.
@@ -551,6 +565,7 @@ impl Panel {
}
}
#[must_use]
pub fn shape_parameters(&self) -> crate::scene::Parameters {
crate::scene::Parameters {
style: self.style,
@@ -567,6 +582,7 @@ impl Panel {
}
}
#[must_use]
pub fn resolved_geometry(&self) -> PanelGeometry {
let mut geometry = self.geometry.resolve(self.style);
if self.style == PanelStyle::Morph && self.morph.attached {
@@ -575,6 +591,7 @@ impl Panel {
geometry
}
#[must_use]
pub fn exclusive_zone(&self, hidden: bool) -> i32 {
if hidden {
0
@@ -584,6 +601,7 @@ impl Panel {
}
}
#[must_use]
pub fn group_count(&self) -> usize {
[&self.start, &self.center, &self.end]
.into_iter()
@@ -592,6 +610,7 @@ impl Panel {
}
/// Appearance changes do not replace item ownership or measured allocation.
#[must_use]
pub fn same_composition(&self, other: &Self) -> bool {
let same_zone = |left: &Zone, right: &Zone| {
left.spacing == right.spacing
@@ -615,12 +634,14 @@ impl Panel {
&& same_zone(&self.end, &other.end)
}
#[must_use]
pub fn resolved_radius(&self, shell_radius: f32) -> CornerRadii {
self.corner_radius
.unwrap_or_else(|| CornerRadii::uniform(shell_radius))
}
/// The overflow trigger is generated, never persisted as a configured item.
#[must_use]
pub fn overflow_group(&self) -> Group {
Group {
id: GroupId("_overflow".into()),
@@ -646,6 +667,7 @@ impl Panel {
}
}
#[must_use]
pub fn item(&self, id: &ItemId) -> Option<&Item> {
[&self.start, &self.center, &self.end]
.into_iter()
@@ -655,6 +677,7 @@ impl Panel {
}
/// Build a stable initial composition; presets are an editor convenience.
#[must_use]
pub fn from_preset(preset: PanelStyle) -> Self {
use ItemKind::*;
@@ -16,6 +16,7 @@ pub struct GeometryIntent {
}
impl GeometryIntent {
#[must_use]
pub fn resolve(self, style: crate::PanelStyle) -> PanelGeometry {
use crate::PanelStyle::*;
let flush = matches!(style, Notch | Flat | Frame | CornerWrap);
+4
View File
@@ -92,12 +92,14 @@ impl Resolution {
}
}
#[must_use]
pub fn fits(&self, available: f32) -> bool {
self.required_width() <= available
}
/// Clamp a requested inset independently for each output. Content may still
/// overflow on an output too small to fit it even without an inset.
#[must_use]
pub fn side_margin(&self, requested: i32, output_width: i32, padding: f32) -> i32 {
let maximum = ((output_width as f32 - 2. * padding - self.minimum_width) / 2.)
.floor()
@@ -106,6 +108,7 @@ impl Resolution {
}
}
#[must_use]
pub fn resolve(
panel: &Panel,
measurements: &[Measurement],
@@ -116,6 +119,7 @@ pub fn resolve(
}
/// Reserve separation between zones while keeping the center on the panel midpoint.
#[must_use]
pub fn resolve_with_gap(
panel: &Panel,
measurements: &[Measurement],
+5
View File
@@ -5,6 +5,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub struct CornerRadii(pub [f32; 4]);
impl CornerRadii {
#[must_use]
pub fn uniform(radius: f32) -> Self {
Self([radius; 4])
}
@@ -18,6 +19,7 @@ impl CornerRadii {
}
/// A panel touching the top edge must cover its top corners.
#[must_use]
pub fn at_top_edge(mut self, touching: bool) -> Self {
if touching {
self.0[0] = 0.;
@@ -26,6 +28,7 @@ impl CornerRadii {
self
}
#[must_use]
pub fn at_edge(mut self, edge: super::Edge, touching: bool) -> Self {
if touching {
match edge {
@@ -44,6 +47,7 @@ impl CornerRadii {
}
/// Keep every corner within half the shorter surface dimension.
#[must_use]
pub fn clamped(self, width: f32, height: f32) -> Self {
let limit = width.min(height).max(0.) / 2.;
Self(self.0.map(|value| value.min(limit)))
@@ -52,6 +56,7 @@ impl CornerRadii {
/// Scale all corners together when their sums exceed an adjacent edge.
/// Unlike the symmetric round-box clamp, this permits a large free-side
/// corner when the corner on the anchored side is square.
#[must_use]
pub fn normalized(self, width: f32, height: f32) -> Self {
let [tl, tr, br, bl] = self.0;
let mut factor = 1.0_f64;
+17 -8
View File
@@ -16,6 +16,7 @@ pub struct Primitive {
}
impl Primitive {
#[must_use]
pub fn packet(self) -> [f32; 10] {
let Rect {
x,
@@ -66,6 +67,7 @@ impl Primitive {
})
}
#[must_use]
pub fn distance(self, point: [f64; 2]) -> f64 {
let rect = rectangle_distance(point, self.bounds);
if self.operation >= 2 {
@@ -103,6 +105,7 @@ fn rectangle_distance(point: [f64; 2], b: Rect) -> f64 {
x.max(0.).hypot(y.max(0.)) + x.max(y).min(0.)
}
#[must_use]
pub fn smooth_min(a: f64, b: f64, strength: f64) -> f64 {
if strength <= 0. {
return a.min(b);
@@ -141,16 +144,19 @@ impl Scene {
zones.into_iter().fold(f64::INFINITY, f64::min).max(-cutout)
}
#[must_use]
pub fn contains(&self, point: [f64; 2]) -> bool {
self.input.iter().any(|shape| shape.contains(point))
}
#[must_use]
pub fn packets(&self) -> Vec<[f32; 10]> {
self.paint.iter().map(|p| p.packet()).collect()
}
/// Horizontal runs at physical-pixel centers. Corners and animated gaps
/// stay click-through; adjacent equal runs are coalesced into one rectangle.
#[must_use]
pub fn input_rectangles(&self, scale: f64) -> Vec<[i32; 4]> {
if !scale.is_finite() || scale <= 0. {
return Vec::new();
@@ -246,7 +252,7 @@ fn add(
input: bool,
zone: u8,
) -> Result<(), &'static str> {
let rounded = RoundedSurface::new(rect, radii, grid)?;
let rounded = RoundedSurface::try_new(rect, radii, grid)?;
if rounded.bounds().width == 0. || rounded.bounds().height == 0. {
return Ok(());
}
@@ -320,7 +326,7 @@ pub fn derive(
operation: 1,
blend_zone: 0,
});
scene.input.push(RoundedSurface::new(
scene.input.push(RoundedSurface::try_new(
Rect::new(bounds.x, bounds.y, bounds.width, top),
CornerRadii::uniform(0.),
grid,
@@ -583,8 +589,11 @@ pub fn derive(
}
for input in &mut scene.input {
let [tl, tr, br, bl] = input.radii().0;
*input =
RoundedSurface::new(mirror(input.bounds()), CornerRadii([bl, br, tr, tl]), grid)?;
*input = RoundedSurface::try_new(
mirror(input.bounds()),
CornerRadii([bl, br, tr, tl]),
grid,
)?;
}
for damage in &mut scene.damage {
*damage = mirror(*damage);
@@ -624,7 +633,7 @@ mod tests {
},
Rect::new(0., 0., 800., 36.),
&groups(),
PixelGrid::new(1.25).unwrap(),
PixelGrid::try_new(1.25).unwrap(),
)
.unwrap()
}
@@ -677,7 +686,7 @@ mod tests {
},
Rect::new(0., 0., 800., 600.),
&[],
PixelGrid::new(1.25).unwrap(),
PixelGrid::try_new(1.25).unwrap(),
)
.unwrap();
assert!(scene.contains([18., 200.]));
@@ -714,7 +723,7 @@ mod tests {
},
Rect::new(0., 0., 1600., 1000.),
&[],
PixelGrid::new(1.5).unwrap(),
PixelGrid::try_new(1.5).unwrap(),
)
.unwrap();
assert!(scene.distance([800., 20.]) < 0. && scene.distance([800., 500.]) > 0.);
@@ -746,7 +755,7 @@ mod tests {
fn attached_styles_mirror_the_whole_outline_at_the_bottom() {
for style in [Style::Notch, Style::Frame, Style::CornerWrap] {
let bounds = Rect::new(0., 0., 800., if style == Style::Notch { 36. } else { 600. });
let grid = PixelGrid::new(1.).unwrap();
let grid = PixelGrid::try_new(1.).unwrap();
let top = derive(
Parameters {
style,
+28 -15
View File
@@ -43,6 +43,7 @@ impl Style {
Self::WindowHug,
];
#[must_use]
pub const fn key(self) -> &'static str {
match self {
Self::Floating => "floating",
@@ -57,6 +58,7 @@ impl Style {
Self::WindowHug => "window-hug",
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Floating => "Floating",
@@ -71,9 +73,11 @@ impl Style {
Self::WindowHug => "Window hug",
}
}
#[must_use]
pub const fn connected(self) -> bool {
matches!(self, Self::Capsules | Self::Notch)
}
#[must_use]
pub const fn detached(self) -> bool {
matches!(
self,
@@ -100,6 +104,7 @@ pub enum ZonePosition {
pub struct AnchoredRadii(pub [f32; 4]);
impl AnchoredRadii {
#[must_use]
pub fn screen_space(self, edge: Edge) -> CornerRadii {
let [flat_left, flat_right, free_right, free_left] = self.0;
CornerRadii(match edge {
@@ -173,13 +178,14 @@ pub struct PixelGrid {
}
impl PixelGrid {
pub fn new(scale: f64) -> Result<Self, &'static str> {
pub fn try_new(scale: f64) -> Result<Self, &'static str> {
if !scale.is_finite() || scale <= 0. {
return Err("output scale must be finite and positive");
}
Ok(Self { scale })
}
#[must_use]
pub fn snap(self, position: f64) -> f64 {
(position * self.scale).round() / self.scale
}
@@ -218,7 +224,11 @@ pub struct RoundedSurface {
}
impl RoundedSurface {
pub fn new(bounds: Rect, radii: CornerRadii, grid: PixelGrid) -> Result<Self, &'static str> {
pub fn try_new(
bounds: Rect,
radii: CornerRadii,
grid: PixelGrid,
) -> Result<Self, &'static str> {
if radii.validate().is_err() {
return Err("surface radii must be finite and nonnegative");
}
@@ -230,16 +240,19 @@ impl RoundedSurface {
Ok(Self { bounds, radii })
}
#[must_use]
pub fn bounds(self) -> Rect {
self.bounds
}
#[must_use]
pub fn radii(self) -> CornerRadii {
self.radii
}
/// Right and bottom boundaries are excluded so a shared seam belongs to
/// exactly one surface, rather than both neighbors.
#[must_use]
pub fn contains(self, point: [f64; 2]) -> bool {
let [x, y] = [point[0] - self.bounds.x, point[1] - self.bounds.y];
let (width, height) = (self.bounds.width, self.bounds.height);
@@ -339,7 +352,7 @@ mod tests {
#[test]
fn shared_boundaries_snap_without_seams_at_fractional_scales() {
for scale in [1., 1.25, 1.5, 2.] {
let grid = PixelGrid::new(scale).unwrap();
let grid = PixelGrid::try_new(scale).unwrap();
for origin in [-1800.3, -0.4, 0., 23.3] {
let left = grid.rect(Rect::new(origin, 0., 33.3, 36.)).unwrap();
let right = grid.rect(Rect::new(origin + 33.3, 0., 42.7, 36.)).unwrap();
@@ -353,14 +366,14 @@ mod tests {
#[test]
fn floating_gaps_and_round_corners_do_not_capture_input() {
let grid = PixelGrid::new(1.25).unwrap();
let left = RoundedSurface::new(
let grid = PixelGrid::try_new(1.25).unwrap();
let left = RoundedSurface::try_new(
Rect::new(8., 8., 100., 36.),
CornerRadii::uniform(18.),
grid,
)
.unwrap();
let right = RoundedSurface::new(
let right = RoundedSurface::try_new(
Rect::new(116., 8., 100., 36.),
CornerRadii::uniform(18.),
grid,
@@ -381,15 +394,15 @@ mod tests {
#[test]
fn asymmetric_hit_testing_matches_a_vertically_mirrored_surface() {
let grid = PixelGrid::new(1.).unwrap();
let grid = PixelGrid::try_new(1.).unwrap();
let bounds = Rect::new(0., 0., 100., 28.);
let top = RoundedSurface::new(
let top = RoundedSurface::try_new(
bounds,
AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Top),
grid,
)
.unwrap();
let bottom = RoundedSurface::new(
let bottom = RoundedSurface::try_new(
bounds,
AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Bottom),
grid,
@@ -409,14 +422,14 @@ mod tests {
#[test]
fn a_connected_seam_has_exactly_one_input_owner() {
let grid = PixelGrid::new(1.5).unwrap();
let left = RoundedSurface::new(
let grid = PixelGrid::try_new(1.5).unwrap();
let left = RoundedSurface::try_new(
Rect::new(0., 0., 30., 36.),
CornerRadii([18., 0., 0., 18.]),
grid,
)
.unwrap();
let right = RoundedSurface::new(
let right = RoundedSurface::try_new(
Rect::new(30., 0., 40., 36.),
CornerRadii([0., 18., 18., 0.]),
grid,
@@ -431,9 +444,9 @@ mod tests {
#[test]
fn invalid_runtime_geometry_is_rejected() {
for scale in [0., -1., f64::NAN, f64::INFINITY] {
assert!(PixelGrid::new(scale).is_err());
assert!(PixelGrid::try_new(scale).is_err());
}
let grid = PixelGrid::new(1.).unwrap();
let grid = PixelGrid::try_new(1.).unwrap();
for bounds in [
Rect::new(0., 0., -1., 28.),
Rect::new(f64::NAN, 0., 10., 28.),
@@ -442,7 +455,7 @@ mod tests {
assert!(grid.rect(bounds).is_err());
}
assert!(
RoundedSurface::new(Rect::new(0., 0., 10., 28.), CornerRadii::uniform(-1.), grid)
RoundedSurface::try_new(Rect::new(0., 0., 10., 28.), CornerRadii::uniform(-1.), grid)
.is_err()
);
assert!(
@@ -9,6 +9,7 @@ pub fn current() -> Snapshot {
}
/// Standalone appearance; it does not represent compositor acceptance.
#[must_use]
pub fn fallback() -> Snapshot {
let theme = ferese_config::theme::default_theme();
Snapshot {
+8
View File
@@ -17,6 +17,7 @@ pub fn rgba(s: &str) -> Result<[f64; 4], String> {
Ok(out)
}
#[must_use]
pub fn hex(c: [f64; 4]) -> String {
format!(
"#{:02X}{:02X}{:02X}{:02X}",
@@ -27,6 +28,7 @@ pub fn hex(c: [f64; 4]) -> String {
)
}
#[must_use]
pub fn luminance(c: [f64; 4]) -> f64 {
let linear = |v: f64| {
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])
}
#[must_use]
pub fn contrast(a: [f64; 4], b: [f64; 4]) -> f64 {
let (a, b) = (luminance(a), luminance(b));
(a.max(b) + 0.05) / (a.min(b) + 0.05)
}
#[must_use]
pub fn composite(fg: [f64; 4], bg: [f64; 4]) -> [f64; 4] {
let mut c = [0.; 4];
for i in 0..3 {
@@ -52,10 +56,12 @@ pub fn composite(fg: [f64; 4], bg: [f64; 4]) -> [f64; 4] {
c
}
#[must_use]
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)
}
#[must_use]
pub fn readable(preferred: [f64; 4], background: [f64; 4], minimum: f64) -> [f64; 4] {
let mut preferred = preferred;
preferred[3] = 1.;
@@ -78,6 +84,7 @@ pub fn readable(preferred: [f64; 4], background: [f64; 4], minimum: f64) -> [f64
target
}
#[must_use]
pub fn readable_across(
mut preferred: [f64; 4],
backgrounds: &[[f64; 4]],
@@ -109,6 +116,7 @@ pub fn readable_across(
target
}
#[must_use]
pub fn material_bounds(mut surface: [f64; 4], opacity: f64) -> [[f64; 4]; 2] {
surface[3] = opacity;
[
@@ -34,6 +34,7 @@ pub struct Family {
}
impl Family {
#[must_use]
pub fn palette(&self, appearance: Appearance) -> Option<&Palette> {
match appearance {
Appearance::Light => self.light.as_ref(),
@@ -41,6 +42,7 @@ impl Family {
}
}
#[must_use]
pub fn availability(&self) -> &'static str {
match (self.light.is_some(), self.dark.is_some()) {
(true, true) => "light and dark",
@@ -121,6 +121,7 @@ pub struct Paint {
}
impl Paint {
#[must_use]
pub fn effective_gradient(&self) -> Option<&Gradient> {
if self.style == PaintStyle::Solid {
None
@@ -153,6 +154,7 @@ pub struct ResolvedTheme {
}
/// Accessibility takes precedence over a panel's authored opacity.
#[must_use]
pub fn effective_panel_opacity(
inherited: f32,
custom: Option<f32>,
@@ -166,6 +168,7 @@ pub fn effective_panel_opacity(
}
impl ResolvedTheme {
#[must_use]
pub fn panel_opacity(&self, custom: Option<f32>) -> f32 {
let inherited = if self.tokens.material.style == "translucent" {
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.
#[must_use]
pub fn accent_button() -> theme::Button {
theme::Button::Custom {
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 jiff::Zoned;
#[must_use]
pub fn format_bar_time(now: &Zoned) -> String {
let months = [
"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" }
)
}
#[must_use]
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 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)
}
#[must_use]
pub fn grid<'a, M: Clone + 'a>(
today: jiff::civil::Date,
offset: i32,
@@ -41,6 +44,7 @@ pub fn grid<'a, M: Clone + 'a>(
grid_with_density(today, offset, palette, font, opacity, navigation, false)
}
#[must_use]
pub fn compact_grid<'a, M: Clone + 'a>(
today: jiff::civil::Date,
offset: i32,
+6
View File
@@ -1,5 +1,6 @@
use cosmic::iced::Color;
#[must_use]
pub fn luminance(color: Color) -> f32 {
let linear = |v: f32| {
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)
}
#[must_use]
pub fn contrast(a: Color, b: Color) -> f32 {
let (a, b) = (luminance(a), luminance(b));
(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.
/// The returned fill is opaque, so wallpaper and translucent panels cannot change
/// the contrast of a small, filled control.
#[must_use]
pub fn composite(fill: Color, surface: Color) -> Color {
let a = fill.a.clamp(0., 1.);
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.
/// 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 {
let preferred = composite(preferred, background);
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
/// brightness until the text has enough contrast.
#[must_use]
pub fn accent_pair(background: Color, preferred: Color) -> (Color, Color) {
let on = composite(preferred, 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.
#[must_use]
pub fn accent_color(accent: Color, surface: Color) -> cosmic::cosmic_theme::palette::Srgb {
rgba(composite(accent, surface)).color
}
+13
View File
@@ -5,23 +5,28 @@ use cosmic::{theme, widget};
use crate::{Palette, mix};
#[must_use]
pub fn surface(background: Color, radius: f32) -> theme::Container<'static> {
theme::Container::custom(move |_| surface_appearance(background, radius))
}
#[must_use]
pub fn button_style(p: Palette, selected: bool) -> theme::Button {
button_style_with_focus(p, selected, true)
}
#[must_use]
pub fn button_style_with_focus(p: Palette, selected: bool, focus_visible: bool) -> theme::Button {
styled_button(p, selected, false, 1.0, focus_visible)
}
/// 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 {
material_button_style_with_focus(p, selected, material_opacity, true)
}
#[must_use]
pub fn material_button_style_with_focus(
p: Palette,
selected: bool,
@@ -101,10 +106,12 @@ pub(crate) fn switch_colors(palette: Palette, enabled: bool, hovered: bool) -> (
(track, thumb)
}
#[must_use]
pub fn navigation_style(p: Palette, selected: bool) -> theme::Button {
styled_button(p, selected, true, 1.0, true)
}
#[must_use]
pub fn settings_input(p: Palette) -> theme::TextInput {
let appearance = move |focused: bool, hovered: bool| cosmic::widget::text_input::Appearance {
shape: Some(BorderShape::Continuous),
@@ -229,6 +236,7 @@ fn styled_button(
/// 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.
#[must_use]
pub fn authentication_button(palette: Palette) -> theme::Button {
let paint = move |enabled: bool, interaction: f32, focused: bool| {
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 {
let text = palette.text;
let muted = palette.muted;
@@ -293,6 +302,7 @@ pub fn authentication_input(palette: Palette) -> theme::TextInput {
}
}
#[must_use]
pub fn lock_input(
radius: f32,
accent: cosmic::iced::Color,
@@ -361,6 +371,7 @@ pub fn shell_button(
}
}
#[must_use]
pub fn surface_appearance(background: Color, radius: f32) -> container::Style {
container::Style {
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(
foreground: 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 {
let paint = move |outline| button::Style {
shape: Some(BorderShape::Continuous),
+2
View File
@@ -174,6 +174,7 @@ fn desktop(p: &Palette) -> String {
)
}
#[must_use]
pub fn svg(
family: &Family,
variant: Option<Appearance>,
@@ -274,6 +275,7 @@ fn svg_with_radius(
result
}
#[must_use]
pub fn neighbor(index: usize, count: usize, key: &keyboard::key::Named) -> Option<usize> {
use keyboard::key::Named::*;
if count == 0 {
+1
View File
@@ -1,4 +1,5 @@
/// 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 {
(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> {
let rgba = |foreground: Color| {
format!(
+5
View File
@@ -37,6 +37,7 @@ pub fn heading<'a, M: 'a>(
.align_y(Alignment::Center)
}
#[must_use]
pub fn badge<'a, M: 'a>(
content: Element<'a, M>,
accent: Color,
@@ -55,6 +56,7 @@ pub fn badge<'a, M: 'a>(
.into()
}
#[must_use]
pub fn section<'a, M: 'a>(
content: Element<'a, M>,
foreground: Color,
@@ -84,6 +86,7 @@ pub fn section_label<'a>(
.class(cosmic::theme::Text::Color(muted))
}
#[must_use]
pub fn separator<'a, M: 'a>(foreground: Color, opacity: f32) -> Element<'a, M> {
container(cosmic::widget::Space::new().width(Length::Fill).height(1))
.class(crate::controls::surface(
@@ -93,6 +96,7 @@ pub fn separator<'a, M: 'a>(foreground: Color, opacity: f32) -> Element<'a, M> {
.into()
}
#[must_use]
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_radius = 10.;
@@ -123,6 +127,7 @@ pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) -
.into()
}
#[must_use]
pub fn slider(foreground: Color, opacity: f32) -> cosmic::theme::iced::Slider {
let style = std::rc::Rc::new(move |_: &cosmic::Theme| {
use cosmic::iced::widget::slider::{Breakpoint, Handle, HandleShape, Rail, Style};
+7
View File
@@ -19,6 +19,7 @@ pub struct Palette {
}
impl Palette {
#[must_use]
pub fn from_resolved(theme: &ferese_theme_model::ResolvedTheme) -> Self {
let color = |value: &str| parse_color(value).expect("validated resolved theme color");
Self {
@@ -50,6 +51,7 @@ impl Palette {
}
}
#[must_use]
pub fn flat(mut self) -> Self {
self.background = self.sidebar;
self.card = self.sidebar;
@@ -138,6 +140,7 @@ impl Palette {
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 {
cosmic::iced::theme::Style {
background_color: background,
@@ -147,6 +150,7 @@ impl Palette {
}
}
#[must_use]
pub fn parse_color(value: &str) -> Option<Color> {
let hex = value.strip_prefix('#')?;
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 {
Color::from_rgb(
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 {
if crate::luminance(base) > 0.5 {
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 {
if theme.tokens.material.style == "translucent" {
theme.tokens.material.opacity as f32
+1 -1
View File
@@ -107,7 +107,7 @@ impl<M> Surface<'_, M> {
&mut collector,
);
let size = layout.bounds().size();
let scene = PixelGrid::new(self.options.scale)
let scene = PixelGrid::try_new(self.options.scale)
.and_then(|grid| {
ferese_panel::scene::derive(
self.options.parameters,
+4
View File
@@ -16,6 +16,7 @@ pub struct Workspace<'a> {
pub visible_elsewhere: bool,
}
#[must_use]
pub fn width(style: WorkspaceStyle) -> f32 {
match style {
WorkspaceStyle::Numbers | WorkspaceStyle::Dots => 24.,
@@ -25,6 +26,7 @@ pub fn width(style: WorkspaceStyle) -> f32 {
}
}
#[must_use]
pub fn tooltip_label(workspace: &Workspace<'_>) -> String {
let name = if workspace.name.is_empty() || workspace.name == workspace.index.to_string() {
String::new()
@@ -44,6 +46,7 @@ pub fn tooltip_label(workspace: &Workspace<'_>) -> String {
)
}
#[must_use]
pub fn indicator<'a, M: 'a>(
style: WorkspaceStyle,
workspace: Workspace<'a>,
@@ -193,6 +196,7 @@ pub fn indicator<'a, M: 'a>(
.into()
}
#[must_use]
pub fn sample<'a, M: 'a>(
style: WorkspaceStyle,
palette: Palette,