add timeline f button
This commit is contained in:
parent
1014062db7
commit
7a1b9adb61
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -462,6 +462,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"topojson",
|
"topojson",
|
||||||
|
"tracker",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -66,6 +66,7 @@ euclid = "0.22.9"
|
|||||||
gl = "0.14.0"
|
gl = "0.14.0"
|
||||||
crossbeam = "0.8.4"
|
crossbeam = "0.8.4"
|
||||||
chrono = "0.4.32"
|
chrono = "0.4.32"
|
||||||
|
tracker = "0.2.1"
|
||||||
# plotters-cairo = "0.5.0"
|
# plotters-cairo = "0.5.0"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
use crate::timeline::TimeLine;
|
use crate::timeline::TimeLine;
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt, ToggleButtonExt};
|
use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt, ToggleButtonExt};
|
||||||
use relm4::typed_list_view::{RelmListItem, TypedListView};
|
use relm4::typed_list_view::{RelmListItem, TypedListView};
|
||||||
use relm4::*;
|
use relm4::*;
|
||||||
use relm4_components::open_button::{OpenButton, OpenButtonSettings};
|
use relm4_components::open_button::{OpenButton, OpenButtonSettings};
|
||||||
use relm4_components::open_dialog::OpenDialogSettings;
|
use relm4_components::open_dialog::OpenDialogSettings;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use chrono::{DateTime, Utc, Duration};
|
|
||||||
|
|
||||||
|
|
||||||
|
#[tracker::track]
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ControlPanelModel {
|
pub struct ControlPanelModel {
|
||||||
|
timeline_start: DateTime<Utc>,
|
||||||
|
#[tracker::no_eq]
|
||||||
open_button: Controller<OpenButton>,
|
open_button: Controller<OpenButton>,
|
||||||
|
#[tracker::no_eq]
|
||||||
list_img_wrapper: TypedListView<ImgItem, gtk::SingleSelection>,
|
list_img_wrapper: TypedListView<ImgItem, gtk::SingleSelection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,9 +73,16 @@ pub enum HeaderOutput {
|
|||||||
Export,
|
Export,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TimelineMsg {
|
||||||
|
Rewind(Duration),
|
||||||
|
FastForward(Duration),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AppMsg {
|
pub enum AppMsg {
|
||||||
Open(PathBuf),
|
Open(PathBuf),
|
||||||
|
TimeLine(TimelineMsg),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[relm4::component(pub)]
|
#[relm4::component(pub)]
|
||||||
@ -142,13 +153,29 @@ impl SimpleComponent for ControlPanelModel {
|
|||||||
set_halign: gtk::Align::Start,
|
set_halign: gtk::Align::Start,
|
||||||
},
|
},
|
||||||
gtk::Box{
|
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{
|
TimeLine{
|
||||||
set_height_request: 40,
|
set_height_request: 40,
|
||||||
set_width_request: 381,
|
set_width_request: 400,
|
||||||
set_time_start: Utc::now(),
|
#[track = "model.changed(ControlPanelModel::timeline_start())"]
|
||||||
set_selection: Some(crate::timeline::Selection::Point(
|
set_time_start: model.timeline_start,
|
||||||
Utc::now() + Duration::hours(1)
|
},
|
||||||
)),
|
gtk::Button{
|
||||||
|
set_icon_name: "fast-forward-filled",
|
||||||
|
connect_clicked[sender] => move |_| {
|
||||||
|
sender.input(AppMsg::TimeLine(
|
||||||
|
TimelineMsg::FastForward(Duration::minutes(12))
|
||||||
|
));
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
gtk::ScrolledWindow{
|
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(
|
fn init(
|
||||||
@ -190,9 +225,13 @@ impl SimpleComponent for ControlPanelModel {
|
|||||||
|
|
||||||
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 timeline_start = Utc::now();
|
||||||
|
|
||||||
let model = ControlPanelModel {
|
let model = ControlPanelModel {
|
||||||
|
timeline_start,
|
||||||
open_button,
|
open_button,
|
||||||
list_img_wrapper,
|
list_img_wrapper,
|
||||||
|
tracker: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let my_view = &model.list_img_wrapper.view;
|
let my_view = &model.list_img_wrapper.view;
|
||||||
@ -201,8 +240,15 @@ impl SimpleComponent for ControlPanelModel {
|
|||||||
ComponentParts { model, widgets }
|
ComponentParts { model, widgets }
|
||||||
}
|
}
|
||||||
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
|
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
|
||||||
|
self.reset();
|
||||||
match msg {
|
match msg {
|
||||||
AppMsg::Open(p) => println!("Open file: {:?}", p),
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,23 @@
|
|||||||
use chrono::{prelude::*, Duration};
|
use chrono::{prelude::*, Duration};
|
||||||
use gtk::glib::clone;
|
use glib::Properties;
|
||||||
use gtk::prelude::{DrawingAreaExtManual, GestureSingleExt, StyleContextExt};
|
use gtk::prelude::*;
|
||||||
use gtk::subclass::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::cell::{Cell, RefCell};
|
||||||
use std::num::NonZeroU32;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use svg::parser::Event;
|
|
||||||
|
|
||||||
use crate::render::Render;
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Selection {
|
pub enum Selection {
|
||||||
Slice((DateTime<Utc>, DateTime<Utc>)),
|
Slice((DateTime<Utc>, DateTime<Utc>)),
|
||||||
Point(DateTime<Utc>),
|
Point(DateTime<Utc>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #[derive(Properties)]
|
||||||
|
// #[properties(wrapper_type = super::TimeLine)]
|
||||||
pub struct TimeLine {
|
pub struct TimeLine {
|
||||||
drawing_area: RefCell<Option<gtk::DrawingArea>>,
|
pub(super) drawing_area: RefCell<Option<gtk::DrawingArea>>,
|
||||||
height: Cell<u32>,
|
pub(super) height: Cell<u32>,
|
||||||
width: Cell<u32>,
|
pub(super) width: Cell<u32>,
|
||||||
pub(super) selection: Rc<RefCell<Option<Selection>>>,
|
pub(super) selection: Cell<Option<Selection>>,
|
||||||
pub(super) margin_horizontal: Cell<u32>,
|
pub(super) margin_horizontal: Cell<u32>,
|
||||||
pub(super) margin_vertical: Cell<u32>,
|
pub(super) margin_vertical: Cell<u32>,
|
||||||
pub(super) major_tick_interval: Cell<u32>,
|
pub(super) major_tick_interval: Cell<u32>,
|
||||||
@ -37,7 +34,7 @@ impl Default for TimeLine {
|
|||||||
drawing_area: RefCell::new(None),
|
drawing_area: RefCell::new(None),
|
||||||
height: Cell::new(40),
|
height: Cell::new(40),
|
||||||
width: Cell::new(380),
|
width: Cell::new(380),
|
||||||
selection: Rc::new(RefCell::new(None)),
|
selection: Cell::new(None),
|
||||||
margin_horizontal: Cell::new(10),
|
margin_horizontal: Cell::new(10),
|
||||||
margin_vertical: Cell::new(0),
|
margin_vertical: Cell::new(0),
|
||||||
major_tick_interval: Cell::new(30),
|
major_tick_interval: Cell::new(30),
|
||||||
@ -45,7 +42,7 @@ impl Default for TimeLine {
|
|||||||
minor_tick_step: Cell::new(360),
|
minor_tick_step: Cell::new(360),
|
||||||
border_radius: Cell::new(8.0),
|
border_radius: Cell::new(8.0),
|
||||||
tick_selection: Rc::new(Cell::new(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 {
|
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 WidgetImpl 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 {
|
impl TimeLine {
|
||||||
pub(super) fn set_start(&self, start: DateTime<Utc>) {
|
pub(super) fn set_start(&self, start: DateTime<Utc>) {
|
||||||
let nearest = Self::round_to_nearest(start, self.minor_tick_step.get());
|
self.start_time.set(start);
|
||||||
self.start_time.set(nearest);
|
let drawing_area = self.drawing_area.borrow();
|
||||||
}
|
|
||||||
|
|
||||||
fn round_to_nearest(dt: DateTime<Utc>, _seconds: usize) -> DateTime<Utc> {
|
if let Some(drawing_area) = drawing_area.as_ref() {
|
||||||
let _seconds = _seconds as i64;
|
drawing_area.queue_draw();
|
||||||
|
}
|
||||||
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_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<Utc>, _seconds: usize) -> DateTime<Utc> {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
mod imp;
|
mod imp;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{prelude::*, DateTime, Duration, Utc};
|
||||||
|
use glib::clone;
|
||||||
pub use glib::subclass::prelude::*;
|
pub use glib::subclass::prelude::*;
|
||||||
use glib::{clone, Time};
|
use gtk::prelude::*;
|
||||||
use gtk::traits::WidgetExt;
|
use gtk::traits::WidgetExt;
|
||||||
use gtk::{EventControllerScrollFlags, Inhibit};
|
use gtk::EventControllerMotion;
|
||||||
pub use imp::Selection;
|
pub use imp::Selection;
|
||||||
|
use imp::{draw_cursor, draw_rounded_rectangle, round_to_nearest};
|
||||||
|
|
||||||
glib::wrapper! {
|
glib::wrapper! {
|
||||||
pub struct TimeLine(ObjectSubclass<imp::TimeLine>)
|
pub struct TimeLine(ObjectSubclass<imp::TimeLine>)
|
||||||
@ -20,6 +22,182 @@ impl Default for TimeLine {
|
|||||||
impl TimeLine {
|
impl TimeLine {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let this: Self = glib::Object::new();
|
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
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,8 +211,4 @@ impl TimeLine {
|
|||||||
pub fn set_major_tick_interval(&self) {}
|
pub fn set_major_tick_interval(&self) {}
|
||||||
pub fn set_major_tick_step(&self) {}
|
pub fn set_major_tick_step(&self) {}
|
||||||
pub fn set_minor_tick_step(&self) {}
|
pub fn set_minor_tick_step(&self) {}
|
||||||
pub fn set_selection(&self, selection: Option<Selection>) {
|
|
||||||
let self_ = self.imp();
|
|
||||||
self_.selection.replace(selection);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user