a lightweight, interval-based utility to combat digital strain through "Ma" (intentional pauses) for the eyes and body.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(ui): adapt profile chips to font size, switch rhythm tab to scrollview

+240 -140
+30 -4
Cargo.lock
··· 1744 1744 "log", 1745 1745 "slotmap", 1746 1746 "tinyvec", 1747 - "ttf-parser", 1747 + "ttf-parser 0.25.1", 1748 + ] 1749 + 1750 + [[package]] 1751 + name = "fontdue" 1752 + version = "0.7.3" 1753 + source = "registry+https://github.com/rust-lang/crates.io-index" 1754 + checksum = "0793f5137567643cf65ea42043a538804ff0fbf288649e2141442b602d81f9bc" 1755 + dependencies = [ 1756 + "hashbrown 0.13.2", 1757 + "ttf-parser 0.15.2", 1748 1758 ] 1749 1759 1750 1760 [[package]] ··· 2339 2349 2340 2350 [[package]] 2341 2351 name = "hashbrown" 2352 + version = "0.13.2" 2353 + source = "registry+https://github.com/rust-lang/crates.io-index" 2354 + checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 2355 + dependencies = [ 2356 + "ahash", 2357 + ] 2358 + 2359 + [[package]] 2360 + name = "hashbrown" 2342 2361 version = "0.14.5" 2343 2362 source = "registry+https://github.com/rust-lang/crates.io-index" 2344 2363 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" ··· 2975 2994 "dirs 5.0.1", 2976 2995 "display-info", 2977 2996 "env_logger", 2997 + "fontdue", 2978 2998 "gtk", 2979 2999 "image", 2980 3000 "log", ··· 4217 4237 source = "registry+https://github.com/rust-lang/crates.io-index" 4218 4238 checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" 4219 4239 dependencies = [ 4220 - "ttf-parser", 4240 + "ttf-parser 0.25.1", 4221 4241 ] 4222 4242 4223 4243 [[package]] ··· 5016 5036 "core_maths", 5017 5037 "log", 5018 5038 "smallvec", 5019 - "ttf-parser", 5039 + "ttf-parser 0.25.1", 5020 5040 "unicode-bidi-mirroring", 5021 5041 "unicode-ccc", 5022 5042 "unicode-properties", ··· 5980 6000 5981 6001 [[package]] 5982 6002 name = "ttf-parser" 6003 + version = "0.15.2" 6004 + source = "registry+https://github.com/rust-lang/crates.io-index" 6005 + checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" 6006 + 6007 + [[package]] 6008 + name = "ttf-parser" 5983 6009 version = "0.25.1" 5984 6010 source = "registry+https://github.com/rust-lang/crates.io-index" 5985 6011 checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" ··· 6137 6163 "strict-num", 6138 6164 "svgtypes", 6139 6165 "tiny-skia-path 0.12.0", 6140 - "ttf-parser", 6166 + "ttf-parser 0.25.1", 6141 6167 "unicode-bidi", 6142 6168 "unicode-script", 6143 6169 "unicode-vo",
+1
Cargo.toml
··· 7 7 [dependencies] 8 8 # UI 9 9 slint = { version = "1", features = ["unstable-winit-030"] } 10 + fontdue = "0.7" 10 11 11 12 # System tray + menu 12 13 tray-icon = "0.19"
+26 -1
src/settings/mod.rs
··· 2 2 use std::sync::{Arc, Mutex}; 3 3 4 4 use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel}; 5 + use fontdue::Font; 5 6 6 7 use crate::autostart; 7 8 use crate::config::{AppConfig, BreakLevelConfig, BreakModeConfig, LongBreakConfig, OverlayTheme}; ··· 143 144 .map(|p| SharedString::from(p.name.as_str())) 144 145 .collect(); 145 146 names.sort(); 146 - window.set_profile_names(ModelRc::new(VecModel::from(names))); 147 + window.set_profile_names(ModelRc::new(VecModel::from(names.clone()))); 148 + 149 + // Measure profile name widths (pixels) using bundled Nunito Regular at 12px 150 + fn measure_name_widths(names: &[SharedString]) -> Vec<i32> { 151 + // include_bytes with concat! so path is resolved from manifest dir at compile time 152 + let font_bytes = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/fonts/Nunito-Regular.ttf")); 153 + let font = Font::from_bytes(font_bytes.as_ref(), fontdue::FontSettings::default()).unwrap(); 154 + let font_size = 12.0; 155 + names 156 + .iter() 157 + .map(|s| { 158 + let mut w = 0.0_f32; 159 + for ch in s.as_str().chars() { 160 + let m = font.metrics(ch, font_size); 161 + w += m.advance_width; 162 + } 163 + // round and ensure at least 16px (defensive) 164 + w.round() as i32 165 + }) 166 + .collect() 167 + } 168 + 169 + let name_widths = measure_name_widths(&names); 170 + // set as a VecModel of ints for the Slint property 171 + window.set_profile_name_widths(ModelRc::new(VecModel::from(name_widths))); 147 172 148 173 window.set_sound_enabled(cfg.appearance.sound_enabled); 149 174 window.set_sound_volume(cfg.appearance.sound_volume);
+7 -5
ui/components/chips.slint
··· 11 11 12 12 for label[i] in root.labels: chip-root := Rectangle { 13 13 height: 32px; 14 - min-width: 48px; 15 14 16 15 accessible-role: AccessibleRole.radio-button; 17 16 accessible-label: label; ··· 49 48 border-color: chip-fs.has-focus 50 49 ? Theme.accent-muted 51 50 : (i == root.selected-index ? transparent : Theme.line); 51 + min-width: 48px; 52 52 animate background { duration: 120ms; } 53 53 animate border-color { duration: 120ms; } 54 54 ··· 57 57 padding-right: 12px; 58 58 alignment: center; 59 59 60 - Text { 60 + chipText := Text { 61 61 text: label; 62 62 font-size: 12px; 63 63 font-weight: i == root.selected-index ? 600 : 400; ··· 73 73 // String-based chip group — for named profile selection. 74 74 export component ProfileChips inherits HorizontalLayout { 75 75 in property <[string]> profile-names; 76 + in property <[int]> name-widths; 76 77 in-out property <string> active-profile: ""; 77 78 callback selection-changed(string); 78 79 79 80 spacing: 6px; 80 81 alignment: center; 81 82 82 - for name in root.profile-names: chip-root := Rectangle { 83 + for name[i] in root.profile-names: chip-root := Rectangle { 83 84 height: 32px; 84 - min-width: 48px; 85 85 86 86 accessible-role: AccessibleRole.radio-button; 87 87 accessible-label: name; ··· 119 119 border-color: chip-fs.has-focus 120 120 ? Theme.accent-muted 121 121 : (name == root.active-profile ? transparent : Theme.line); 122 + // adaptive min-width: provided name-width (px) + 24px padding, but at least 48px 123 + min-width: ((root.name-widths[i] * 1px) + 24px) < 48px ? 48px : ((root.name-widths[i] * 1px) + 24px); 122 124 animate background { duration: 120ms; } 123 125 animate border-color { duration: 120ms; } 124 126 ··· 127 129 padding-right: 12px; 128 130 alignment: center; 129 131 130 - Text { 132 + chipText := Text { 131 133 text: name; 132 134 font-size: 12px; 133 135 font-weight: name == root.active-profile ? 600 : 400;
+3
ui/settings.slint
··· 17 17 title: "ioma — Settings"; 18 18 preferred-width: 760px; 19 19 preferred-height: 680px; 20 + min-width: 520px; 20 21 background: Theme.bg; 21 22 default-font-family: "Nunito"; 22 23 ··· 32 33 in-out property <int> idle-threshold-mins: 5; 33 34 in-out property <string> active-profile: "20·20·20"; 34 35 in-out property <[string]> profile-names: ["20·20·20", "Pomodoro", "52/17", "Focus+Micro"]; 36 + in-out property <[int]> profile-name-widths: [56, 56, 35, 77]; 35 37 in-out property <[LevelEntry]> levels: []; 36 38 in-out property <bool> long-break-enabled: false; 37 39 in-out property <int> long-break-after-cycles: 3; ··· 93 95 idle-threshold-mins <=> root.idle-threshold-mins; 94 96 active-profile <=> root.active-profile; 95 97 profile-names: root.profile-names; 98 + profile-name-widths: root.profile-name-widths; 96 99 theme-mode <=> root.theme-mode; 97 100 set-password-clicked => { root.set-password-clicked(); } 98 101 open-config-dir => { root.open-config-dir(); }
+173 -130
ui/views/rhythm_tab.slint
··· 1 + import { ScrollView } from "std-widgets.slint"; 1 2 import { Theme } from "../theme.slint"; 2 3 import { SettingLabel, PaperDivider } from "../components/atoms.slint"; 3 4 import { PaperButton } from "../components/buttons.slint"; ··· 5 6 import { NumberField, VolumeSlider } from "../components/inputs.slint"; 6 7 import { PaperToggle } from "../components/toggles.slint"; 7 8 8 - export component RhythmTab inherits VerticalLayout { 9 + export component RhythmTab inherits ScrollView { 9 10 in-out property <bool> enforced-mode: false; 10 11 in-out property <bool> sound-enabled: true; 11 12 in-out property <float> sound-volume: 0.7; ··· 14 15 in-out property <int> idle-threshold-mins: 5; 15 16 in-out property <string> active-profile: ""; 16 17 in property <[string]> profile-names; 18 + in property <[int]> profile-name-widths; 17 19 in-out property <int> theme-mode: 0; 18 20 19 21 callback set-password-clicked; ··· 21 23 callback profile-changed(string); 22 24 callback theme-mode-changed(int); 23 25 24 - padding-left: 28px; 25 - padding-right: 28px; 26 - padding-top: 20px; 27 - padding-bottom: 16px; 28 - spacing: 0px; 29 26 vertical-stretch: 1; 30 27 31 - HorizontalLayout { 32 - padding-top: 10px; 33 - padding-bottom: 14px; 34 - spacing: 24px; 28 + VerticalLayout { 29 + padding-left: 28px; 30 + padding-right: 28px; 31 + padding-top: 20px; 32 + padding-bottom: 16px; 33 + spacing: 0px; 35 34 36 - SettingLabel { 37 - title: "Active profile"; 38 - description: "The cadence ioma follows today."; 39 - } 35 + HorizontalLayout { 36 + padding-top: 10px; 37 + padding-bottom: 14px; 38 + spacing: 24px; 40 39 41 - Rectangle { horizontal-stretch: 1; } 40 + SettingLabel { 41 + title: "Active profile"; 42 + description: "The cadence ioma follows today."; 43 + } 42 44 43 - ProfileChips { 44 - profile-names: root.profile-names; 45 - active-profile <=> root.active-profile; 46 - selection-changed(name) => { root.profile-changed(name); } 45 + Rectangle { 46 + horizontal-stretch: 1; 47 + } 48 + 49 + ProfileChips { 50 + profile-names: root.profile-names; 51 + name-widths: root.profile-name-widths; 52 + active-profile <=> root.active-profile; 53 + selection-changed(name) => { 54 + root.profile-changed(name); 55 + } 56 + } 47 57 } 48 - } 49 58 50 - PaperDivider { } 59 + PaperDivider { } 51 60 52 - HorizontalLayout { 53 - padding-top: 12px; 54 - padding-bottom: 4px; 55 - spacing: 24px; 61 + HorizontalLayout { 62 + padding-top: 12px; 63 + padding-bottom: 4px; 64 + spacing: 24px; 56 65 57 - SettingLabel { 58 - title: "Enforced mode"; 59 - description: "Full-screen break. Emergency unlock requires a password."; 60 - } 66 + SettingLabel { 67 + title: "Enforced mode"; 68 + description: "Full-screen break. Emergency unlock requires a password."; 69 + } 61 70 62 - Rectangle { horizontal-stretch: 1; } 71 + Rectangle { 72 + horizontal-stretch: 1; 73 + } 63 74 64 - PaperToggle { checked <=> root.enforced-mode; label: "Enforced mode"; } 65 - } 75 + PaperToggle { 76 + checked <=> root.enforced-mode; 77 + label: "Enforced mode"; 78 + } 79 + } 66 80 67 - HorizontalLayout { 68 - padding-bottom: 12px; 81 + HorizontalLayout { 82 + padding-bottom: 12px; 69 83 70 - PaperButton { 71 - text: "Set emergency unlock password…"; 72 - preferred-width: 220px; 73 - clicked => { root.set-password-clicked(); } 84 + PaperButton { 85 + text: "Set emergency unlock password…"; 86 + preferred-width: 220px; 87 + clicked => { 88 + root.set-password-clicked(); 89 + } 90 + } 74 91 } 75 - } 76 92 77 - PaperDivider { } 93 + PaperDivider { } 78 94 79 - HorizontalLayout { 80 - padding-top: 12px; 81 - padding-bottom: 4px; 82 - spacing: 24px; 95 + HorizontalLayout { 96 + padding-top: 12px; 97 + padding-bottom: 4px; 98 + spacing: 24px; 99 + 100 + SettingLabel { 101 + title: "Pause on idle"; 102 + description: "If you've stepped away, ioma waits."; 103 + } 83 104 84 - SettingLabel { 85 - title: "Pause on idle"; 86 - description: "If you've stepped away, ioma waits."; 105 + Rectangle { 106 + horizontal-stretch: 1; 107 + } 108 + 109 + PaperToggle { 110 + checked <=> root.idle-detection-enabled; 111 + label: "Pause on idle"; 112 + } 87 113 } 88 114 89 - Rectangle { horizontal-stretch: 1; } 115 + HorizontalLayout { 116 + padding-bottom: 12px; 117 + spacing: 10px; 118 + alignment: start; 90 119 91 - PaperToggle { checked <=> root.idle-detection-enabled; label: "Pause on idle"; } 92 - } 120 + Text { 121 + text: "for"; 122 + font-size: 12px; 123 + color: Theme.ink-mid; 124 + vertical-alignment: center; 125 + } 93 126 94 - HorizontalLayout { 95 - padding-bottom: 12px; 96 - spacing: 10px; 97 - alignment: start; 127 + NumberField { 128 + width: 80px; 129 + value <=> root.idle-threshold-mins; 130 + minimum: 1; 131 + maximum: 60; 132 + field-label: "Idle threshold minutes"; 133 + } 98 134 99 - Text { 100 - text: "threshold"; 101 - font-size: 12px; 102 - color: Theme.ink-mid; 103 - vertical-alignment: center; 135 + Text { 136 + text: "minutes before resetting"; 137 + font-size: 12px; 138 + color: Theme.ink-mid; 139 + vertical-alignment: center; 140 + } 104 141 } 105 142 106 - NumberField { 107 - width: 80px; 108 - value <=> root.idle-threshold-mins; 109 - minimum: 1; 110 - maximum: 60; 111 - field-label: "Idle threshold minutes"; 112 - } 143 + PaperDivider { } 113 144 114 - Text { 115 - text: "minutes"; 116 - font-size: 12px; 117 - color: Theme.ink-mid; 118 - vertical-alignment: center; 119 - } 120 - } 145 + HorizontalLayout { 146 + padding-top: 12px; 147 + padding-bottom: 4px; 148 + spacing: 24px; 121 149 122 - PaperDivider { } 150 + SettingLabel { 151 + title: "Chime on break start"; 152 + description: "A single, gentle tone."; 153 + } 123 154 124 - HorizontalLayout { 125 - padding-top: 12px; 126 - padding-bottom: 4px; 127 - spacing: 24px; 155 + Rectangle { 156 + horizontal-stretch: 1; 157 + } 128 158 129 - SettingLabel { 130 - title: "Chime on break start"; 131 - description: "A single, gentle tone."; 159 + PaperToggle { 160 + checked <=> root.sound-enabled; 161 + label: "Chime on break start"; 162 + } 132 163 } 133 164 134 - Rectangle { horizontal-stretch: 1; } 165 + HorizontalLayout { 166 + padding-bottom: 12px; 167 + spacing: 12px; 135 168 136 - PaperToggle { checked <=> root.sound-enabled; label: "Chime on break start"; } 137 - } 138 - 139 - HorizontalLayout { 140 - padding-bottom: 12px; 141 - spacing: 12px; 169 + Text { 170 + text: "volume"; 171 + font-size: 12px; 172 + color: Theme.ink-mid; 173 + vertical-alignment: center; 174 + } 142 175 143 - Text { 144 - text: "volume"; 145 - font-size: 12px; 146 - color: Theme.ink-mid; 147 - vertical-alignment: center; 176 + VolumeSlider { 177 + value <=> root.sound-volume; 178 + horizontal-stretch: 1; 179 + } 148 180 } 149 181 150 - VolumeSlider { value <=> root.sound-volume; horizontal-stretch: 1; } 151 - } 182 + PaperDivider { } 183 + 184 + HorizontalLayout { 185 + padding-top: 12px; 186 + padding-bottom: 10px; 187 + spacing: 24px; 152 188 153 - PaperDivider { } 189 + SettingLabel { 190 + title: "Launch with system"; 191 + description: "ioma starts quietly at login."; 192 + } 154 193 155 - HorizontalLayout { 156 - padding-top: 12px; 157 - padding-bottom: 10px; 158 - spacing: 24px; 194 + Rectangle { 195 + horizontal-stretch: 1; 196 + } 159 197 160 - SettingLabel { 161 - title: "Launch with system"; 162 - description: "ioma starts quietly at login."; 198 + PaperToggle { 199 + checked <=> root.autostart; 200 + label: "Launch with system"; 201 + } 163 202 } 164 203 165 - Rectangle { horizontal-stretch: 1; } 204 + HorizontalLayout { 205 + padding-bottom: 4px; 166 206 167 - PaperToggle { checked <=> root.autostart; label: "Launch with system"; } 168 - } 207 + PaperButton { 208 + text: "Open config file location"; 209 + preferred-width: 180px; 210 + clicked => { 211 + root.open-config-dir(); 212 + } 213 + } 214 + } 169 215 170 - HorizontalLayout { 171 - padding-bottom: 4px; 216 + PaperDivider { } 172 217 173 - PaperButton { 174 - text: "Open config file location"; 175 - preferred-width: 180px; 176 - clicked => { root.open-config-dir(); } 177 - } 178 - } 218 + HorizontalLayout { 219 + padding-top: 12px; 220 + padding-bottom: 10px; 221 + spacing: 24px; 179 222 180 - PaperDivider { } 223 + SettingLabel { 224 + title: "Appearance"; 225 + description: "Follows system by default."; 226 + } 181 227 182 - HorizontalLayout { 183 - padding-top: 12px; 184 - padding-bottom: 10px; 185 - spacing: 24px; 228 + Rectangle { 229 + horizontal-stretch: 1; 230 + } 186 231 187 - SettingLabel { 188 - title: "Appearance"; 189 - description: "Follows system by default."; 232 + ChipGroup { 233 + labels: ["System", "Light", "Dark"]; 234 + selected-index <=> root.theme-mode; 235 + selection-changed(i) => { 236 + root.theme-mode-changed(i); 237 + } 238 + } 190 239 } 191 240 192 - Rectangle { horizontal-stretch: 1; } 193 - 194 - ChipGroup { 195 - labels: ["System", "Light", "Dark"]; 196 - selected-index <=> root.theme-mode; 197 - selection-changed(i) => { root.theme-mode-changed(i); } 241 + Rectangle { 242 + vertical-stretch: 1; 198 243 } 199 244 } 200 - 201 - Rectangle { vertical-stretch: 1; } 202 245 }