diff --git a/Cargo.lock b/Cargo.lock index 7001e59..53f6991 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -462,6 +462,7 @@ dependencies = [ "thiserror", "tokio", "topojson", + "tracker", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8b05524..05f4c50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ euclid = "0.22.9" gl = "0.14.0" crossbeam = "0.8.4" chrono = "0.4.32" +tracker = "0.2.1" # plotters-cairo = "0.5.0" diff --git a/src/components/control_panel/control_panel.rs b/src/components/control_panel/control_panel.rs index 08079bc..1626278 100644 --- a/src/components/control_panel/control_panel.rs +++ b/src/components/control_panel/control_panel.rs @@ -1,16 +1,20 @@ use crate::timeline::TimeLine; use adw::prelude::*; +use chrono::{DateTime, Duration, Utc}; use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt, ToggleButtonExt}; use relm4::typed_list_view::{RelmListItem, TypedListView}; use relm4::*; use relm4_components::open_button::{OpenButton, OpenButtonSettings}; use relm4_components::open_dialog::OpenDialogSettings; use std::path::PathBuf; -use chrono::{DateTime, Utc, Duration}; - +#[tracker::track] +#[derive(Debug)] pub struct ControlPanelModel { + timeline_start: DateTime, + #[tracker::no_eq] open_button: Controller, + #[tracker::no_eq] list_img_wrapper: TypedListView, } @@ -69,9 +73,16 @@ pub enum HeaderOutput { Export, } +#[derive(Debug)] +pub enum TimelineMsg { + Rewind(Duration), + FastForward(Duration), +} + #[derive(Debug)] pub enum AppMsg { Open(PathBuf), + TimeLine(TimelineMsg), } #[relm4::component(pub)] @@ -142,13 +153,29 @@ impl SimpleComponent for ControlPanelModel { set_halign: gtk::Align::Start, }, gtk::Box{ + set_orientation: gtk::Orientation::Horizontal, + set_spacing:5, + gtk::Button{ + set_icon_name: "rewind-filled", + connect_clicked[sender] => move |_| { + sender.input(AppMsg::TimeLine( + TimelineMsg::Rewind(-Duration::minutes(12)) + )); + }, + }, TimeLine{ 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) - )), + set_width_request: 400, + #[track = "model.changed(ControlPanelModel::timeline_start())"] + set_time_start: model.timeline_start, + }, + gtk::Button{ + set_icon_name: "fast-forward-filled", + connect_clicked[sender] => move |_| { + sender.input(AppMsg::TimeLine( + TimelineMsg::FastForward(Duration::minutes(12)) + )); + }, } }, gtk::ScrolledWindow{ @@ -161,10 +188,18 @@ impl SimpleComponent for ControlPanelModel { }, } }, + gtk::ScrolledWindow{ + set_hexpand: true, + gtk::Box{ + set_height_request: 140, + set_width_request: 300, + set_orientation:gtk::Orientation::Horizontal, + gtk::Calendar{ + } + } + } }, - gtk::ScrolledWindow{ - set_vexpand: true, - } + } fn init( @@ -190,9 +225,13 @@ impl SimpleComponent for ControlPanelModel { list_img_wrapper.append(ImgItem::new("00:00:00".to_string(), None, true)); + let timeline_start = Utc::now(); + let model = ControlPanelModel { + timeline_start, open_button, list_img_wrapper, + tracker: 0, }; let my_view = &model.list_img_wrapper.view; @@ -201,8 +240,15 @@ impl SimpleComponent for ControlPanelModel { ComponentParts { model, widgets } } fn update(&mut self, msg: Self::Input, _sender: ComponentSender) { + self.reset(); match msg { AppMsg::Open(p) => println!("Open file: {:?}", p), + AppMsg::TimeLine(line_msg) => match line_msg { + TimelineMsg::Rewind(c) | TimelineMsg::FastForward(c) => { + let current = self.get_timeline_start().clone(); + self.set_timeline_start(current + c); + } + }, } } } diff --git a/src/timeline/imp.rs b/src/timeline/imp.rs index 4900468..e1800e6 100644 --- a/src/timeline/imp.rs +++ b/src/timeline/imp.rs @@ -1,26 +1,23 @@ use chrono::{prelude::*, Duration}; -use gtk::glib::clone; -use gtk::prelude::{DrawingAreaExtManual, GestureSingleExt, StyleContextExt}; +use glib::Properties; +use gtk::prelude::*; use gtk::subclass::prelude::*; -use gtk::traits::{GLAreaExt, WidgetExt}; -use gtk::{cairo, EventControllerMotion}; -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; +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Selection { Slice((DateTime, DateTime)), Point(DateTime), } +// #[derive(Properties)] +// #[properties(wrapper_type = super::TimeLine)] pub struct TimeLine { - drawing_area: RefCell>, - height: Cell, - width: Cell, - pub(super) selection: Rc>>, + pub(super) drawing_area: RefCell>, + pub(super) height: Cell, + pub(super) width: Cell, + pub(super) selection: Cell>, pub(super) margin_horizontal: Cell, pub(super) margin_vertical: Cell, pub(super) major_tick_interval: Cell, @@ -37,7 +34,7 @@ impl Default for TimeLine { drawing_area: RefCell::new(None), height: Cell::new(40), width: Cell::new(380), - selection: Rc::new(RefCell::new(None)), + selection: Cell::new(None), margin_horizontal: Cell::new(10), margin_vertical: Cell::new(0), major_tick_interval: Cell::new(30), @@ -45,7 +42,7 @@ impl Default for TimeLine { 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)), + start_time: Cell::new(Utc::now()), } } } @@ -61,222 +58,82 @@ impl ObjectSubclass for TimeLine { } } -impl ObjectImpl for TimeLine { - fn constructed(&self) { - let drawing_area = gtk::DrawingArea::new(); - let settings = gtk::Settings::default().unwrap(); - let prefers_dark_theme = settings.is_gtk_application_prefer_dark_theme(); - - let obj = self.obj().clone(); - 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.89, 0.89, 0.89)) - }; - - 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.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_rgb(br, bg, bb); - - 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; - - cr.fill().unwrap(); - cr.set_source_rgb(r, g, b); - cr.set_line_width(1.5); - - let y_pos = h / 2.0; // 时间轴位于中央 - cr.move_to(margin_horizontal, y_pos); - cr.line_to(w + margin_horizontal, y_pos); - cr.stroke().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; - } - } - - 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); // 红色 - 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)) => {} - } - } - }), - ); - - 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); - - 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); - - self.parent_constructed(); - } -} +impl ObjectImpl for TimeLine {} impl WidgetImpl for TimeLine {} -impl TimeLine { - fn draw_rounded_rectangle( - cr: >k::cairo::Context, - x: f64, - y: f64, - width: f64, - height: f64, - radius: f64, - ) { - cr.new_sub_path(); - cr.arc( - x + width - radius, - y + radius, - radius, - -90f64.to_radians(), - 0f64.to_radians(), - ); - cr.arc( - x + width - radius, - y + height - radius, - radius, - 0f64.to_radians(), - 90f64.to_radians(), - ); - cr.arc( - x + radius, - y + height - radius, - radius, - 90f64.to_radians(), - 180f64.to_radians(), - ); - cr.arc( - x + radius, - y + radius, - radius, - 180f64.to_radians(), - 270f64.to_radians(), - ); - cr.close_path(); - } - - fn draw_cursor(cr: >k::cairo::Context, x: f64, y: f64, width: f64, height: f64) { - // 绘制矩形部分 - cr.rectangle(x - width / 2.0, y, width, height); - cr.fill().expect("Failed to fill the rectangle"); - - // 绘制三角形部分 - 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); - } + self.start_time.set(start); + let drawing_area = self.drawing_area.borrow(); - 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() + if let Some(drawing_area) = drawing_area.as_ref() { + drawing_area.queue_draw(); + } } } + +pub(super) fn draw_rounded_rectangle( + cr: >k::cairo::Context, + x: f64, + y: f64, + width: f64, + height: f64, + radius: f64, +) { + cr.new_sub_path(); + cr.arc( + x + width - radius, + y + radius, + radius, + -90f64.to_radians(), + 0f64.to_radians(), + ); + cr.arc( + x + width - radius, + y + height - radius, + radius, + 0f64.to_radians(), + 90f64.to_radians(), + ); + cr.arc( + x + radius, + y + height - radius, + radius, + 90f64.to_radians(), + 180f64.to_radians(), + ); + cr.arc( + x + radius, + y + radius, + radius, + 180f64.to_radians(), + 270f64.to_radians(), + ); + cr.close_path(); +} + +pub(super) 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() +} + +pub(super) fn draw_cursor(cr: >k::cairo::Context, x: f64, y: f64, width: f64, height: f64) { + // 绘制矩形部分 + cr.rectangle(x - width / 2.0, y, width, height); + cr.fill().expect("Failed to fill the rectangle"); + + // 绘制三角形部分 + cr.fill().expect("Failed to fill the triangle"); +} diff --git a/src/timeline/mod.rs b/src/timeline/mod.rs index 2d33915..b2822e7 100644 --- a/src/timeline/mod.rs +++ b/src/timeline/mod.rs @@ -1,10 +1,12 @@ mod imp; -use chrono::{DateTime, Utc}; +use chrono::{prelude::*, DateTime, Duration, Utc}; +use glib::clone; pub use glib::subclass::prelude::*; -use glib::{clone, Time}; +use gtk::prelude::*; use gtk::traits::WidgetExt; -use gtk::{EventControllerScrollFlags, Inhibit}; +use gtk::EventControllerMotion; pub use imp::Selection; +use imp::{draw_cursor, draw_rounded_rectangle, round_to_nearest}; glib::wrapper! { pub struct TimeLine(ObjectSubclass) @@ -20,6 +22,182 @@ impl Default for TimeLine { impl TimeLine { pub fn new() -> Self { let this: Self = glib::Object::new(); + + let drawing_area = gtk::DrawingArea::new(); + let settings = gtk::Settings::default().unwrap(); + let prefers_dark_theme = settings.is_gtk_application_prefer_dark_theme(); + + let self_ = this.imp(); + + let ((r, g, b), (br, bg, bb), (ccr, ccg, ccb)) = if prefers_dark_theme { + ( + (1.0, 1.0, 1.0), + (0.2274, 0.2274, 0.2274), + (0.89, 0.89, 0.89), + ) + } else { + ( + (0.0, 0.0, 0.0), + (0.89, 0.89, 0.89), + (0.2274, 0.2274, 0.2274), + ) + }; + + let cursor_pos = std::rc::Rc::new(std::cell::Cell::new(None)); + 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(); + + motion_controller.connect_motion(clone!(@weak drawing_area, @weak this => move |_, x, _| { + let self_ = this.imp(); + let width = self_.width.get(); + let margin_horizontal = self_.margin_horizontal.get() as f64; + 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(); + })); + + drawing_area.set_draw_func(clone!(@strong this => move |_, cr, w, h| { + let self_ = this.imp(); + 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(); + + let start = (&self_.start_time).get(); + + let mut nearst_rounded_time = round_to_nearest(start, minor_tick_step); + + if nearst_rounded_time < start { + nearst_rounded_time += Duration::seconds(minor_tick_step as i64); + } + + let diff_seconds = nearst_rounded_time.timestamp() - start.timestamp(); + let start_pixels = + diff_seconds as f64 / minor_tick_step as f64 * minor_tick_interval as f64; + + cr.set_source_rgb(br, bg, bb); + 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; + + cr.fill().unwrap(); + cr.set_source_rgb(r, g, b); + cr.set_line_width(1.5); + + let y_pos = h / 2.0; // 时间轴位于中央 + cr.move_to(margin_horizontal, y_pos); + cr.line_to(w + margin_horizontal, y_pos); + cr.stroke().unwrap(); + + { + let mut time_cursor = start_pixels; + let mut time_stamp = nearst_rounded_time.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; + + let d = Utc.timestamp_opt(time_stamp, 0).unwrap(); + + if d.hour() % 3 == 0 { + let d_format = d.format("%H:%M").to_string(); + let extents = cr.text_extents(&d_format).unwrap(); + let x = time_cursor - (extents.width() / 2.0 + extents.x_bearing()); + cr.move_to(x + margin_horizontal, h / 2.0 + 15.0); + cr.show_text(&d_format).unwrap(); + } + + 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; + } + } + + cr.set_source_rgb(ccr,ccg,ccb); + if let Some(x) = cursor_pos_clone.get() { + draw_cursor(cr, x, h / 2.0 - 15.0, 2f64, 35f64); + } + + cr.set_source_rgb(0.98, 0.26, 0.24); // 红色 + if let Some(selection) = self_.selection.get().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; + draw_cursor( + cr, + x_pos + margin_horizontal, + h / 2.0 - 15.0, + 2f64, + 35f64, + ); + } + } + Selection::Slice((p1, p2)) => {} + } + } + + }), + ); + + let gesture_click = gtk::GestureClick::new(); + gesture_click.set_button(1); + gesture_click.connect_pressed(clone!(@weak drawing_area, @weak this =>move |gesture, _, x, y| { + let self_ = this.imp(); + let mut gesture_click_selection = self_.selection.clone(); + + 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 height = self_.height.get(); + let width = self_.width.get(); + + let start = (&self_.start_time).get(); + let x = x.clamp(margin_horizontal, width as f64 - margin_horizontal); + let secs = (x - margin_horizontal) / (major_tick_interval / major_tick_step as f64); + + 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_hexpand(true); + drawing_area.set_vexpand(true); + drawing_area.set_parent(&this); + + self_.drawing_area.replace(Some(drawing_area)); + this } @@ -33,8 +211,4 @@ impl TimeLine { 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); - } }