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