diff --git a/Cargo.toml b/Cargo.toml index 49e34c7..7675ffc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ toml = "0.9.5" slint-build = "1.12.1" [profile.release] -opt-level = "s" +opt-level = 3 diff --git a/src/config.rs b/src/config.rs index 68afdf0..e1afe8d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,23 +2,138 @@ use std::path::PathBuf; use serde::Deserialize; #[derive(Deserialize)] +struct RawColors { + pub background: Option, + pub timeline: Option, + pub background_text: Option +} + +#[derive(Clone)] +pub struct Colors { + pub background: u32, + pub timeline: u32, + pub background_text: u32 +} + +impl Default for Colors { + fn default() -> Self { + Colors { + background: 0xff_808080, + timeline: 0xff_a9a9a9, + background_text: 0xff_000000 + } + } +} + +impl From for Colors { + fn from(value: RawColors) -> Self { + let default_colors: Colors = Default::default(); + Colors { + background: value.background.unwrap_or(default_colors.background), + timeline: value.timeline.unwrap_or(default_colors.timeline), + background_text: value.background_text.unwrap_or(default_colors.background_text), + } + } +} + +#[derive(Deserialize)] +struct RawConfig { + pub log_path: Option, + pub colors: Option, + pub event_colors: Option>, + pub text_colors: Option> +} + pub struct Config { /// directory, where config is located - #[serde(skip)] pub conf_path: PathBuf, - pub log_path: PathBuf + pub log_path: PathBuf, + pub colors: Colors, + pub event_colors: Vec, + pub text_colors: Vec +} + +impl Default for Config { + fn default() -> Self { + let conf_path = PathBuf::new(); + let colors: Colors = Default::default(); + let event_colors: Vec = vec![ + 0xff_97f9f9, + 0xff_a4def9, + 0xff_c1e0f7, + 0xff_cfbae1, + 0xff_c59fc9, + 0xff_4e3d42, + 0xff_c9d5b5, + 0xff_2d82b7, + 0xff_556f44, + 0xff_772e25, + 0xff_c44536, + 0xff_7c6a0a, + 0xff_babd8d, + 0xff_ffdac6, + 0xff_fa9500, + 0xff_eb6424 + ]; + let text_colors: Vec = vec![ + 0xff000000, + 0xff000000, + 0xff000000, + 0xff000000, + 0xff000000, + 0xffffffff, + 0xff000000, + 0xff000000, + 0xff000000, + 0xffffffff, + 0xff000000, + 0xff000000, + 0xff000000, + 0xff000000, + 0xff000000, + 0xff000000 + ]; + Config { + conf_path, + log_path: PathBuf::from("./logs"), + colors, + event_colors, + text_colors + } + } +} + +impl From for Config { + fn from(value: RawConfig) -> Self { + let default_config: Config = Default::default(); + let colors: Colors = match value.colors { + Some(raw_colors) => raw_colors.into(), + None => default_config.colors.clone() + }; + Config { + conf_path: default_config.conf_path, + log_path: value.log_path.unwrap_or(default_config.log_path), + colors, + event_colors: value.event_colors.unwrap_or(default_config.event_colors.clone()), + text_colors: value.text_colors.unwrap_or(default_config.text_colors.clone()) + } + } } impl Config { pub fn new(conf_path: PathBuf) -> Self { let conf_dir: PathBuf = conf_path.parent().unwrap().into(); - Config { conf_path: conf_dir, log_path: PathBuf::from("./logs") } + Config { + conf_path: conf_dir, + ..Default::default() + } } pub fn load(path: PathBuf) -> Self { if let Ok(toml_string) = std::fs::read_to_string(path.clone()) { - let conf = toml::from_str::(&toml_string); - if let Ok(mut conf) = conf { + let conf = toml::from_str::(&toml_string); + if let Ok(raw_conf) = conf { + let mut conf: Config = raw_conf.into(); conf.conf_path = path.parent().unwrap().into(); return conf; } diff --git a/src/lib.rs b/src/lib.rs index bdf3eb3..7eb4df8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ use config::Config; -use std::path::PathBuf; +use std::{hash::{DefaultHasher, Hash, Hasher}, path::PathBuf}; pub mod config; pub mod log; @@ -13,3 +13,10 @@ pub fn load_config() -> Config { } Config::new(PathBuf::from("./config.toml")) } + +/// Get random-like color id in range 0..16 by computing string hash +pub fn color_id_from_name(name: String) -> i32 { + let mut s = DefaultHasher::new(); + name.hash(&mut s); + let hash = s.finish(); + (hash % 16) as i32 } diff --git a/src/main.rs b/src/main.rs index 3285d0f..9b1c5a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,9 @@ use std::{error::Error, rc::Rc, sync::{Arc, Mutex}}; -use aliveline::{config::Config, load_config, log::{Event, Log}}; +use aliveline::{color_id_from_name, config::Config, load_config, log::{Event, Log}}; use chrono::{Datelike, Timelike}; -use slint::{Model, ModelRc, SharedString, ToSharedString, VecModel, Weak}; +use slint::{Color, Model, ModelRc, SharedString, ToSharedString, VecModel, Weak}; use toml::value::{Date as TomlDate, Time}; slint::include_modules!(); @@ -22,7 +22,8 @@ impl From for TimelineEvent { start, duration: end - start, label: event.name.to_shared_string(), - finished: event.finished + finished: event.finished, + color_id: color_id_from_name(event.name) } } } @@ -60,6 +61,26 @@ fn load_log(ui_weak: Weak, log: Arc>) { ui.set_in_progress(in_progress); } +fn load_colors(ui_weak: Weak, config: Arc) { + let ui = ui_weak.unwrap(); + let pal = ui.global::(); + pal.set_background(Color::from_argb_encoded(config.colors.background)); + pal.set_timeline(Color::from_argb_encoded(config.colors.timeline)); + pal.set_background_text(Color::from_argb_encoded(config.colors.background_text)); + + // This looks like war crime + let event_colors_rc: ModelRc = Rc::new(VecModel::from( + config.event_colors.iter() + .map(|value| Color::from_argb_encoded(*value)).collect::>() + )).into(); + pal.set_event_colors(event_colors_rc); + let event_text_rc: ModelRc = Rc::new(VecModel::from( + config.text_colors.iter() + .map(|value| Color::from_argb_encoded(*value)).collect::>() + )).into(); + pal.set_event_text(event_text_rc); +} + fn main() -> Result<(), Box> { let ui = AppWindow::new()?; @@ -79,6 +100,10 @@ fn main() -> Result<(), Box> { let log = writing_log.clone(); load_log(ui_weak, log); + let ui_weak = ui.as_weak(); + let config_arc = config.clone(); + load_colors(ui_weak, config_arc); + ui.invoke_update_record_offset(offset as i32); ui.on_fetch_log({ @@ -143,8 +168,9 @@ fn main() -> Result<(), Box> { let event = TimelineEvent { duration: 0, finished: false, - label: event_name, - start: offset + label: event_name.clone(), + start: offset, + color_id: color_id_from_name(event_name.to_string()) }; { @@ -177,8 +203,9 @@ fn main() -> Result<(), Box> { let new_event = TimelineEvent { duration: offset - event.start, finished: true, - label: event.label, - start: event.start + label: event.label.clone(), + start: event.start, + color_id: color_id_from_name(event.label.to_string()) }; { @@ -215,7 +242,7 @@ fn main() -> Result<(), Box> { let maybe_unfinished_event = log_guard.events.iter().find(|event| !event.finished); match maybe_unfinished_event { Some(unfinished_event) => Some(Event::new(unfinished_event.name.clone(), 0, 0, false)), - None => None + _ => None } }; diff --git a/ui/app-window.slint b/ui/app-window.slint index 373e060..7535149 100644 --- a/ui/app-window.slint +++ b/ui/app-window.slint @@ -2,6 +2,7 @@ import { TabWidget } from "std-widgets.slint"; import { RecordWidget } from "record.slint"; import { ReviewWidget } from "review.slint"; import { TimelineEvent } from "timeline.slint"; +export { Palette } from "theme.slint"; export component AppWindow inherits Window { callback start-new-event <=> record.start-new-event; @@ -31,7 +32,6 @@ export component AppWindow inherits Window { property<[string]> combo-spans: ["1 Hour", "4 Hours", "8 Hours", "24 Hours"]; title: "Aliveline"; - TabWidget { Tab { title: "Record"; diff --git a/ui/record.slint b/ui/record.slint index 1dff7b4..4d7090c 100644 --- a/ui/record.slint +++ b/ui/record.slint @@ -16,7 +16,6 @@ export component RecordWidget inherits VerticalBox { property event-name: ""; property minimized: false; property combo-index: 0; - tl := Timeline { preferred-height: 100%; updating: true; diff --git a/ui/theme.slint b/ui/theme.slint new file mode 100644 index 0000000..30b211a --- /dev/null +++ b/ui/theme.slint @@ -0,0 +1,43 @@ +export global Palette { + in-out property background: gray; + in-out property timeline: darkgray; + in-out property background-text: black; + // Note: these colors were almost randomly picked + in-out property<[color]> event-colors: [ + #97f9f9, + #a4def9, + #c1e0f7, + #cfbae1, + #c59fc9, + #4e3d42, + #c9d5b5, + #2d82b7, + #556f44, + #772e25, + #c44536, + #7c6a0a, + #babd8d, + #ffdac6, + #fa9500, + #eb6424 + ]; + + in-out property <[color]> event-text: [ + #000000, + #000000, + #000000, + #000000, + #000000, + #ffffff, + #000000, + #000000, + #000000, + #ffffff, + #000000, + #000000, + #000000, + #000000, + #000000, + #000000 + ]; +} diff --git a/ui/timeline.slint b/ui/timeline.slint index c9fef81..2e9db7f 100644 --- a/ui/timeline.slint +++ b/ui/timeline.slint @@ -1,8 +1,11 @@ +import { Palette } from "theme.slint"; + export struct TimelineEvent { start: int, duration: int, finished: bool, - label: string + label: string, + color-id: int } global TimeString { @@ -26,6 +29,7 @@ global TimeString { export component Timeline inherits Rectangle { callback new-day-started; callback clicked <=> ta.clicked; + background: Palette.background; in-out property updating: true; in-out property<[TimelineEvent]> events: []; @@ -52,7 +56,6 @@ export component Timeline inherits Rectangle { preferred-height: 100%; } - background: gray; border-width: 1px; border-color: black; Rectangle { @@ -63,19 +66,21 @@ export component Timeline inherits Rectangle { height: parent.height / 2; border-color: black; border-width: 1px; - background: purple; + background: Palette.timeline; } Text { x: 0; y: parent.height - self.height; text: TimeString.from(visible-offset - visible-time); + color: Palette.background-text; } Text { x: parent.width - self.width; y: parent.height - self.height; text: TimeString.from(visible-offset); + color: Palette.background-text; } for event in events: timeline-event := Rectangle { @@ -91,13 +96,14 @@ export component Timeline inherits Rectangle { visible: self.width > 0 && self.real-x < parent.width; border-color: black; border-width: 1px; - background: red; + background: Palette.event-colors[event.color-id]; Text { x: 0; y: -self.height; text: event.label; visible: timeline-event.visible; + color: Palette.background-text; } start-txt := Text { x: 0; @@ -108,6 +114,7 @@ export component Timeline inherits Rectangle { visible: timeline-event.visible && (self.width * 2 < timeline-event.width || (!end-txt.visible && self.width < timeline-event.width)); + color: Palette.event-text[event.color-id]; } end-txt := Text { x: timeline-event.width - self.width; @@ -116,6 +123,7 @@ export component Timeline inherits Rectangle { TimeString.from(event.start + event.duration) : TimeString.from(visible-offset); visible: timeline-event.visible && timeline-event.width - self.width * 2 > 0; + color: Palette.event-text[event.color-id]; } } @children