Compare commits
2 Commits
68e8205276
...
99a436571b
| Author | SHA1 | Date | |
|---|---|---|---|
|
99a436571b
|
|||
|
ab01696537
|
845
Cargo.lock
generated
845
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,12 +18,12 @@ tracing = "0.1.44"
|
|||||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||||
|
|
||||||
[dependencies.slint]
|
[dependencies.slint]
|
||||||
version = "1.15.1"
|
version = "1.16.1"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["backend-winit", "compat-1-2", "live-preview", "renderer-femtovg", "std"]
|
features = ["backend-winit", "compat-1-2", "live-preview", "renderer-femtovg", "std"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
slint-build = "1.15.1"
|
slint-build = "1.16.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|||||||
12
src/api.rs
12
src/api.rs
@@ -162,6 +162,10 @@ impl Message<GetAssetThumbnail> for Api {
|
|||||||
msg: GetAssetThumbnail,
|
msg: GetAssetThumbnail,
|
||||||
_ctx: &mut Context<Self, Self::Reply>,
|
_ctx: &mut Context<Self, Self::Reply>,
|
||||||
) -> Self::Reply {
|
) -> Self::Reply {
|
||||||
|
if let Some(thumbnail) = self.thumbnails.get(&msg.id).cloned() {
|
||||||
|
return Ok(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.client
|
.client
|
||||||
.assets()
|
.assets()
|
||||||
@@ -182,9 +186,13 @@ impl Message<GetAssetThumbnail> for Api {
|
|||||||
thumbnail.height(),
|
thumbnail.height(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Arc::new(AssetThumbnail {
|
let thumbnail = Arc::new(AssetThumbnail {
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
thumbnail: pixel_buffer,
|
thumbnail: pixel_buffer,
|
||||||
}))
|
});
|
||||||
|
|
||||||
|
self.thumbnails.insert(msg.id, Arc::clone(&thumbnail));
|
||||||
|
|
||||||
|
Ok(thumbnail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
198
src/main.rs
198
src/main.rs
@@ -1,6 +1,8 @@
|
|||||||
// Prevent console window in addition to Slint window in Windows release builds when, e.g., starting the app via file manager. Ignored on other platforms.
|
// Prevent console window in addition to Slint window in Windows release builds when, e.g., starting the app via file manager. Ignored on other platforms.
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use std::{mem, ops::Deref};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use immich_sdk::AssetId;
|
use immich_sdk::AssetId;
|
||||||
use kameo::actor::{ActorRef, Spawn};
|
use kameo::actor::{ActorRef, Spawn};
|
||||||
@@ -12,7 +14,7 @@ use tracing::Level;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{Api, GetAssetThumbnail, GetTimeBucket, GetTimeBuckets, TimeBucketKey},
|
api::{Api, GetAssetThumbnail, GetTimeBucket, GetTimeBuckets, TimeBucketKey},
|
||||||
ui::AppWindow,
|
ui::{AppWindow, ImageBucket},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
@@ -68,6 +70,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let api_ = Api::spawn(api);
|
let api_ = Api::spawn(api);
|
||||||
|
|
||||||
let app = ui::AppWindow::new()?;
|
let app = ui::AppWindow::new()?;
|
||||||
|
let global = app.global::<ui::Global>();
|
||||||
|
|
||||||
let app_weak = app.as_weak();
|
let app_weak = app.as_weak();
|
||||||
let api = api_.clone();
|
let api = api_.clone();
|
||||||
@@ -76,17 +79,9 @@ fn main() -> anyhow::Result<()> {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
for bucket in buckets.iter() {
|
let api = api.clone();
|
||||||
load_bucket(bucket.key.clone(), app_weak.clone(), api.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = app_weak.upgrade_in_event_loop(move |app| {
|
let _ = app_weak.upgrade_in_event_loop(move |app| {
|
||||||
let preview_image = slint::Image::from_rgb8(SharedPixelBuffer::clone_from_slice(
|
let preview_image = placeholder_preview();
|
||||||
&[0x33, 0x33, 0x33], // a single dark gray pixel
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
));
|
|
||||||
|
|
||||||
let global = app.global::<ui::Global>();
|
let global = app.global::<ui::Global>();
|
||||||
let buckets = buckets
|
let buckets = buckets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -103,29 +98,173 @@ fn main() -> anyhow::Result<()> {
|
|||||||
})
|
})
|
||||||
.collect::<VecModel<_>>(),
|
.collect::<VecModel<_>>(),
|
||||||
),
|
),
|
||||||
|
y: Default::default(),
|
||||||
|
height: Default::default(),
|
||||||
|
visibility: ui::Visibility::Hidden,
|
||||||
})
|
})
|
||||||
.collect::<VecModel<_>>();
|
.collect::<VecModel<_>>();
|
||||||
global.set_image_buckets(ModelRc::new(buckets));
|
global.set_image_buckets(ModelRc::new(buckets));
|
||||||
|
calculate_timeline_layout(&app, api, global.get_timeline_width());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIXME: visibility-based callbacks are broken
|
let app_weak = app.as_weak();
|
||||||
// let app_weak = app.as_weak();
|
let api = api_.clone();
|
||||||
// let api = api_.clone();
|
global.on_set_timeline_width(move |new_width| {
|
||||||
// global.on_bucket_view_state(move |key, new_state| {
|
let api = api.clone();
|
||||||
// eprintln!("{key} => {new_state:?}");
|
let _ = app_weak.upgrade_in_event_loop(move |app| {
|
||||||
|
calculate_timeline_layout(&app, api, new_width);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// // TODO
|
let app_weak = app.as_weak();
|
||||||
// if new_state == ui::ViewState::Hidden {
|
let api = api_.clone();
|
||||||
// return;
|
global.on_timeline_scrolled(move |scroll| {
|
||||||
// }
|
let api = api.clone();
|
||||||
// });
|
let _ = app_weak.upgrade_in_event_loop(move |app| {
|
||||||
|
calculate_timeline_visibility(&app, api, scroll);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.run()?;
|
app.run()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn calculate_timeline_visibility(app: &AppWindow, api: ActorRef<Api>, scroll: f32) {
|
||||||
|
let global = app.global::<ui::Global>();
|
||||||
|
global.set_timeline_scroll(scroll);
|
||||||
|
|
||||||
|
let window_height = app.get_window_height();
|
||||||
|
let visible_range = scroll..=(scroll + window_height);
|
||||||
|
let buckets = get_image_buckets(&app);
|
||||||
|
|
||||||
|
for i in 0..buckets.row_count() {
|
||||||
|
let Some(mut bucket) = buckets.row_data(i) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let top_y = bucket.y;
|
||||||
|
let bottom_y = bucket.y + bucket.height;
|
||||||
|
|
||||||
|
let is_visible = &top_y <= &visible_range.end() && visible_range.start() <= &bottom_y;
|
||||||
|
|
||||||
|
let visibility = if is_visible {
|
||||||
|
ui::Visibility::InView
|
||||||
|
} else {
|
||||||
|
ui::Visibility::Hidden
|
||||||
|
};
|
||||||
|
|
||||||
|
let prev_visibility = mem::replace(&mut bucket.visibility, visibility);
|
||||||
|
|
||||||
|
if prev_visibility != visibility {
|
||||||
|
tracing::debug!("{i} {prev_visibility:?} => {visibility:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
match (prev_visibility, visibility) {
|
||||||
|
(ui::Visibility::Hidden, ui::Visibility::InView) => {
|
||||||
|
load_bucket(bucket.key.to_string(), app.as_weak(), api.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
(ui::Visibility::NearView, ui::Visibility::Hidden)
|
||||||
|
| (ui::Visibility::InView, ui::Visibility::Hidden) => {
|
||||||
|
unload_bucket(&bucket.key.to_string(), app);
|
||||||
|
}
|
||||||
|
|
||||||
|
(..) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
buckets.set_row_data(i, bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_timeline_layout(app: &AppWindow, api: ActorRef<Api>, timeline_width: f32) {
|
||||||
|
let global = app.global::<ui::Global>();
|
||||||
|
let min_image_size = global.get_min_image_size();
|
||||||
|
let image_margin = global.get_image_margin();
|
||||||
|
let min_size_with_margin = min_image_size + image_margin;
|
||||||
|
let buckets = get_image_buckets(&app);
|
||||||
|
|
||||||
|
let count_x = (timeline_width / min_size_with_margin).floor() as usize;
|
||||||
|
let remaining_length = timeline_width.rem_euclid(min_size_with_margin);
|
||||||
|
let image_size = min_image_size + remaining_length / (count_x as f32);
|
||||||
|
let image_size_with_margin = image_size + image_margin;
|
||||||
|
let header_height = 48.0; // TODO
|
||||||
|
|
||||||
|
let mut y = 0.0;
|
||||||
|
for i in 0..buckets.row_count() {
|
||||||
|
let Some(mut bucket) = buckets.row_data(i) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
let count_y = ((bucket.count as f32) / (count_x as f32)).ceil();
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
dbg!(count_x);
|
||||||
|
dbg!(count_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket.y = y;
|
||||||
|
bucket.height = header_height + count_y * image_size_with_margin;
|
||||||
|
y += bucket.height;
|
||||||
|
|
||||||
|
buckets.set_row_data(i, bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
global.set_timeline_width(timeline_width);
|
||||||
|
global.set_timeline_height(y);
|
||||||
|
calculate_timeline_visibility(app, api, global.get_timeline_scroll());
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppImageBuckets {
|
||||||
|
buckets: ModelRc<ImageBucket>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for AppImageBuckets {
|
||||||
|
type Target = VecModel<ImageBucket>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.buckets
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<VecModel<ui::ImageBucket>>()
|
||||||
|
.expect("Image buckets is a VecModel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_image_buckets(app: &AppWindow) -> impl Deref<Target = VecModel<ImageBucket>> {
|
||||||
|
let global = app.global::<ui::Global>();
|
||||||
|
AppImageBuckets {
|
||||||
|
buckets: global.get_image_buckets(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_preview() -> slint::Image {
|
||||||
|
slint::Image::from_rgb8(SharedPixelBuffer::clone_from_slice(
|
||||||
|
&[0x33, 0x33, 0x33], // a single dark gray pixel
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unload_bucket(time_bucket: &TimeBucketKey, app: &AppWindow) {
|
||||||
|
let buckets = get_image_buckets(app);
|
||||||
|
let Some(i) = buckets.iter().position(|b| &b.key == &time_bucket) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let bucket = buckets.row_data(i).expect("i is in the list");
|
||||||
|
|
||||||
|
let placeholder_preview = placeholder_preview();
|
||||||
|
for j in 0..bucket.previews.row_count() {
|
||||||
|
let Some(mut preview) = bucket.previews.row_data(j) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
preview.kind = ui::PreviewKind::None;
|
||||||
|
preview.image = placeholder_preview.clone();
|
||||||
|
bucket.previews.set_row_data(j, preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: write `bucket` into `buckets?`
|
||||||
|
}
|
||||||
|
|
||||||
fn load_bucket(time_bucket: TimeBucketKey, app_weak: Weak<AppWindow>, api: ActorRef<Api>) {
|
fn load_bucket(time_bucket: TimeBucketKey, app_weak: Weak<AppWindow>, api: ActorRef<Api>) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let Ok(api_bucket) = api
|
let Ok(api_bucket) = api
|
||||||
@@ -138,13 +277,7 @@ fn load_bucket(time_bucket: TimeBucketKey, app_weak: Weak<AppWindow>, api: Actor
|
|||||||
};
|
};
|
||||||
|
|
||||||
let _ = app_weak.upgrade_in_event_loop(move |app| {
|
let _ = app_weak.upgrade_in_event_loop(move |app| {
|
||||||
let global = app.global::<ui::Global>();
|
let buckets = get_image_buckets(&app);
|
||||||
|
|
||||||
let buckets = global.get_image_buckets();
|
|
||||||
let buckets = buckets
|
|
||||||
.as_any()
|
|
||||||
.downcast_ref::<VecModel<ui::ImageBucket>>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let Some(i) = buckets.iter().position(|b| &b.key == &time_bucket) else {
|
let Some(i) = buckets.iter().position(|b| &b.key == &time_bucket) else {
|
||||||
return;
|
return;
|
||||||
@@ -192,14 +325,7 @@ fn load_thumbnail(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let _ = app_weak.upgrade_in_event_loop(move |app| {
|
let _ = app_weak.upgrade_in_event_loop(move |app| {
|
||||||
let global = app.global::<ui::Global>();
|
let buckets = get_image_buckets(&app);
|
||||||
|
|
||||||
let buckets = global.get_image_buckets();
|
|
||||||
let buckets = buckets
|
|
||||||
.as_any()
|
|
||||||
.downcast_ref::<VecModel<ui::ImageBucket>>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let Some(i) = buckets.iter().position(|b| &b.key == &time_bucket) else {
|
let Some(i) = buckets.iter().position(|b| &b.key == &time_bucket) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,17 +12,20 @@ struct ImagePreview {
|
|||||||
kind: PreviewKind,
|
kind: PreviewKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Visibility {
|
||||||
|
Hidden,
|
||||||
|
NearView,
|
||||||
|
InView,
|
||||||
|
}
|
||||||
|
|
||||||
struct ImageBucket {
|
struct ImageBucket {
|
||||||
key: string,
|
key: string,
|
||||||
title: string,
|
title: string,
|
||||||
count: int,
|
count: int,
|
||||||
previews: [ImagePreview],
|
previews: [ImagePreview],
|
||||||
}
|
y: length,
|
||||||
|
height: length,
|
||||||
enum ViewState {
|
visibility: Visibility,
|
||||||
Hidden,
|
|
||||||
NearView,
|
|
||||||
InView,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export global Global {
|
export global Global {
|
||||||
@@ -33,8 +36,11 @@ export global Global {
|
|||||||
{ key: "2026-02-02", title: "Feb 2, 2026", count: 12 },
|
{ key: "2026-02-02", title: "Feb 2, 2026", count: 12 },
|
||||||
{ key: "2026-02-03", title: "Feb 3, 2026", count: 12 },
|
{ key: "2026-02-03", title: "Feb 3, 2026", count: 12 },
|
||||||
];
|
];
|
||||||
|
in property <length> timeline-height;
|
||||||
callback bucket-view-state(string, ViewState);
|
in property <length> timeline-width;
|
||||||
|
in property <length> timeline-scroll;
|
||||||
|
callback set-timeline-width(length);
|
||||||
|
callback timeline-scrolled(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -71,18 +77,11 @@ component ImagePreview inherits Rectangle {
|
|||||||
|
|
||||||
component TimelineBlock inherits VerticalLayout {
|
component TimelineBlock inherits VerticalLayout {
|
||||||
in property <int> index: -1;
|
in property <int> index: -1;
|
||||||
in property <ImageBucket> bucket;
|
in-out property <ImageBucket> bucket;
|
||||||
|
|
||||||
function calc-view-state() -> ViewState {
|
|
||||||
// TODO: fix this function
|
|
||||||
return ViewState.InView;
|
|
||||||
}
|
|
||||||
|
|
||||||
in property <ViewState> view-state: calc-view-state();
|
|
||||||
|
|
||||||
property <length> min-image-size: Global.min-image-size;
|
property <length> min-image-size: Global.min-image-size;
|
||||||
property <length> min-size-with-margin: min-image-size + Global.image-margin;
|
property <length> min-size-with-margin: min-image-size + Global.image-margin;
|
||||||
property <int> count-x: self.width / min-size-with-margin;
|
property <int> count-x: Math.floor(self.width / min-size-with-margin); // TODO: or is it ceil?
|
||||||
property <int> count-y: Math.ceil(bucket.count / count-x);
|
property <int> count-y: Math.ceil(bucket.count / count-x);
|
||||||
|
|
||||||
function calc-image-size() -> length {
|
function calc-image-size() -> length {
|
||||||
@@ -93,19 +92,16 @@ component TimelineBlock inherits VerticalLayout {
|
|||||||
property <length> image-size: calc-image-size();
|
property <length> image-size: calc-image-size();
|
||||||
property <length> image-size-with-margin: image-size + Global.image-margin;
|
property <length> image-size-with-margin: image-size + Global.image-margin;
|
||||||
|
|
||||||
min-width: min-image-size;
|
property <length> title-box-height: 36px;
|
||||||
alignment: start;
|
|
||||||
height: title-box.height + count-y * image-size-with-margin;
|
height: title-box.height + count-y * image-size-with-margin;
|
||||||
|
|
||||||
// FIXME: this triggers recursion errors in slint.
|
y: bucket.y;
|
||||||
// changed view-state => {
|
min-width: min-image-size;
|
||||||
// Global.bucket-view-state(bucket.key, view-state);
|
alignment: start;
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: don't render subtree when self.view-state = Hidden
|
|
||||||
title-box := HorizontalBox {
|
title-box := HorizontalBox {
|
||||||
alignment: space-between;
|
alignment: space-between;
|
||||||
height: 36px;
|
height: title-box-height;
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: bucket.title;
|
text: bucket.title;
|
||||||
@@ -117,7 +113,6 @@ component TimelineBlock inherits VerticalLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: don't render subtree when self.view-state = Hidden
|
|
||||||
image-box := Rectangle {
|
image-box := Rectangle {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: count-y * image-size-with-margin;
|
height: count-y * image-size-with-margin;
|
||||||
@@ -141,6 +136,15 @@ component TimelineBlock inherits VerticalLayout {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
export component AppWindow inherits Window {
|
export component AppWindow inherits Window {
|
||||||
|
out property <length> window-height: self.height;
|
||||||
|
|
||||||
|
// Do not base preferred-width on children
|
||||||
|
preferred-width: 480px;
|
||||||
|
|
||||||
|
changed width => {
|
||||||
|
Global.set-timeline-width(self.width);
|
||||||
|
}
|
||||||
|
|
||||||
VerticalLayout {
|
VerticalLayout {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -148,45 +152,26 @@ export component AppWindow inherits Window {
|
|||||||
Header {}
|
Header {}
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
width: 100%;
|
|
||||||
mouse-drag-pan-enabled: true;
|
mouse-drag-pan-enabled: true;
|
||||||
|
viewport-height: rect.height;
|
||||||
|
|
||||||
// viewport-height: 20000px;
|
changed viewport-y => {
|
||||||
|
Global.timeline-scrolled(-self.viewport-y);
|
||||||
// self.viewport-y goes into the negative. invert it to make things easier.
|
}
|
||||||
// property <length> viewport-top-y: -self.viewport-y;
|
|
||||||
// property <length> viewport-bottom-y: viewport-top-y + self.height;
|
|
||||||
// property <length> almost-visible-margin: self.height;
|
|
||||||
|
|
||||||
// function is-visible(top-y: length, bottom-y: length) -> bool {
|
|
||||||
// top-y <= viewport-bottom-y && viewport-top-y <= bottom-y
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function is-almost-visible(top-y: length, bottom-y: length) -> bool {
|
|
||||||
// top-y <= (viewport-bottom-y + almost-visible-margin)
|
|
||||||
// && (viewport-top-y - almost-visible-margin) <= bottom-y
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function view-state(top-y: length, bottom-y: length) -> ViewState {
|
|
||||||
// if is-visible(top-y, bottom-y) {
|
|
||||||
// return ViewState.InView;
|
|
||||||
// }
|
|
||||||
// if is-almost-visible(top-y, bottom-y) {
|
|
||||||
// return ViewState.NearView;
|
|
||||||
// }
|
|
||||||
// return ViewState.Hidden;
|
|
||||||
// }
|
|
||||||
|
|
||||||
VerticalLayout {
|
rect := Rectangle {
|
||||||
alignment: start;
|
y: 0;
|
||||||
padding: 0px;
|
x: 0;
|
||||||
width: 100%;
|
width: root.width;
|
||||||
|
height: Global.timeline-height;
|
||||||
for bucket[i] in Global.image-buckets : TimelineBlock {
|
preferred-width: self.width;
|
||||||
width: 100%;
|
preferred-height: self.height;
|
||||||
index: i;
|
for bucket[i] in Global.image-buckets : Rectangle {
|
||||||
bucket: bucket;
|
if bucket.visibility != Visibility.InView : TimelineBlock {
|
||||||
// view-state: view-state(self.y, self.y + self.height);
|
width: root.width;
|
||||||
|
index: i;
|
||||||
|
bucket: bucket;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user