diff --git a/build.rs b/build.rs index 9bc3037..ce86137 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,6 @@ fn main() { - slint_build::compile("ui/app-window.slint").expect("Slint build failed"); + let config = slint_build::CompilerConfiguration::new() + .with_style("cosmic".into()); + + slint_build::compile_with_config("ui/app-window.slint", config).expect("Slint build failed"); } diff --git a/src/main.rs b/src/main.rs index f466a39..dd5c9a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,9 +8,6 @@ use chrono::{Datelike, Timelike}; use slint::{Color, Model, ModelRc, SharedString, ToSharedString, VecModel, Weak}; use toml::value::{Date as TomlDate, Time}; -const DEFAULT_WINDOW_WIDTH: f32 = 800.; -const DEFAULT_WINDOW_HEIGHT: f32 = 600.; - slint::include_modules!(); impl From for TimelineEvent { @@ -60,7 +57,9 @@ fn load_log(ui_weak: Weak, log: Arc>) { .collect(); let in_progress = events.iter().any(|event| !event.finished); let model: ModelRc = Rc::new(VecModel::from(events)).into(); - ui.set_record_events(model); + let mut state = ui.get_record_state(); + state.events = model; + ui.set_record_state(state); ui.set_in_progress(in_progress); ui.invoke_get_previous_event(); } @@ -109,7 +108,6 @@ fn main() -> Result<(), Box> { load_colors(ui_weak, config_arc); ui.invoke_update_record_offset(offset as i32); - ui.invoke_update_window_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT); ui.on_fetch_log({ let config = config.clone(); @@ -127,7 +125,9 @@ fn main() -> Result<(), Box> { .map(|event| TimelineEvent::from((*event).clone())) .collect(); let model: ModelRc = Rc::new(VecModel::from(events)).into(); - ui.set_review_events(model); + let mut state = ui.get_review_state(); + state.events = model; + ui.set_review_state(state); } }); @@ -151,10 +151,14 @@ fn main() -> Result<(), Box> { .map(|h| h.parse::().unwrap()) .unwrap(); if is_record { - ui.set_record_visible_time(hours * 3600); + let mut state = ui.get_record_state(); + state.visible_time = hours * 3600; + ui.set_record_state(state); } else { - ui.set_review_visible_time(hours * 3600); - } + let mut state = ui.get_review_state(); + state.visible_time = hours * 3600; + ui.set_review_state(state); + }; } }); @@ -163,18 +167,17 @@ fn main() -> Result<(), Box> { let log = writing_log.clone(); move |event_name: SharedString| { let ui = ui_weak.unwrap(); - - let events_rc = ui.get_record_events(); - let events = events_rc.as_any() + + let state = ui.get_record_state(); + let events = state.events.as_any() .downcast_ref::>() .unwrap(); - let offset = ui.get_record_offset(); let event = TimelineEvent { duration: 0, finished: false, label: event_name.clone(), - start: offset, + start: state.offset, color_id: color_id_from_name(event_name.to_string()) }; @@ -194,11 +197,10 @@ fn main() -> Result<(), Box> { let log = writing_log.clone(); move || { let ui = ui_weak.unwrap(); - let events_rc = ui.get_record_events(); - let events = events_rc.as_any() + let state = ui.get_record_state(); + let events = state.events.as_any() .downcast_ref::>() .unwrap(); - let offset = ui.get_record_offset(); let event_id = events.iter() .position(|data| !data.finished) @@ -206,7 +208,7 @@ fn main() -> Result<(), Box> { let event = events.row_data(event_id) .expect("stop-event called without unfinished events"); let new_event = TimelineEvent { - duration: offset - event.start, + duration: state.offset - event.start, finished: true, label: event.label.clone(), start: event.start, diff --git a/ui/app-window.slint b/ui/app-window.slint index 311de17..fc0f984 100644 --- a/ui/app-window.slint +++ b/ui/app-window.slint @@ -1,61 +1,104 @@ -import { TabWidget } from "std-widgets.slint"; +import { TabWidget, VerticalBox, ComboBox } from "std-widgets.slint"; import { RecordWidget } from "record.slint"; import { ReviewWidget } from "review.slint"; -import { TimelineEvent } from "timeline.slint"; +import { TimelineEvent, Timeline, TimelineState } from "timeline.slint"; +import { Const } from "global.slint"; export { Palette } from "theme.slint"; export component AppWindow inherits Window { + callback update-record-offset(int); + callback save-log; + callback new-day-started(); + + update-record-offset(new-offset) => { + record-state.offset = new-offset; + } + + preferred-width: 800px; + preferred-height: 600px; + max-width: 2147483647px; + callback start-new-event <=> record.start-new-event; callback stop-event <=> record.stop-event; callback chain-event <=> record.chain-event; - callback new-day-started <=> record.new-day-started; callback get-previous-event <=> record.get-previous-event; - callback update-record-offset(int); - callback update-window-size(length, length); - callback save-log; callback fetch-log <=> review.fetch-log; callback update-visible-time(bool, string); - - update-record-offset(new-offset) => { - record.offset = new-offset; - } - - update-window-size(width, height) => { - self.width = width; - self.height = height; - } - - in-out property record-events <=> record.events; - in-out property record-offset <=> record.offset; - in-out property record-visible-time <=> record.visible-time; - in-out property in-progress <=> record.in-progress; - in property previous-event-name <=> record.previous-event-name; - in-out property review-events <=> review.events; - in-out property review-offset <=> review.offset; - in-out property review-visible-time <=> review.visible-time; + in-out property record-state <=> record.state; + + in-out property review-state <=> review.state; + + in-out property in-progress <=> record.in-progress; + + in property previous-event-name <=> record.previous-event-name; property<[string]> combo-spans: ["1 Hour", "4 Hours", "8 Hours", "24 Hours"]; + property minimized: false; + property in-record-mode: true; + + init => { + record-state.visible-time = 3600; + review-state.visible-time = 3600; + } + + Timer { + interval: 1s; + running: true; + triggered => { + if (record-state.offset >= Const.max-offset) { + root.new-day-started(); + record-state.offset = 0; + return; + } + record-state.offset += 1; + } + } title: "Aliveline"; - TabWidget { - Tab { - title: "Record"; - record := RecordWidget { - combo-spans: combo-spans; - update-visible-time(time) => { - root.update-visible-time(true, time); - } + VerticalLayout { + width: 100%; + height: 100%; + tl := Timeline { + preferred-height: 100%; + state: in-record-mode ? record-state : review-state; + clicked => { + minimized = !minimized; } } - Tab { - title: "Review"; - review := ReviewWidget { - combo-spans: combo-spans; - update-visible-time(time) => { - root.update-visible-time(false, time) + spacing: minimized ? 0 : 8px; + record := RecordWidget { + combo-spans: combo-spans; + update-visible-time(time) => { + root.update-visible-time(true, time); + } + minimized: minimized || !in-record-mode; + } + review := ReviewWidget { + combo-spans: combo-spans; + update-visible-time(time) => { + root.update-visible-time(false, time) + } + minimized: minimized; + is-active: !in-record-mode; + } + if !minimized: HorizontalLayout { + padding-left: 8px; + padding-right: 8px; + padding-bottom: 8px; + spacing: 16px; + Text { + text: "Mode:"; + font-size: 24px; + horizontal-alignment: right; + } + ComboBox { + model: ["Record", "Review"]; + current-index: in-record-mode ? 0 : 1; + selected(current-value) => { + in-record-mode = current-value == "Record"; } } } diff --git a/ui/global.slint b/ui/global.slint new file mode 100644 index 0000000..3297fae --- /dev/null +++ b/ui/global.slint @@ -0,0 +1,21 @@ +export global TimeString { + pure function pad-mh(seconds: int, param: int) -> string { + if seconds / param < 10 { + return "0\{floor(seconds / param)}"; + } + return "\{floor(seconds / param)}"; + } + pure function pad-s(seconds: int) -> string { + if mod(seconds, 60) < 10 { + return "0\{mod(seconds, 60)}"; + } + return "\{mod(seconds, 60)}"; + } + public pure function from(seconds: int) -> string { + return "\{pad-mh(seconds, 3600)}:\{pad-mh(mod(seconds, 3600), 60)}:\{pad-s(seconds)}"; + } +} + +export global Const { + out property max-offset: 24 * 3600 - 1; +} diff --git a/ui/record.slint b/ui/record.slint index 0f0c8e5..1d9e810 100644 --- a/ui/record.slint +++ b/ui/record.slint @@ -1,33 +1,25 @@ import { VerticalBox, LineEdit, Button, ComboBox } from "std-widgets.slint"; -import { Timeline } from "timeline.slint"; +import { Timeline, TimelineState } from "timeline.slint"; -export component RecordWidget inherits VerticalBox { - callback new-day-started <=> tl.new-day-started; +export component RecordWidget inherits VerticalLayout { callback update-visible-time(string); callback start-new-event(string); callback chain-event(string); callback stop-event; callback get-previous-event(); - in-out property visible-time <=> tl.visible-time; - in-out property updating <=> tl.updating; - in-out property offset <=> tl.offset; - in-out property events <=> tl.events; + + in-out property state; in property<[string]> combo-spans: []; in-out property in-progress: false; property event-name: ""; in property previous-event-name: ""; - property minimized: false; property combo-index: 0; - tl := Timeline { - preferred-height: 100%; - updating: true; - clicked => { - minimized = !minimized; - } - } + in-out property minimized; + if !minimized: GridLayout { spacing-vertical: 8px; spacing-horizontal: 16px; + padding: 8px; le := LineEdit { placeholder-text: "Event name"; text: event-name; @@ -56,7 +48,7 @@ export component RecordWidget inherits VerticalBox { Button { text: "Chain"; enabled: in-progress; - col: 3; + col: 2; row: 1; colspan: 2; clicked => { @@ -67,7 +59,7 @@ export component RecordWidget inherits VerticalBox { Button { text: previous-event-name == "" ? "Chain previous event (None)" : "Chain previous event (\{previous-event-name})"; enabled: in-progress && previous-event-name != ""; - col: 5; + col: 4; row: 1; colspan: 2; clicked => { @@ -77,7 +69,7 @@ export component RecordWidget inherits VerticalBox { } } Text { - text: "Span:"; + text: "Span: "; font-size: 24px; row: 2; colspan: 3; diff --git a/ui/review.slint b/ui/review.slint index 14727ce..e0d59cd 100644 --- a/ui/review.slint +++ b/ui/review.slint @@ -1,62 +1,58 @@ import { VerticalBox, LineEdit, Button, DatePickerPopup, ComboBox, Slider } from "std-widgets.slint"; -import { Timeline } from "timeline.slint"; +import { Timeline, TimelineState } from "timeline.slint"; +import { Const } from "global.slint"; -export component ReviewWidget inherits VerticalBox { +export component ReviewWidget inherits VerticalLayout { callback update-visible-time(string); callback fetch-log(int, int, int); - property max-offset: 24 * 3600; property current-year; property current-month; property current-day; in property<[string]> combo-spans: []; - in-out property visible-time <=> tl.visible-time; - in-out property offset <=> tl.offset; - in-out property events <=> tl.events; + in-out property state; + in-out property minimized; + in-out property is-active; - tl := Timeline { - updating: false; - } - GridLayout { - spacing-vertical: 8px; - spacing-horizontal: 16px; + if is-active: VerticalLayout { + spacing: 8px; Slider { - minimum: visible-time; - maximum: tl.max-offset; - value: offset; - row: 0; - colspan: 2; + minimum: state.visible-time; + maximum: Const.max-offset; + value: state.offset; changed(value) => { - offset = value; + state.offset = value; } } - Text { - text: "Day: \{current-day}/\{current-month}/\{current-year}"; - font-size: 32px; - horizontal-alignment: right; - row: 1; - } - Button { - text: "Select"; - clicked => { - date-picker.show() + if !minimized: GridLayout { + spacing-horizontal: 16px; + padding: 8px; + Text { + text: "Day: \{current-day}/\{current-month}/\{current-year}"; + font-size: 24px; + horizontal-alignment: right; } - row: 1; - col: 1; - } - Text { - text: "Span: "; - font-size: 24px; - row: 2; - horizontal-alignment: right; - } - ComboBox { - model: combo-spans; - current-index: 0; - row: 2; - col: 1; - selected(current-value) => { - root.update-visible-time(current-value); + Button { + text: "Select"; + clicked => { + date-picker.show() + } + col: 1; + } + Text { + text: "Span: "; + font-size: 24px; + row: 1; + horizontal-alignment: right; + } + ComboBox { + model: combo-spans; + current-index: 0; + row: 1; + col: 1; + selected(current-value) => { + root.update-visible-time(current-value); + } } } } diff --git a/ui/timeline.slint b/ui/timeline.slint index 2e9db7f..1fa828b 100644 --- a/ui/timeline.slint +++ b/ui/timeline.slint @@ -1,55 +1,28 @@ import { Palette } from "theme.slint"; +import { TimeString, Const } from "global.slint"; export struct TimelineEvent { start: int, duration: int, finished: bool, label: string, - color-id: int + color-id: int, } -global TimeString { - pure function pad-mh(seconds: int, param: int) -> string { - if seconds / param < 10 { - return "0\{floor(seconds / param)}"; - } - return "\{floor(seconds / param)}"; - } - pure function pad-s(seconds: int) -> string { - if mod(seconds, 60) < 10 { - return "0\{mod(seconds, 60)}"; - } - return "\{mod(seconds, 60)}"; - } - public pure function from(seconds: int) -> string { - return "\{pad-mh(seconds, 3600)}:\{pad-mh(mod(seconds, 3600), 60)}:\{pad-s(seconds)}"; - } +export struct TimelineState { + visible-time: int, + offset: int, + events: [TimelineEvent], } 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: []; - in-out property visible-time: 3600; - property visible-offset: max(offset, visible-time); - in-out property offset: 0; - out property max-offset: 24 * 3600 - 1; - - timer := Timer { - interval: 1s; - running: updating; - triggered => { - if (offset >= max-offset) { - root.new-day-started(); - offset = 0; - return; - } - offset += 1; - } - } + + in-out property state; + property visible-offset: max(state.offset, state.visible-time); + out property max-offset: Const.max-offset; ta := TouchArea { preferred-width: 100%; @@ -72,7 +45,7 @@ export component Timeline inherits Rectangle { Text { x: 0; y: parent.height - self.height; - text: TimeString.from(visible-offset - visible-time); + text: TimeString.from(visible-offset - state.visible-time); color: Palette.background-text; } @@ -83,9 +56,9 @@ export component Timeline inherits Rectangle { 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); + for event in state.events: timeline-event := Rectangle { + property real-x: ((state.visible-time - (visible-offset - event.start)) / state.visible-time) * parent.width; + property real-width: event.duration / state.visible-time * parent.width + min(real-x, 0); x: max(real-x, 0); y: parent.height / 4; z: 1; @@ -110,7 +83,7 @@ export component Timeline inherits Rectangle { 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); + TimeString.from(visible-offset - state.visible-time); visible: timeline-event.visible && (self.width * 2 < timeline-event.width || (!end-txt.visible && self.width < timeline-event.width));