Compare commits

..

12 commits

Author SHA1 Message Date
dc10194971 Bump version to 0.2.0 2025-09-19 00:33:23 +03:00
190ed0639a Added config example 2025-09-19 00:30:55 +03:00
ba976d9e12 Made every thing in Config optional internally 2025-09-19 00:10:20 +03:00
1b6f8ef282 Configurable colors 2025-09-18 17:33:34 +03:00
ca3c171698 Improved color picker algorithm 2025-09-18 16:35:30 +03:00
218ee49a8b Pseudo pseudorandom color picker 2025-09-18 16:30:37 +03:00
8df3893baa Minimizable record options 2025-09-15 17:26:04 +03:00
31281295bb Added event start/end timestamps 2025-09-15 16:54:05 +03:00
4650fde884 Updated Readme again again... 2025-09-12 15:22:43 +03:00
0c2a3d7e95 Updated Readme again 2025-09-12 15:21:41 +03:00
6a1f371f1d Updated Readme 2025-09-12 15:18:36 +03:00
b5e9a4115a Several bugfixes
- Fixed event invisibility when offset is after event's half width
- Fixed event being shown outside timeline component
- When timer reaches 24:00:00, new day is automatically started
2025-09-12 13:59:12 +03:00
12 changed files with 426 additions and 80 deletions

2
Cargo.lock generated
View file

@ -147,7 +147,7 @@ dependencies = [
[[package]] [[package]]
name = "aliveline" name = "aliveline"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde", "serde",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "aliveline" name = "aliveline"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # 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" slint-build = "1.12.1"
[profile.release] [profile.release]
opt-level = "s" opt-level = 3

View file

@ -1,38 +1,45 @@
# Slint Rust Template # Aliveline
A template for a Rust application that's using [Slint](https://slint.rs/) for the user interface.
## About ## About
This template helps you get started developing a Rust application with Slint as toolkit Aliveline is a small app made with Rust + Slint to track daily activity on a timeline.
for the user interface. It demonstrates the integration between the `.slint` UI markup and All activity is saved into TOML logs, which are human readable/editable.
Rust code, how to react to callbacks, get and set properties, and use basic widgets.
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`
## Usage ## Usage
1. Install Rust by following its [getting-started guide](https://www.rust-lang.org/learn/get-started). Just run `aliveline` by any preferred way, for example:
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). $ ./aliveline
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
```
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). ## 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.
## Next Steps See the example [config.toml](http://2ndbeam.ru/git/2ndbeam/aliveline/src/branch/master/config.toml) for default values.
We hope that this template helps you get started, and that you enjoy exploring making user interfaces with Slint. To learn more ## Contribution
about the Slint APIs and the `.slint` markup language, check out our [online documentation](https://slint.dev/docs). You can contribute to Aliveline by creating issue on this repository, then we'll discuss it.
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.

53
config.toml Normal file
View file

@ -0,0 +1,53 @@
# 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

View file

@ -2,23 +2,138 @@ use std::path::PathBuf;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
struct RawColors {
pub background: Option<u32>,
pub timeline: Option<u32>,
pub background_text: Option<u32>
}
#[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<RawColors> 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<PathBuf>,
pub colors: Option<RawColors>,
pub event_colors: Option<Vec<u32>>,
pub text_colors: Option<Vec<u32>>
}
pub struct Config { pub struct Config {
/// directory, where config is located /// directory, where config is located
#[serde(skip)]
pub conf_path: PathBuf, pub conf_path: PathBuf,
pub log_path: PathBuf pub log_path: PathBuf,
pub colors: Colors,
pub event_colors: Vec<u32>,
pub text_colors: Vec<u32>
}
impl Default for Config {
fn default() -> Self {
let conf_path = PathBuf::new();
let colors: Colors = Default::default();
let event_colors: Vec<u32> = 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<u32> = 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<RawConfig> 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 { impl Config {
pub fn new(conf_path: PathBuf) -> Self { pub fn new(conf_path: PathBuf) -> Self {
let conf_dir: PathBuf = conf_path.parent().unwrap().into(); 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 { pub fn load(path: PathBuf) -> Self {
if let Ok(toml_string) = std::fs::read_to_string(path.clone()) { if let Ok(toml_string) = std::fs::read_to_string(path.clone()) {
let conf = toml::from_str::<Self>(&toml_string); let conf = toml::from_str::<RawConfig>(&toml_string);
if let Ok(mut conf) = conf { if let Ok(raw_conf) = conf {
let mut conf: Config = raw_conf.into();
conf.conf_path = path.parent().unwrap().into(); conf.conf_path = path.parent().unwrap().into();
return conf; return conf;
} }

View file

@ -1,5 +1,5 @@
use config::Config; use config::Config;
use std::path::PathBuf; use std::{hash::{DefaultHasher, Hash, Hasher}, path::PathBuf};
pub mod config; pub mod config;
pub mod log; pub mod log;
@ -13,3 +13,10 @@ pub fn load_config() -> Config {
} }
Config::new(PathBuf::from("./config.toml")) 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 }

View file

@ -3,9 +3,9 @@
use std::{error::Error, rc::Rc, sync::{Arc, Mutex}}; 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 chrono::{Datelike, Timelike};
use slint::{Model, ModelRc, SharedString, ToSharedString, VecModel}; use slint::{Color, Model, ModelRc, SharedString, ToSharedString, VecModel, Weak};
use toml::value::{Date as TomlDate, Time}; use toml::value::{Date as TomlDate, Time};
slint::include_modules!(); slint::include_modules!();
@ -22,7 +22,8 @@ impl From<Event> for TimelineEvent {
start, start,
duration: end - start, duration: end - start,
label: event.name.to_shared_string(), label: event.name.to_shared_string(),
finished: event.finished finished: event.finished,
color_id: color_id_from_name(event.name)
} }
} }
} }
@ -46,6 +47,40 @@ impl From<TimelineEvent> for Event {
} }
} }
fn load_log(ui_weak: Weak<AppWindow>, log: Arc<Mutex<Log>>) {
let ui = ui_weak.unwrap();
let log_guard = log.lock().expect("Log shouldn't be used twice");
let events: Vec<TimelineEvent> = (*log_guard)
.events
.iter()
.map(|event| TimelineEvent::from((*event).clone()))
.collect();
let in_progress = events.iter().any(|event| !event.finished);
let model: ModelRc<TimelineEvent> = Rc::new(VecModel::from(events)).into();
ui.set_record_events(model);
ui.set_in_progress(in_progress);
}
fn load_colors(ui_weak: Weak<AppWindow>, config: Arc<Config>) {
let ui = ui_weak.unwrap();
let pal = ui.global::<Palette>();
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<Color> = Rc::new(VecModel::from(
config.event_colors.iter()
.map(|value| Color::from_argb_encoded(*value)).collect::<Vec<Color>>()
)).into();
pal.set_event_colors(event_colors_rc);
let event_text_rc: ModelRc<Color> = Rc::new(VecModel::from(
config.text_colors.iter()
.map(|value| Color::from_argb_encoded(*value)).collect::<Vec<Color>>()
)).into();
pal.set_event_text(event_text_rc);
}
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
let ui = AppWindow::new()?; let ui = AppWindow::new()?;
@ -61,23 +96,13 @@ fn main() -> Result<(), Box<dyn Error>> {
let config: Arc<Config> = Arc::new(load_config()); let config: Arc<Config> = Arc::new(load_config());
let writing_log: Arc<Mutex<Log>> = Arc::new(Mutex::new(Log::load_from(&config, date))); let writing_log: Arc<Mutex<Log>> = Arc::new(Mutex::new(Log::load_from(&config, date)));
{ let ui_weak = ui.as_weak();
let ui_weak = ui.as_weak(); let log = writing_log.clone();
let log = writing_log.clone(); load_log(ui_weak, log);
(move || {
let ui = ui_weak.unwrap(); let ui_weak = ui.as_weak();
let log_guard = log.lock().expect("Log shouldn't be used twice"); let config_arc = config.clone();
let events: Vec<TimelineEvent> = (*log_guard) load_colors(ui_weak, config_arc);
.events
.iter()
.map(|event| TimelineEvent::from((*event).clone()))
.collect();
let in_progress = events.iter().any(|event| !event.finished);
let model: ModelRc<TimelineEvent> = Rc::new(VecModel::from(events)).into();
ui.set_record_events(model);
ui.set_in_progress(in_progress);
})()
}
ui.invoke_update_record_offset(offset as i32); ui.invoke_update_record_offset(offset as i32);
@ -143,8 +168,9 @@ fn main() -> Result<(), Box<dyn Error>> {
let event = TimelineEvent { let event = TimelineEvent {
duration: 0, duration: 0,
finished: false, finished: false,
label: event_name, label: event_name.clone(),
start: offset start: offset,
color_id: color_id_from_name(event_name.to_string())
}; };
{ {
@ -165,7 +191,8 @@ fn main() -> Result<(), Box<dyn Error>> {
let ui = ui_weak.unwrap(); let ui = ui_weak.unwrap();
let events_rc = ui.get_record_events(); let events_rc = ui.get_record_events();
let events = events_rc.as_any() let events = events_rc.as_any()
.downcast_ref::<VecModel<TimelineEvent>>().unwrap(); .downcast_ref::<VecModel<TimelineEvent>>()
.unwrap();
let offset = ui.get_record_offset(); let offset = ui.get_record_offset();
let event_id = events.iter() let event_id = events.iter()
@ -176,15 +203,16 @@ fn main() -> Result<(), Box<dyn Error>> {
let new_event = TimelineEvent { let new_event = TimelineEvent {
duration: offset - event.start, duration: offset - event.start,
finished: true, finished: true,
label: event.label, label: event.label.clone(),
start: event.start start: event.start,
color_id: color_id_from_name(event.label.to_string())
}; };
{ {
let mut log_guard = log.lock().expect("Log shouldn't be used twice"); let mut log_guard = log.lock().expect("Log shouldn't be used twice");
(*log_guard).events.push(Event::from(new_event.clone())); log_guard.events.push(Event::from(new_event.clone()));
let index = (*log_guard).events.iter().position(|data| !data.finished).unwrap(); let index = log_guard.events.iter().position(|data| !data.finished).unwrap();
(*log_guard).events.swap_remove(index); log_guard.events.swap_remove(index);
} }
ui.invoke_save_log(); ui.invoke_save_log();
@ -202,6 +230,47 @@ fn main() -> Result<(), Box<dyn Error>> {
} }
}); });
ui.on_new_day_started({
let ui_weak = ui.as_weak();
let log = writing_log.clone();
move || {
let ui = ui_weak.unwrap();
let new_event: Option<Event> = {
let log_guard = log.lock().expect("Log shouldn't be used twice");
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
}
};
ui.invoke_stop_event();
{
let mut log_guard = log.lock().expect("Log shouldn't be used twice");
log_guard.events.clear();
let now = chrono::Local::now();
let date = TomlDate {
year: now.year() as u16,
month: now.month() as u8,
day: now.day() as u8
};
log_guard.date = date;
log_guard.events.clear();
if let Some(event) = new_event {
log_guard.events.push(event);
}
}
load_log(ui.as_weak(), log.clone());
ui.invoke_save_log();
}
});
ui.run()?; ui.run()?;

View file

@ -2,11 +2,13 @@ import { TabWidget } from "std-widgets.slint";
import { RecordWidget } from "record.slint"; import { RecordWidget } from "record.slint";
import { ReviewWidget } from "review.slint"; import { ReviewWidget } from "review.slint";
import { TimelineEvent } from "timeline.slint"; import { TimelineEvent } from "timeline.slint";
export { Palette } from "theme.slint";
export component AppWindow inherits Window { export component AppWindow inherits Window {
callback start-new-event <=> record.start-new-event; callback start-new-event <=> record.start-new-event;
callback stop-event <=> record.stop-event; callback stop-event <=> record.stop-event;
callback chain-event <=> record.chain-event; callback chain-event <=> record.chain-event;
callback new-day-started <=> record.new-day-started;
callback update-record-offset(int); callback update-record-offset(int);
callback save-log; callback save-log;
@ -30,7 +32,6 @@ export component AppWindow inherits Window {
property<[string]> combo-spans: ["1 Hour", "4 Hours", "8 Hours", "24 Hours"]; property<[string]> combo-spans: ["1 Hour", "4 Hours", "8 Hours", "24 Hours"];
title: "Aliveline"; title: "Aliveline";
TabWidget { TabWidget {
Tab { Tab {
title: "Record"; title: "Record";

View file

@ -2,6 +2,7 @@ import { VerticalBox, LineEdit, Button, ComboBox } from "std-widgets.slint";
import { Timeline } from "timeline.slint"; import { Timeline } from "timeline.slint";
export component RecordWidget inherits VerticalBox { export component RecordWidget inherits VerticalBox {
callback new-day-started <=> tl.new-day-started;
callback update-visible-time(string); callback update-visible-time(string);
callback start-new-event(string); callback start-new-event(string);
callback chain-event(string); callback chain-event(string);
@ -12,21 +13,29 @@ export component RecordWidget inherits VerticalBox {
in-out property events <=> tl.events; in-out property events <=> tl.events;
in property<[string]> combo-spans: []; in property<[string]> combo-spans: [];
in-out property<bool> in-progress: false; in-out property<bool> in-progress: false;
property<string> event-name <=> le.text; property<string> event-name: "";
property<bool> minimized: false;
property<int> combo-index: 0;
tl := Timeline { tl := Timeline {
preferred-height: 100%;
updating: true; updating: true;
clicked => {
minimized = !minimized;
}
} }
GridLayout { if !minimized: GridLayout {
spacing-vertical: 8px; spacing-vertical: 8px;
spacing-horizontal: 16px; spacing-horizontal: 16px;
le := LineEdit { le := LineEdit {
placeholder-text: "Event name"; placeholder-text: "Event name";
text: "Event name"; text: event-name;
font-size: 24px; font-size: 24px;
horizontal-alignment: center; horizontal-alignment: center;
colspan: 2; colspan: 2;
row: 0; row: 0;
edited(text) => {
event-name = text;
}
} }
Button { Button {
text: in-progress ? "Stop" : "Start"; text: in-progress ? "Stop" : "Start";
@ -57,11 +66,12 @@ export component RecordWidget inherits VerticalBox {
} }
ComboBox { ComboBox {
model: combo-spans; model: combo-spans;
current-index: 0; current-index: combo-index;
row: 2; row: 2;
col: 1; col: 1;
selected(current-value) => { selected(current-value) => {
root.update-visible-time(current-value); root.update-visible-time(current-value);
combo-index = self.current-index;
} }
} }
} }

View file

@ -22,7 +22,7 @@ export component ReviewWidget inherits VerticalBox {
spacing-horizontal: 16px; spacing-horizontal: 16px;
Slider { Slider {
minimum: visible-time; minimum: visible-time;
maximum: 24 * 3600; maximum: tl.max-offset;
value: offset; value: offset;
row: 0; row: 0;
colspan: 2; colspan: 2;

43
ui/theme.slint Normal file
View file

@ -0,0 +1,43 @@
export global Palette {
in-out property<color> background: gray;
in-out property<color> timeline: darkgray;
in-out property<color> 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
];
}

View file

@ -1,8 +1,11 @@
import { Palette } from "theme.slint";
export struct TimelineEvent { export struct TimelineEvent {
start: int, start: int,
duration: int, duration: int,
finished: bool, finished: bool,
label: string label: string,
color-id: int
} }
global TimeString { global TimeString {
@ -24,21 +27,35 @@ global TimeString {
} }
export component Timeline inherits Rectangle { export component Timeline inherits Rectangle {
callback new-day-started;
callback clicked <=> ta.clicked;
background: Palette.background;
in-out property<bool> updating: true; in-out property<bool> updating: true;
in-out property<[TimelineEvent]> events: []; in-out property<[TimelineEvent]> events: [];
in-out property<int> visible-time: 3600; in-out property<int> visible-time: 3600;
property<int> visible-offset: max(offset, visible-time); property<int> visible-offset: max(offset, visible-time);
in-out property<int> offset: 0; in-out property<int> offset: 0;
out property<int> max-offset: 24 * 3600 - 1;
timer := Timer { timer := Timer {
interval: 1s; interval: 1s;
running: updating; running: updating;
triggered => { triggered => {
if (offset >= max-offset) {
root.new-day-started();
offset = 0;
return;
}
offset += 1; offset += 1;
} }
} }
background: gray; ta := TouchArea {
preferred-width: 100%;
preferred-height: 100%;
}
border-width: 1px; border-width: 1px;
border-color: black; border-color: black;
Rectangle { Rectangle {
@ -49,40 +66,64 @@ export component Timeline inherits Rectangle {
height: parent.height / 2; height: parent.height / 2;
border-color: black; border-color: black;
border-width: 1px; border-width: 1px;
background: purple; background: Palette.timeline;
} }
Text { Text {
x: 0; x: 0;
y: parent.height - self.height; y: parent.height - self.height;
text: TimeString.from(visible-offset - visible-time); text: TimeString.from(visible-offset - visible-time);
color: Palette.background-text;
} }
Text { Text {
x: parent.width - self.width; x: parent.width - self.width;
y: parent.height - self.height; y: parent.height - self.height;
text: TimeString.from(visible-offset); text: TimeString.from(visible-offset);
color: Palette.background-text;
} }
for event in events: timeline-event := Rectangle { for event in events: timeline-event := Rectangle {
property<length> real-x: ((visible-time - (visible-offset - event.start)) / visible-time) * parent.width; property<length> real-x: ((visible-time - (visible-offset - event.start)) / visible-time) * parent.width;
property<length> real-width: event.duration / visible-time * parent.width + min(real-x, 0);
x: max(real-x, 0); x: max(real-x, 0);
y: parent.height / 4; y: parent.height / 4;
z: 1; z: 1;
width: event.finished ? width: event.finished ?
(event.duration) / visible-time * parent.width + min(real-x, 0): min(parent.width - self.x, real-width) :
parent.width - self.x; parent.width - self.x;
height: parent.height / 2; height: parent.height / 2;
visible: self.real-x + self.width > 0 && self.real-x < parent.width; visible: self.width > 0 && self.real-x < parent.width;
border-color: black; border-color: black;
border-width: 1px; border-width: 1px;
background: red; background: Palette.event-colors[event.color-id];
Text { Text {
x: 0; x: 0;
y: -self.height; y: -self.height;
text: event.label; text: event.label;
visible: timeline-event.visible; 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 @children