diff --git a/Cargo.lock b/Cargo.lock index 283cabe..7877dc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "aliveline" -version = "0.2.0" +version = "0.1.1" dependencies = [ "chrono", "serde", diff --git a/Cargo.toml b/Cargo.toml index 4780d86..49e34c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aliveline" -version = "0.2.0" +version = "0.1.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -15,4 +15,4 @@ toml = "0.9.5" slint-build = "1.12.1" [profile.release] -opt-level = 3 +opt-level = "s" diff --git a/README.md b/README.md index 563edcf..3a77de6 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,38 @@ -# Aliveline +# Slint Rust Template + +A template for a Rust application that's using [Slint](https://slint.rs/) for the user interface. ## About -Aliveline is a small app made with Rust + Slint to track daily activity on a timeline. -All activity is saved into TOML logs, which are human readable/editable. - -Aliveline currently supports Linux. - -## Features - -### Events -Events are main timeline building blocks. They have name, start and end. -Example of event in TOML format shown below: -```toml -[[events]] -name = "test" -start = 12:05:45 -end = 13:00:11 -finished = true -``` -_Note: if event is not finished yet, it may have_ `end = 00:00:00`. - -## Building -Requirements: -- Rust toolchain - -Instructions: -Run `cargo build --release` +This template helps you get started developing a Rust application with Slint as toolkit +for the user interface. It demonstrates the integration between the `.slint` UI markup and +Rust code, how to react to callbacks, get and set properties, and use basic widgets. ## Usage -Just run `aliveline` by any preferred way, for example: -``` -$ ./aliveline -``` +1. Install Rust by following its [getting-started guide](https://www.rust-lang.org/learn/get-started). + Once this is done, you should have the `rustc` compiler and the `cargo` build system installed in your `PATH`. +2. Download and extract the [ZIP archive of this repository](https://github.com/slint-ui/slint-rust-template/archive/refs/heads/main.zip). +3. Rename the extracted directory and change into it: + ``` + mv slint-rust-template-main my-project + cd my-project + ``` +4. Build with `cargo`: + ``` + cargo build + ``` +5. Run the application binary: + ``` + cargo run + ``` -## Configuration -Aliveline tries to find config at `$XDG_CONFIG_DIR/aliveline/config.toml`. -If config isn't found, or `$XDG_CONFIG_DIR` is not set, Aliveline uses default values. +We recommend using an IDE for development, along with our [LSP-based IDE integration for `.slint` files](https://github.com/slint-ui/slint/blob/master/tools/lsp/README.md). You can also load this project directly in [Visual Studio Code](https://code.visualstudio.com) and install our [Slint extension](https://marketplace.visualstudio.com/items?itemName=Slint.slint). -See the example [config.toml](http://2ndbeam.ru/git/2ndbeam/aliveline/src/branch/master/config.toml) for default values. +## Next Steps -## Contribution -You can contribute to Aliveline by creating issue on this repository, then we'll discuss it. +We hope that this template helps you get started, and that you enjoy exploring making user interfaces with Slint. To learn more +about the Slint APIs and the `.slint` markup language, check out our [online documentation](https://slint.dev/docs). + +Don't forget to edit this readme to replace it by yours, and edit the `name =` field in `Cargo.toml` to match the name of your +project. diff --git a/config.toml b/config.toml deleted file mode 100644 index 1bfb6b9..0000000 --- a/config.toml +++ /dev/null @@ -1,53 +0,0 @@ -# This is the default config for Aliveline. -# Note: All colors are of format 0xAARRGGBB - -# Path where logs are saved. May be relative to config dir or absolute. -log_path = "logs" - -# Colors used for events. For now Aliveline expects to have exactly 16 colors, but this is subject to change in future. -event_colors = [ - 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 -] - -# Colors used for event colors. Aliveline expects it to have same size as events. -text_colors = [ - 0xff_000000, - 0xff_000000, - 0xff_000000, - 0xff_000000, - 0xff_000000, - 0xff_ffffff, - 0xff_000000, - 0xff_000000, - 0xff_000000, - 0xff_ffffff, - 0xff_000000, - 0xff_000000, - 0xff_000000, - 0xff_000000, - 0xff_000000, - 0xff_000000 -] - -[colors] -# Color behind the timeline -background = 0xFF_808080 -# Color of the base timeline -timeline = 0xFF_a9a9a9 -# Color of background text (timestamps, event names, etc.) -background_text = 0xFF_000000 diff --git a/src/config.rs b/src/config.rs index e1afe8d..68afdf0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,138 +2,23 @@ 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 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()) - } - } + pub log_path: PathBuf } impl Config { pub fn new(conf_path: PathBuf) -> Self { let conf_dir: PathBuf = conf_path.parent().unwrap().into(); - Config { - conf_path: conf_dir, - ..Default::default() - } + Config { conf_path: conf_dir, log_path: PathBuf::from("./logs") } } 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(raw_conf) = conf { - let mut conf: Config = raw_conf.into(); + let conf = toml::from_str::(&toml_string); + if let Ok(mut conf) = conf { conf.conf_path = path.parent().unwrap().into(); return conf; } diff --git a/src/lib.rs b/src/lib.rs index 7eb4df8..bdf3eb3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ use config::Config; -use std::{hash::{DefaultHasher, Hash, Hasher}, path::PathBuf}; +use std::path::PathBuf; pub mod config; pub mod log; @@ -13,10 +13,3 @@ 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 9b1c5a0..3285d0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,9 @@ use std::{error::Error, rc::Rc, sync::{Arc, Mutex}}; -use aliveline::{color_id_from_name, config::Config, load_config, log::{Event, Log}}; +use aliveline::{config::Config, load_config, log::{Event, Log}}; use chrono::{Datelike, Timelike}; -use slint::{Color, Model, ModelRc, SharedString, ToSharedString, VecModel, Weak}; +use slint::{Model, ModelRc, SharedString, ToSharedString, VecModel, Weak}; use toml::value::{Date as TomlDate, Time}; slint::include_modules!(); @@ -22,8 +22,7 @@ impl From for TimelineEvent { start, duration: end - start, label: event.name.to_shared_string(), - finished: event.finished, - color_id: color_id_from_name(event.name) + finished: event.finished } } } @@ -61,26 +60,6 @@ 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()?; @@ -100,10 +79,6 @@ 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({ @@ -168,9 +143,8 @@ fn main() -> Result<(), Box> { let event = TimelineEvent { duration: 0, finished: false, - label: event_name.clone(), - start: offset, - color_id: color_id_from_name(event_name.to_string()) + label: event_name, + start: offset }; { @@ -203,9 +177,8 @@ fn main() -> Result<(), Box> { let new_event = TimelineEvent { duration: offset - event.start, finished: true, - label: event.label.clone(), - start: event.start, - color_id: color_id_from_name(event.label.to_string()) + label: event.label, + start: event.start }; { @@ -242,7 +215,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 7535149..373e060 100644 --- a/ui/app-window.slint +++ b/ui/app-window.slint @@ -2,7 +2,6 @@ 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; @@ -32,6 +31,7 @@ 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 4d7090c..d8a942a 100644 --- a/ui/record.slint +++ b/ui/record.slint @@ -13,29 +13,21 @@ export component RecordWidget inherits VerticalBox { in-out property events <=> tl.events; in property<[string]> combo-spans: []; in-out property in-progress: false; - property event-name: ""; - property minimized: false; - property combo-index: 0; + property event-name <=> le.text; + tl := Timeline { - preferred-height: 100%; updating: true; - clicked => { - minimized = !minimized; - } } - if !minimized: GridLayout { + GridLayout { spacing-vertical: 8px; spacing-horizontal: 16px; le := LineEdit { placeholder-text: "Event name"; - text: event-name; + text: "Event name"; font-size: 24px; horizontal-alignment: center; colspan: 2; row: 0; - edited(text) => { - event-name = text; - } } Button { text: in-progress ? "Stop" : "Start"; @@ -66,12 +58,11 @@ export component RecordWidget inherits VerticalBox { } ComboBox { model: combo-spans; - current-index: combo-index; + current-index: 0; row: 2; col: 1; selected(current-value) => { root.update-visible-time(current-value); - combo-index = self.current-index; } } } diff --git a/ui/theme.slint b/ui/theme.slint deleted file mode 100644 index 30b211a..0000000 --- a/ui/theme.slint +++ /dev/null @@ -1,43 +0,0 @@ -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 2e9db7f..66d571a 100644 --- a/ui/timeline.slint +++ b/ui/timeline.slint @@ -1,11 +1,8 @@ -import { Palette } from "theme.slint"; - export struct TimelineEvent { start: int, duration: int, finished: bool, - label: string, - color-id: int + label: string } global TimeString { @@ -28,8 +25,6 @@ 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: []; @@ -51,11 +46,7 @@ export component Timeline inherits Rectangle { } } - ta := TouchArea { - preferred-width: 100%; - preferred-height: 100%; - } - + background: gray; border-width: 1px; border-color: black; Rectangle { @@ -66,64 +57,40 @@ export component Timeline inherits Rectangle { height: parent.height / 2; border-color: black; border-width: 1px; - background: Palette.timeline; + background: purple; } 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 { property real-x: ((visible-time - (visible-offset - event.start)) / visible-time) * parent.width; - property real-width: event.duration / visible-time * parent.width + min(real-x, 0); x: max(real-x, 0); y: parent.height / 4; z: 1; width: event.finished ? - min(parent.width - self.x, real-width) : + min(parent.width - self.x, event.duration / visible-time * parent.width + min(real-x, 0)): parent.width - self.x; height: parent.height / 2; visible: self.width > 0 && self.real-x < parent.width; border-color: black; border-width: 1px; - background: Palette.event-colors[event.color-id]; - + background: red; + Text { x: 0; y: -self.height; text: event.label; visible: timeline-event.visible; - color: Palette.background-text; - } - start-txt := Text { - x: 0; - y: root.height - self.height - timeline-event.height; - text: timeline-event.x == timeline-event.real-x ? - TimeString.from(event.start) : - TimeString.from(visible-offset - visible-time); - 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; - y: root.height - self.height - timeline-event.height; - text: timeline-event.x + timeline-event.real-width <= root.width ? - 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