From 1014062db73399aba630f346f0b7a254dbd69e11 Mon Sep 17 00:00:00 2001 From: Tsuki Date: Tue, 23 Jan 2024 20:06:15 +0800 Subject: [PATCH] timeline click --- Cargo.lock | 7 +- Cargo.toml | 1 + src/components/control_panel/control_panel.rs | 31 ++- src/timeline/imp.rs | 209 +++++++++++++----- src/timeline/mod.rs | 17 ++ 5 files changed, 197 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e4037b..7001e59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,16 +403,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.48.1", + "windows-targets 0.52.0", ] [[package]] @@ -422,6 +422,7 @@ dependencies = [ "anyhow", "async-trait", "cairo-rs", + "chrono", "crossbeam", "epoxy", "euclid", diff --git a/Cargo.toml b/Cargo.toml index 8b45a7d..8b05524 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ surfman = "0.8.1" euclid = "0.22.9" gl = "0.14.0" crossbeam = "0.8.4" +chrono = "0.4.32" # plotters-cairo = "0.5.0" diff --git a/src/components/control_panel/control_panel.rs b/src/components/control_panel/control_panel.rs index a873d8e..08079bc 100644 --- a/src/components/control_panel/control_panel.rs +++ b/src/components/control_panel/control_panel.rs @@ -6,6 +6,8 @@ use relm4::*; use relm4_components::open_button::{OpenButton, OpenButtonSettings}; use relm4_components::open_dialog::OpenDialogSettings; use std::path::PathBuf; +use chrono::{DateTime, Utc, Duration}; + pub struct ControlPanelModel { open_button: Controller, @@ -129,26 +131,39 @@ impl SimpleComponent for ControlPanelModel { } }, gtk::Frame{ - set_width_request: 500, gtk::Box{ set_orientation:gtk::Orientation::Vertical, - set_spacing: 10, + set_margin_horizontal:10, + set_margin_vertical:10, + set_spacing: 4, + gtk::Label{ + set_label: "TimeLine", + add_css_class:"h2", + set_halign: gtk::Align::Start, + }, gtk::Box{ TimeLine{ - set_width_request: 500, + set_height_request: 40, + set_width_request: 381, + set_time_start: Utc::now(), + set_selection: Some(crate::timeline::Selection::Point( + Utc::now() + Duration::hours(1) + )), } }, gtk::ScrolledWindow{ - set_height_request: 70, + set_height_request: 75, #[local_ref] my_view -> gtk::ListView{ add_css_class: "lv", set_orientation: gtk::Orientation::Horizontal, } }, - } }, + }, + gtk::ScrolledWindow{ + set_vexpand: true, } } @@ -173,11 +188,7 @@ impl SimpleComponent for ControlPanelModel { let mut list_img_wrapper: TypedListView = TypedListView::with_sorting(); - list_img_wrapper.append(ImgItem::new( - "00:00:00".to_string(), - None, - true, - )); + list_img_wrapper.append(ImgItem::new("00:00:00".to_string(), None, true)); let model = ControlPanelModel { open_button, diff --git a/src/timeline/imp.rs b/src/timeline/imp.rs index 4ee5aa9..4900468 100644 --- a/src/timeline/imp.rs +++ b/src/timeline/imp.rs @@ -1,22 +1,51 @@ +use chrono::{prelude::*, Duration}; use gtk::glib::clone; -use gtk::prelude::{DrawingAreaExtManual, StyleContextExt}; +use gtk::prelude::{DrawingAreaExtManual, GestureSingleExt, StyleContextExt}; use gtk::subclass::prelude::*; use gtk::traits::{GLAreaExt, WidgetExt}; use gtk::{cairo, EventControllerMotion}; -use std::cell::RefCell; +use std::borrow::BorrowMut; +use std::cell::{Cell, RefCell}; use std::num::NonZeroU32; +use std::rc::Rc; use svg::parser::Event; use crate::render::Render; +pub enum Selection { + Slice((DateTime, DateTime)), + Point(DateTime), +} pub struct TimeLine { drawing_area: RefCell>, + height: Cell, + width: Cell, + pub(super) selection: Rc>>, + pub(super) margin_horizontal: Cell, + pub(super) margin_vertical: Cell, + pub(super) major_tick_interval: Cell, + pub(super) major_tick_step: Cell, + pub(super) minor_tick_step: Cell, + pub(super) border_radius: Cell, + pub(super) tick_selection: Rc>, + pub(super) start_time: Cell>, } impl Default for TimeLine { fn default() -> Self { Self { drawing_area: RefCell::new(None), + height: Cell::new(40), + width: Cell::new(380), + selection: Rc::new(RefCell::new(None)), + margin_horizontal: Cell::new(10), + margin_vertical: Cell::new(0), + major_tick_interval: Cell::new(30), + major_tick_step: Cell::new(3600), + minor_tick_step: Cell::new(360), + border_radius: Cell::new(8.0), + tick_selection: Rc::new(Cell::new(0)), + start_time: Cell::new(Self::round_to_nearest(Utc::now(), 360)), } } } @@ -39,92 +68,139 @@ impl ObjectImpl for TimeLine { let prefers_dark_theme = settings.is_gtk_application_prefer_dark_theme(); let obj = self.obj().clone(); - - let (r, g, b) = if prefers_dark_theme { - (1.0, 1.0, 1.0) + let ((r, g, b), (br, bg, bb)) = if prefers_dark_theme { + ((1.0, 1.0, 1.0), (0.2274, 0.2274, 0.2274)) } else { - (0.0, 0.0, 0.0) + ((0.0, 0.0, 0.0), (0.89, 0.89, 0.89)) }; - let container = gtk::Box::builder() - .orientation(gtk::Orientation::Horizontal) - .height_request(70) - .hexpand(true) - .margin_start(10) - .margin_end(10) - .build(); - - let cursor_pos = std::rc::Rc::new(std::cell::RefCell::new(50.0)); + let cursor_pos = std::rc::Rc::new(std::cell::Cell::new(None)); let cursor_pos_clone = cursor_pos.clone(); - + let cursor_pos_leave_clone = cursor_pos.clone(); let motion_controller = EventControllerMotion::new(); let cursor_pos_clone = cursor_pos.clone(); + let cursor_pos_clicker_clone = cursor_pos.clone(); + let height = self.height.get(); + let width = self.width.get(); + + let margin_horizontal = self.margin_horizontal.get() as f64; + let major_tick_step = self.major_tick_step.get(); + let minor_tick_step = self.minor_tick_step.get(); + let major_tick_interval = self.major_tick_interval.get() as f64; + let minor_tick_interval = major_tick_interval / (major_tick_step / minor_tick_step) as f64; + let border_radius = self.border_radius.get(); + + drawing_area.set_height_request(height as i32); + drawing_area.set_width_request(width as i32); + motion_controller.connect_motion(clone!(@weak drawing_area => move |_, x, _| { - *cursor_pos.borrow_mut() = x; - drawing_area.queue_draw(); // 重绘时间线和游标 + cursor_pos.set(Some(x.clamp(margin_horizontal, width as f64 - margin_horizontal))); + drawing_area.queue_draw(); })); + motion_controller.connect_leave(clone!(@weak drawing_area => move |_| { + cursor_pos_leave_clone.set(None); + drawing_area.queue_draw(); + })); + + let selection = self.selection.clone(); + let start = (&self.start_time).get(); + drawing_area.set_draw_func( (move |_, cr, w, h| { - cr.set_source_rgba(0.89, 0.89, 0.89, 1.0); + cr.set_source_rgb(br, bg, bb); - Self::draw_rounded_rectangle(cr, 0.0, h as f64 / 2.0 - 20.0, w as f64, 45.0, 5.0); + Self::draw_rounded_rectangle(cr, 0.0, 0.0, w as f64, h as f64, border_radius); + let w = w - (2.0 * margin_horizontal) as i32; + let w = w as f64; + let h = h as f64; - let w = w - 20; cr.fill().unwrap(); cr.set_source_rgb(r, g, b); cr.set_line_width(1.5); - let major_tick_interval = 30; // 每小时的像素宽度 - let minor_tick_interval = major_tick_interval / 10; - - let y_pos = h as f64 / 2.0; // 时间轴位于中央 - cr.move_to(10.0, y_pos); - cr.line_to(w as f64, y_pos); + let y_pos = h / 2.0; // 时间轴位于中央 + cr.move_to(margin_horizontal, y_pos); + cr.line_to(w + margin_horizontal, y_pos); cr.stroke().unwrap(); - // 绘制大刻度(一小时) - for i in 0..(w / major_tick_interval + 1) { - let x_pos = i * major_tick_interval; - cr.set_line_width(1.0); - cr.move_to(x_pos as f64 + 10.0, h as f64 / 2.0); - cr.line_to(x_pos as f64 + 10.0, h as f64 / 2.0 - 8.0); - cr.stroke().unwrap(); - let time_text = format!("{}", i); // 假设时间轴从0点开始 - cr.move_to(x_pos as f64 + 10.0, y_pos + 15.0); // 文本位置 - cr.show_text(&time_text).unwrap(); + { + let mut time_cursor = 0.0; + let mut time_stamp = start.timestamp(); + let minor_tick_step = minor_tick_step as i64; + let major_tick_step = major_tick_step as i64; + while time_cursor < w { + if time_stamp % major_tick_step == 0 { + cr.set_line_width(1.0); + cr.move_to(time_cursor + margin_horizontal, h / 2.0); + cr.line_to(time_cursor + margin_horizontal, h / 2.0 - 8.0); + cr.stroke().unwrap(); + time_cursor += minor_tick_interval; + time_stamp += minor_tick_step; + continue; + } + cr.set_line_width(0.5); + cr.move_to(time_cursor + margin_horizontal, h / 2.0); + cr.line_to(time_cursor + margin_horizontal, h / 2.0 - 5.0); + cr.stroke().unwrap(); + time_cursor += minor_tick_interval; + time_stamp += minor_tick_step; + } } - // 绘制小刻度(一分钟) - for i in 0..(w / minor_tick_interval + 1) { - let x_pos = i * minor_tick_interval; - cr.set_line_width(0.5); - cr.move_to(x_pos as f64 + 10.0, h as f64 / 2.0); - cr.line_to(x_pos as f64 + 10.0, h as f64 / 2.0 - 5.0); - cr.stroke().unwrap(); + cr.set_source_rgb(1.0, 1.0, 1.0); + if let Some(x) = cursor_pos_clone.get() { + Self::draw_cursor(cr, x, h / 2.0 - 15.0, 2f64, 35f64); } cr.set_source_rgb(0.98, 0.26, 0.24); // 红色 - - Self::draw_cursor( - cr, - *cursor_pos_clone.borrow(), - h as f64 / 2.0 - 15.0, - 2f64, - 35f64, - ); + if let Some(selection) = selection.borrow().as_ref() { + match selection { + Selection::Point(p) => { + let duration = p.signed_duration_since(start); + let secs = duration.num_seconds(); + if secs > 0 { + let x_pos = + secs as f64 / major_tick_step as f64 * major_tick_interval; + Self::draw_cursor( + cr, + x_pos + margin_horizontal, + h / 2.0 - 15.0, + 2f64, + 35f64, + ); + } + } + Selection::Slice((p1, p2)) => {} + } + } }), ); - drawing_area.add_controller(motion_controller); + let gesture_click = gtk::GestureClick::new(); + gesture_click.set_button(1); + let mut gesture_click_selection = self.selection.clone(); + gesture_click.connect_pressed(clone!(@weak drawing_area =>move |gesture, _, x, y| { + let x = x.clamp(margin_horizontal, width as f64 - margin_horizontal); + let secs = (x - margin_horizontal) / (major_tick_interval / major_tick_step as f64); - drawing_area.set_parent(&container); + gesture_click_selection.replace(Some(Selection::Point( + start + Duration::seconds(secs as i64), + ))); + + cursor_pos_clicker_clone.replace(None); + drawing_area.queue_draw(); + + })); + + drawing_area.add_controller(gesture_click); + drawing_area.add_controller(motion_controller); + drawing_area.set_parent(&obj); drawing_area.set_vexpand(true); drawing_area.set_hexpand(true); self.drawing_area.borrow_mut().replace(drawing_area); - container.set_parent(&obj); self.parent_constructed(); } } @@ -181,3 +257,26 @@ impl TimeLine { cr.fill().expect("Failed to fill the triangle"); } } + +impl TimeLine { + pub(super) fn set_start(&self, start: DateTime) { + let nearest = Self::round_to_nearest(start, self.minor_tick_step.get()); + self.start_time.set(nearest); + } + + fn round_to_nearest(dt: DateTime, _seconds: usize) -> DateTime { + let _seconds = _seconds as i64; + + let seconds = dt.timestamp(); + let rounded_seconds = (seconds / _seconds) * _seconds; // 向下规整到最近的整 6 分钟 + let remainder = seconds % _seconds; + + let stamp = if remainder >= _seconds / 2 { + seconds + (_seconds - remainder) as i64 + } else { + seconds - remainder as i64 + }; + + Utc.timestamp_opt(stamp, 0).unwrap() + } +} diff --git a/src/timeline/mod.rs b/src/timeline/mod.rs index 86c1527..2d33915 100644 --- a/src/timeline/mod.rs +++ b/src/timeline/mod.rs @@ -1,8 +1,10 @@ mod imp; +use chrono::{DateTime, Utc}; pub use glib::subclass::prelude::*; use glib::{clone, Time}; use gtk::traits::WidgetExt; use gtk::{EventControllerScrollFlags, Inhibit}; +pub use imp::Selection; glib::wrapper! { pub struct TimeLine(ObjectSubclass) @@ -20,4 +22,19 @@ impl TimeLine { let this: Self = glib::Object::new(); this } + + pub fn set_time_start(&self, start_time: DateTime) { + self.imp().set_start(start_time); + } + pub fn set_border_radius(&self) {} + pub fn set_margin_horizontal(&self) {} + pub fn set_margin_vertical(&self) {} + + pub fn set_major_tick_interval(&self) {} + pub fn set_major_tick_step(&self) {} + pub fn set_minor_tick_step(&self) {} + pub fn set_selection(&self, selection: Option) { + let self_ = self.imp(); + self_.selection.replace(selection); + } }