diff --git a/assets/fonts/Cantarell-ExtraBold.otf b/assets/fonts/Cantarell-ExtraBold.otf new file mode 100644 index 0000000..ecb2576 Binary files /dev/null and b/assets/fonts/Cantarell-ExtraBold.otf differ diff --git a/assets/fonts/Cantarell-LICENSE.txt b/assets/fonts/Cantarell-LICENSE.txt new file mode 100644 index 0000000..d9765dc --- /dev/null +++ b/assets/fonts/Cantarell-LICENSE.txt @@ -0,0 +1,111 @@ +Copyright (c) 2009--2019, The Cantarell Authors + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +------------------------------------------------------------------------------- + +Scripts contain parts of the source code of glyphsLib: + +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/assets/fonts/Comfortaa-LICENSE.txt b/assets/fonts/Comfortaa-LICENSE.txt new file mode 100644 index 0000000..8db99f7 --- /dev/null +++ b/assets/fonts/Comfortaa-LICENSE.txt @@ -0,0 +1,94 @@ +Copyright 2011 The Comfortaa Project Authors +(https://github.com/alexeiva/comfortaa), with Reserved Font Name "Comfortaa". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/Comfortaa-Regular.otf b/assets/fonts/Comfortaa-Regular.otf new file mode 100644 index 0000000..b464065 Binary files /dev/null and b/assets/fonts/Comfortaa-Regular.otf differ diff --git a/crates/ferese-core/src/desktop.rs b/crates/ferese-core/src/desktop.rs index c29abb7..de3f2c4 100644 --- a/crates/ferese-core/src/desktop.rs +++ b/crates/ferese-core/src/desktop.rs @@ -179,9 +179,18 @@ pub enum Alignment { Right, } +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ClockStyle { + #[default] + Pixel, + Minimal, +} + #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(default, deny_unknown_fields)] pub struct Clock { + pub style: ClockStyle, pub enabled: bool, /// Empty means every output; otherwise exact connector names. pub outputs: Vec, @@ -212,19 +221,20 @@ pub struct Clock { impl Default for Clock { fn default() -> Self { Self { + style: ClockStyle::Pixel, enabled: false, outputs: Vec::new(), anchor: Anchor::TopLeft, margin_x: 64, margin_y: 80, width: 440, - height: 160, + height: 320, font_family: None, bold: false, - time_size: 72., + time_size: 128., date_size: 18., time_format: "%-I:%M %p".into(), - date_format: "%A, %-d %B".into(), + date_format: "%a, %b %-d".into(), time_zone: None, show_date: true, lowercase: true, diff --git a/docs/desktop-widgets.md b/docs/desktop-widgets.md index 3c6dbbc..1ff87c2 100644 --- a/docs/desktop-widgets.md +++ b/docs/desktop-widgets.md @@ -14,15 +14,16 @@ Enable it in Settings → Desktop widgets or in your Ferese config: ```kdl desktop-widgets { clock enabled=#true anchor="top_left" margin-x=64 margin-y=80 { + style "pixel" // or "minimal" outputs // every output; or outputs "DP-2" width 440 - height 160 - font-family "" // inherit the desktop font + height 320 + font-family "" // style default; or "Comfortaa" bold #false - time-size 72.0 + time-size 128.0 date-size 18.0 time-format "%-I:%M %p" // use "%H:%M" for 24-hour time - date-format "%A, %-d %B" + date-format "%a, %b %-d" time-zone "" // system zone; or "Africa/Lagos" show-date #true lowercase #true @@ -45,6 +46,8 @@ Margins affect anchored axes only: a centered horizontal position ignores All geometry and text sizes use logical pixels and follow the output's scale. Increase the widget width/height for longer formats or larger text. +Pixel is the default clock style: heavy hours above minutes, alternating theme colors, and a small plain date above. Its bundled Cantarell display font can be replaced with the font selector. Minimal retains the plain time/date layout. Pixel zero-pads hours and hides am/pm for the stacked display. Formats with seconds or additional text retain their full formatted label on one line. Both styles shrink time text to fit smaller widget dimensions; an explicit text color overrides the alternating colors. + Saving reloads appearance and labels live. Placement, size, output filters and enable/disable changes recreate only clock surfaces; normal windows and the bar remain intact. Disconnecting an output destroys its clock; reconnecting creates @@ -84,6 +87,8 @@ click Edit for its multiline editor, then Done to finish. Text autosaves after 500 ms without typing. Escape cancels a drag or finishes editing. Position saves on release; dragging changes the anchor to `top_left` and saves pixel margins. Title, size and style are also editable in Settings. Notes persist in the config. +Choose each note’s font in Settings using the installed-font dropdown or enter a family name manually. Default font inherits the interface font. In KDL, use `font-family "Comfortaa"` inside the `note` node. + These are plain-text notes, not Markdown or HTML. ```kdl diff --git a/packaging/config.kdl b/packaging/config.kdl index d2a1efa..05b8ef4 100644 --- a/packaging/config.kdl +++ b/packaging/config.kdl @@ -33,11 +33,12 @@ window-rule title="Choose a wallpaper" floating=#true desktop-widgets { clock { + style "pixel" enabled #false anchor "top_left" margin-x 64 margin-y 80 - time-size 72.0 + time-size 128.0 date-size 18.0 opacity 0.9 } diff --git a/packaging/ferese-screenshot b/packaging/ferese-screenshot index f16396a..fd6279b 100755 --- a/packaging/ferese-screenshot +++ b/packaging/ferese-screenshot @@ -48,13 +48,13 @@ trap '[[ -z $input ]] || rm -f -- "$input"; [[ -s $file ]] || rm -f -- "$file"' input=$(mktemp "$XDG_RUNTIME_DIR/ferese-screenshot-XXXXXX.png") || fail 'Could not create the temporary capture.' grim "${capture_args[@]}" "$input" || fail 'Could not capture the screen. Log back into Ferese after enabling screen capture.' # Enter saves the annotated image and copies it; Escape discards the capture. -# Do not pass the selector lock to Satty or its clipboard subprocess. +exec 9>&- satty --filename "$input" --output-filename "$file" \ --fullscreen current-screen \ --copy-command 'wl-copy --type image/png' \ --save-after-copy \ --actions-on-enter save-to-file,save-to-clipboard,exit \ - --actions-on-escape exit 9>&- || fail 'Could not open the screenshot in Satty.' + --actions-on-escape exit || fail 'Could not open the screenshot in Satty.' if [[ -s $file ]]; then printf '%s\n' "$file" fi diff --git a/scripts/install-session.sh b/scripts/install-session.sh index 512f3c3..7d60190 100644 --- a/scripts/install-session.sh +++ b/scripts/install-session.sh @@ -59,6 +59,8 @@ install -m 0755 -- "$repo_dir/packaging/ferese-session" "$release_dir/ferese-ses install -m 0755 -- "$repo_dir/packaging/ferese-lock" "$release_dir/ferese-lock" install -m 0755 -- "$repo_dir/packaging/ferese-screenshot" "$release_dir/ferese-screenshot" install -m 0644 -- "$repo_dir/packaging/config.kdl" "$release_dir/config.example.kdl" +install -D -m 0644 -- "$repo_dir/assets/fonts/Comfortaa-LICENSE.txt" "$release_dir/licenses/Comfortaa-LICENSE.txt" +install -D -m 0644 -- "$repo_dir/assets/fonts/Cantarell-LICENSE.txt" "$release_dir/licenses/Cantarell-LICENSE.txt" install -d -m 0755 -- "$release_dir/wallpapers" install -m 0644 -- "$repo_dir/assets/wallpapers/ferese.png" "$release_dir/wallpapers/ferese.png" install -m 0644 -- "$repo_dir/assets/wallpapers/ferese.svg" "$release_dir/wallpapers/ferese.svg" diff --git a/shell/ferese-settings/src/fonts.rs b/shell/ferese-settings/src/fonts.rs new file mode 100644 index 0000000..53ee052 --- /dev/null +++ b/shell/ferese-settings/src/fonts.rs @@ -0,0 +1,34 @@ +use std::{process::Command, sync::OnceLock}; + +pub fn families() -> &'static [String] { + static FAMILIES: OnceLock> = OnceLock::new(); + + FAMILIES.get_or_init(|| { + let output = Command::new("fc-list") + .args(["--format", "%{family}\\n"]) + .output() + .ok() + .filter(|output| output.status.success()); + let mut families: Vec = output + .as_ref() + .map(|output| String::from_utf8_lossy(&output.stdout)) + .unwrap_or_default() + .lines() + .flat_map(|line| line.split(',')) + .map(str::trim) + .filter(|family| !family.is_empty() && family.len() <= 128) + .map(str::to_owned) + .collect(); + + families.extend([ + "Comfortaa".into(), + "sans-serif".into(), + "serif".into(), + "monospace".into(), + ]); + families.sort_unstable_by_key(|family| family.to_lowercase()); + families.dedup(); + families.insert(0, "Default font".into()); + families + }) +} diff --git a/shell/ferese-settings/src/main.rs b/shell/ferese-settings/src/main.rs index 66b0e68..7928c4b 100644 --- a/shell/ferese-settings/src/main.rs +++ b/shell/ferese-settings/src/main.rs @@ -1,3 +1,4 @@ +mod fonts; mod schema; mod store; mod visuals; @@ -26,6 +27,7 @@ fn main() -> cosmic::iced::Result { None => store::config_path(), }; let path = path.canonicalize().unwrap_or(path); + fonts::families(); let initial = Snapshot::read(&path); let theme = visuals::native_theme(initial.as_ref().ok()); @@ -45,6 +47,7 @@ enum Message { Page(Page), Search(String), Change(Edit), + SelectFont(String, String), Draft(String, String), Commit(Field), Range(Field, f64), @@ -212,6 +215,10 @@ impl cosmic::Application for App { self.sync_notes(); return task; } + Message::SelectFont(path, family) => { + self.inputs.remove(&path); + return self.change(set(&path, family)); + } Message::DragWindow => return self.core.drag(None), Message::ExternalConfig(result) => match result { Ok(snapshot) @@ -1085,6 +1092,41 @@ impl App { let labels = labels.width(Length::Fill); let path = field.path.clone(); let control: Element<'static, Message> = match field.kind.clone() { + Kind::Font => { + let value = self + .inputs + .get(&path) + .cloned() + .unwrap_or_else(|| self.draft.string(&path, "")); + let families = fonts::families(); + let selected = if value.is_empty() { + Some(0) + } else { + families.iter().position(|family| family == &value) + }; + let selection_path = path.clone(); + let commit = field.clone(); + column([]) + .spacing(6) + .push( + widget::dropdown(families, selected, move |index| { + let family = if index == 0 { "" } else { &families[index] }; + Message::SelectFont(selection_path.clone(), family.to_owned()) + }) + .width(225), + ) + .push( + text_input("Default font", value) + .font(self.font) + .style(visuals::input_style(palette)) + .on_input(move |value| Message::Draft(path.clone(), value)) + .on_submit(move |_| Message::Commit(field.clone())) + .on_unfocus(Message::Commit(commit)) + .width(225) + .size(12), + ) + .into() + } Kind::Toggle(default) => toggler(self.draft.boolean(&path, default)) .size(22) .on_toggle(move |value| Message::Change(set(&path, value))) diff --git a/shell/ferese-settings/src/schema.rs b/shell/ferese-settings/src/schema.rs index d5db414..9cd1fc7 100644 --- a/shell/ferese-settings/src/schema.rs +++ b/shell/ferese-settings/src/schema.rs @@ -90,6 +90,7 @@ impl Page { #[derive(Clone, Debug)] pub enum Kind { + Font, Toggle(bool), Range { default: f64, @@ -198,6 +199,13 @@ fn clock_fields() -> Vec { "Behind windows and click-through.", false, ), + choice( + "desktop_widgets.clock.style", + "Clock style", + "Pixel stacks heavy, alternating-color digits; Minimal uses plain text.", + "pixel", + &[("pixel", "Pixel"), ("minimal", "Minimal")], + ), toggle( "desktop_widgets.clock.bold", "Bold text", @@ -207,7 +215,7 @@ fn clock_fields() -> Vec { toggle( "desktop_widgets.clock.show_date", "Show date", - "A separate line beneath the time.", + "A small date above the digits in Pixel; beneath the time in Minimal.", true, ), toggle( @@ -222,11 +230,11 @@ fn clock_fields() -> Vec { "top_left, top_center, top_right, center_left, center, center_right, bottom_left, bottom_center, bottom_right", "top_left", ), - text( + Field::new( "desktop_widgets.clock.font_family", "Font", - "Empty follows the desktop font.", - "", + "Choose an installed family or enter a name. Empty uses the style default.", + Kind::Font, ), text( "desktop_widgets.clock.time_format", @@ -238,7 +246,7 @@ fn clock_fields() -> Vec { "desktop_widgets.clock.date_format", "Date format", "%A, %-d %B for weekday and date; %-d %b for a short date.", - "%A, %-d %B", + "%a, %b %-d", ), text( "desktop_widgets.clock.time_zone", @@ -301,7 +309,7 @@ fn clock_fields() -> Vec { "desktop_widgets.clock.height", "Widget height", "Leave room for both text lines.", - 160.0, + 320.0, 32.0, 800.0, 1.0, @@ -312,7 +320,7 @@ fn clock_fields() -> Vec { "desktop_widgets.clock.time_size", "Time size", "Logical pixels; scales with your output.", - 72.0, + 128.0, 8.0, 240.0, 1.0, @@ -411,11 +419,11 @@ pub fn note_fields(index: usize) -> Vec { "Nine positions, e.g. top_right or center.", "top_right", ), - text( + Field::new( format!("{prefix}.font_family"), "Font", - "Empty follows the desktop font.", - "", + "Choose an installed family or enter a name. Empty follows the desktop font.", + Kind::Font, ), text( format!("{prefix}.color"), diff --git a/shell/ferese-settings/src/store.rs b/shell/ferese-settings/src/store.rs index dd1f1e1..f5d8923 100644 --- a/shell/ferese-settings/src/store.rs +++ b/shell/ferese-settings/src/store.rs @@ -384,6 +384,7 @@ window-rule app-id="mine" floating=#true for field in fields { use crate::schema::Kind; let values: Vec = match field.kind { + Kind::Font => vec!["".into(), "sans-serif".into()], Kind::Toggle(_) => vec![true.into(), false.into()], Kind::Range { min, max, integer, .. diff --git a/shell/ferese-shell/src/main.rs b/shell/ferese-shell/src/main.rs index e24eba7..1c7d369 100644 --- a/shell/ferese-shell/src/main.rs +++ b/shell/ferese-shell/src/main.rs @@ -107,6 +107,30 @@ impl From for BarMetrics { } } +fn stacked_clock_digits(label: &str) -> Option<[String; 2]> { + let mut parts = label.split_whitespace(); + let time = parts.next()?; + if let Some(period) = parts.next() { + if !period.eq_ignore_ascii_case("am") && !period.eq_ignore_ascii_case("pm") { + return None; + } + } + if parts.next().is_some() { + return None; + } + let (hour, minute) = time.split_once(':')?; + if !(1..=2).contains(&hour.len()) + || minute.len() != 2 + || !hour + .bytes() + .chain(minute.bytes()) + .all(|digit| digit.is_ascii_digit()) + { + return None; + } + Some([format!("{hour:0>2}"), minute.to_owned()]) +} + fn main() -> cosmic::iced::Result { // Small shell surfaces do not need a second GPU device and shader setup. // Keep wgpu as a fallback and honor an explicit renderer preference. @@ -115,6 +139,18 @@ fn main() -> cosmic::iced::Result { // toolkit threads are started. unsafe { std::env::set_var("ICED_BACKEND", "tiny-skia,wgpu") }; } + cosmic::iced::advanced::graphics::text::font_system() + .write() + .unwrap() + .load_font(std::borrow::Cow::Borrowed(include_bytes!( + "../../../assets/fonts/Comfortaa-Regular.otf" + ))); + cosmic::iced::advanced::graphics::text::font_system() + .write() + .unwrap() + .load_font(std::borrow::Cow::Borrowed(include_bytes!( + "../../../assets/fonts/Cantarell-ExtraBold.otf" + ))); let mut config = config::load(); motion::configure(config.animations); let compositor_wallpaper = std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_some(); @@ -1336,16 +1372,22 @@ impl FereseShell { ClockAlignment::Center => alignment::Horizontal::Center, ClockAlignment::Right => alignment::Horizontal::Right, }; - let mut font = configured_font( - clock - .font_family - .as_deref() - .filter(|family| !family.trim().is_empty()) - .or(self.config.font_family.as_deref()), - ); - if clock.bold { + let pixel = clock.style == ferese_core::desktop::ClockStyle::Pixel; + let custom_font = clock + .font_family + .as_deref() + .filter(|family| !family.trim().is_empty()); + let mut font = configured_font(if pixel { + custom_font.or(Some("Cantarell")) + } else { + custom_font.or(self.config.font_family.as_deref()) + }); + if pixel && custom_font.is_none() { + font.weight = cosmic::iced::font::Weight::Black; + } else if clock.bold { font.weight = cosmic::iced::font::Weight::Bold; } + let date_font = configured_font(custom_font.or(self.config.font_family.as_deref())); let tint = |custom: &Option, fallback| { let mut tint = color( custom @@ -1356,21 +1398,98 @@ impl FereseShell { tint.a *= clock.opacity; tint }; + let foreground = tint(&clock.color, self.config.theme.text_primary); + let accent = color(self.config.theme.accent); + let custom_color = clock + .color + .as_deref() + .and_then(config::parse_color) + .is_some(); + let digit_colors = [0.8, 0.45].map(|text_mix| { + if custom_color { + return foreground; + } + Color { + r: foreground.r * text_mix + accent.r * (1.0 - text_mix), + g: foreground.g * text_mix + accent.g * (1.0 - text_mix), + b: foreground.b * text_mix + accent.b * (1.0 - text_mix), + a: foreground.a, + } + }); let mut labels = cosmic::widget::column([]) .spacing(clock.gap) - .align_x(alignment) - .push( - cosmic::widget::text(self.desktop_clock.0.clone()) - .font(font) - .size(clock.time_size) - .width(Length::Fill) - .align_x(alignment) + .align_x(alignment); + + if pixel && clock.show_date { + labels = labels.push( + cosmic::widget::text(self.desktop_clock.1.clone()) + .font(date_font) + .size(clock.date_size) .class(theme::Text::Color(tint( - &clock.color, + &clock.date_color, self.config.theme.text_primary, ))), ); - if clock.show_date { + } + + if let Some(digits) = pixel + .then(|| stacked_clock_digits(&self.desktop_clock.0)) + .flatten() + { + let available_height = (clock.height as f32 + - clock.padding * 2.0 + - if clock.show_date { + clock.date_size * 1.3 + clock.gap + } else { + 0.0 + }) + .max(16.0); + let size = clock + .time_size + .min(available_height / 1.6) + .min((clock.width as f32 - clock.padding * 2.0).max(16.0) / 1.5); + let mut stack = cosmic::widget::column([]).spacing(0).align_x(alignment); + + for (line, digits) in digits.into_iter().enumerate() { + let mut digit_row = row([]).spacing(0); + for (position, digit) in digits.chars().enumerate() { + digit_row = digit_row.push( + cosmic::widget::text(digit.to_string()) + .font(font) + .size(size) + .line_height(cosmic::iced::widget::text::LineHeight::Relative(0.8)) + .class(theme::Text::Color(digit_colors[(line + position) % 2])), + ); + } + stack = stack.push(digit_row); + } + labels = labels.push(stack); + } else { + let characters = self.desktop_clock.0.chars().count().max(1) as f32; + let available_width = (clock.width as f32 - clock.padding * 2.0).max(1.0); + let available_height = (clock.height as f32 + - clock.padding * 2.0 + - if clock.show_date { + clock.date_size * 1.3 + clock.gap + } else { + 0.0 + }) + .max(1.0); + let size = clock + .time_size + .min(available_width / (characters * 0.75)) + .min(available_height / 1.3); + labels = labels.push( + cosmic::widget::text(self.desktop_clock.0.clone()) + .font(font) + .size(size) + .width(Length::Fill) + .align_x(alignment) + .class(theme::Text::Color(foreground)), + ); + } + + if !pixel && clock.show_date { labels = labels.push( cosmic::widget::text(self.desktop_clock.1.clone()) .font(font) @@ -2381,6 +2500,21 @@ mod tests { ); } + #[test] + fn pixel_clock_stacks_only_hour_and_minute_formats() { + assert_eq!( + super::stacked_clock_digits("2:45 pm"), + Some(["02".into(), "45".into()]) + ); + assert_eq!( + super::stacked_clock_digits("14:45"), + Some(["14".into(), "45".into()]) + ); + assert!(super::stacked_clock_digits("14:45:30").is_none()); + assert!(super::stacked_clock_digits("Today 14:45").is_none()); + assert!(super::stacked_clock_digits("14:45 pm extra").is_none()); + } + #[test] fn note_drag_origin_and_bounds_use_logical_output_coordinates() { use cosmic::iced::Point;