feat(gpui): ruler markers + work-area drag reshape
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
//! shorter and unlabeled.
|
//! shorter and unlabeled.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
App, Bounds, Font, SharedString, TextAlign, TextRun, Window, canvas, fill, hsla, point,
|
App, Bounds, Font, Hsla, SharedString, TextAlign, TextRun, Window, canvas, fill, hsla, point,
|
||||||
prelude::*, px, size,
|
prelude::*, px, size,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,6 +28,15 @@ use super::{
|
|||||||
time::{Frame, FrameRange, FrameRate, TimeDisplay},
|
time::{Frame, FrameRange, FrameRate, TimeDisplay},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A marker to paint on the ruler, at a sequence frame with its color.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RulerMarker {
|
||||||
|
/// The marker's frame position.
|
||||||
|
pub frame: Frame,
|
||||||
|
/// The marker's color (the theme accent is the fallback).
|
||||||
|
pub color: Hsla,
|
||||||
|
}
|
||||||
|
|
||||||
/// The sequence ruler rendered above the tracks.
|
/// The sequence ruler rendered above the tracks.
|
||||||
///
|
///
|
||||||
/// Cheap to construct — all fields are plain data copied out of the view
|
/// Cheap to construct — all fields are plain data copied out of the view
|
||||||
@@ -42,6 +51,8 @@ pub struct TimelineRuler {
|
|||||||
frame_rate: FrameRate,
|
frame_rate: FrameRate,
|
||||||
sequence_length: Frame,
|
sequence_length: Frame,
|
||||||
display: TimeDisplay,
|
display: TimeDisplay,
|
||||||
|
/// Markers to paint (from the data source), in ascending frame order.
|
||||||
|
markers: Vec<RulerMarker>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimelineRuler {
|
impl TimelineRuler {
|
||||||
@@ -61,6 +72,7 @@ impl TimelineRuler {
|
|||||||
frame_rate,
|
frame_rate,
|
||||||
sequence_length,
|
sequence_length,
|
||||||
display: TimeDisplay::default(),
|
display: TimeDisplay::default(),
|
||||||
|
markers: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +83,13 @@ impl TimelineRuler {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builder: the markers to paint (sequence markers from the data
|
||||||
|
/// source). Drawn as small diamonds below the tick baseline.
|
||||||
|
pub fn markers(mut self, markers: Vec<RulerMarker>) -> Self {
|
||||||
|
self.markers = markers;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// The tick step (in frames) the ruler would choose at the given zoom.
|
/// The tick step (in frames) the ruler would choose at the given zoom.
|
||||||
///
|
///
|
||||||
/// Exposed for tests and for snapping the playhead-drag indicator to the
|
/// Exposed for tests and for snapping the playhead-drag indicator to the
|
||||||
@@ -142,6 +161,8 @@ struct RulerContent {
|
|||||||
/// Local x-extents `(left, right)` of the work-area band, if a work
|
/// Local x-extents `(left, right)` of the work-area band, if a work
|
||||||
/// area is set.
|
/// area is set.
|
||||||
work_area: Option<(f32, f32)>,
|
work_area: Option<(f32, f32)>,
|
||||||
|
/// Marker positions `(x, color)` for the visible markers.
|
||||||
|
markers: Vec<(f32, Hsla)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for TimelineRuler {
|
impl RenderOnce for TimelineRuler {
|
||||||
@@ -197,6 +218,15 @@ impl RenderOnce for TimelineRuler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Markers within the visible span, in ascending frame order
|
||||||
|
// (the data source provides them sorted).
|
||||||
|
let markers = self
|
||||||
|
.markers
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.frame >= first && m.frame <= last)
|
||||||
|
.map(|m| (state.point_at_frame(m.frame).0, m.color))
|
||||||
|
.collect();
|
||||||
|
|
||||||
RulerContent {
|
RulerContent {
|
||||||
ticks,
|
ticks,
|
||||||
work_area: work_area.map(|range| {
|
work_area: work_area.map(|range| {
|
||||||
@@ -205,6 +235,7 @@ impl RenderOnce for TimelineRuler {
|
|||||||
state.point_at_frame(range.end).0,
|
state.point_at_frame(range.end).0,
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
markers,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
move |bounds, content, window, cx| {
|
move |bounds, content, window, cx| {
|
||||||
@@ -235,6 +266,26 @@ impl RenderOnce for TimelineRuler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Markers as small diamonds hanging below the tick baseline.
|
||||||
|
let marker_half: f32 = 3.0;
|
||||||
|
for (x, color) in &content.markers {
|
||||||
|
let cx = bounds.left() + px(*x);
|
||||||
|
let cy = bounds.bottom() - px(4.0);
|
||||||
|
let mut path = crate::path_builder::PathBuilder::fill();
|
||||||
|
path.add_polygon(
|
||||||
|
&[
|
||||||
|
point(cx, cy - px(marker_half)),
|
||||||
|
point(cx + px(marker_half), cy),
|
||||||
|
point(cx, cy + px(marker_half)),
|
||||||
|
point(cx - px(marker_half), cy),
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if let Ok(path) = path.build() {
|
||||||
|
window.paint_path(path, *color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Baseline along the bottom of the ruler.
|
// Baseline along the bottom of the ruler.
|
||||||
window.paint_quad(fill(
|
window.paint_quad(fill(
|
||||||
Bounds {
|
Bounds {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ use super::{
|
|||||||
clip::{ClipContent, ClipDecorator, ClipElement, NoopClipDecorator, TRIM_HANDLE_WIDTH},
|
clip::{ClipContent, ClipDecorator, ClipElement, NoopClipDecorator, TRIM_HANDLE_WIDTH},
|
||||||
data::{ClipData, ClipId, TimelineDataSource, TrackData, TrackKind},
|
data::{ClipData, ClipId, TimelineDataSource, TrackData, TrackKind},
|
||||||
playhead::PlayheadElement,
|
playhead::PlayheadElement,
|
||||||
ruler::TimelineRuler,
|
ruler::{RulerMarker, TimelineRuler},
|
||||||
state::TimelineState,
|
state::TimelineState,
|
||||||
time::{Frame, FrameRange, SnapKind, SnapPoint, snap},
|
time::{Frame, FrameRange, SnapKind, SnapPoint, snap},
|
||||||
track_header::TrackHeader,
|
track_header::TrackHeader,
|
||||||
@@ -193,6 +193,31 @@ pub enum TimelineEvent {
|
|||||||
height: Pixels,
|
height: Pixels,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// The user is dragging on the ruler, reshaping the work area. `start` /
|
||||||
|
/// `end` are the NEW in/out frames; the widget already updated its local
|
||||||
|
/// band (`[`TimelineState::work_area`]`), and the host should apply the
|
||||||
|
/// new range **live** (not undoable), mirroring Olive's drag behavior.
|
||||||
|
WorkAreaPreview {
|
||||||
|
/// The new work-area in point.
|
||||||
|
start: Frame,
|
||||||
|
/// The new work-area out point.
|
||||||
|
end: Frame,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The ruler work-area drag finished. The host commits the new range as
|
||||||
|
/// ONE undoable entry whose "old" side is `old_start`/`old_end` (the
|
||||||
|
/// range at drag start, before any live preview was applied).
|
||||||
|
WorkAreaCommitted {
|
||||||
|
/// The committed work-area in point.
|
||||||
|
start: Frame,
|
||||||
|
/// The committed work-area out point.
|
||||||
|
end: Frame,
|
||||||
|
/// The in point at drag start (the undo side of the command).
|
||||||
|
old_start: Frame,
|
||||||
|
/// The out point at drag start (the undo side of the command).
|
||||||
|
old_end: Frame,
|
||||||
|
},
|
||||||
|
|
||||||
/// The zoom (pixels per frame) changed via ctrl-scroll or pinch.
|
/// The zoom (pixels per frame) changed via ctrl-scroll or pinch.
|
||||||
/// Persist as view state if desired.
|
/// Persist as view state if desired.
|
||||||
ZoomChanged(f32),
|
ZoomChanged(f32),
|
||||||
@@ -645,6 +670,104 @@ impl<D: TimelineDataSource> TimelineView<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How close (in pixels) a press must be to a work-area band edge for the
|
||||||
|
/// drag to resize that edge instead of reshaping the whole band.
|
||||||
|
const WORK_AREA_EDGE_THRESHOLD_PX: f32 = 6.0;
|
||||||
|
|
||||||
|
/// Updates a ruler work-area drag from the pointer position.
|
||||||
|
///
|
||||||
|
/// The first drag that passes the click/drag threshold (see
|
||||||
|
/// [`RulerDrag`]) reshapes the work area: pressing near an existing band
|
||||||
|
/// edge moves that edge, any other press replaces the whole band with the
|
||||||
|
/// range spanned from the press point. The widget updates its local band
|
||||||
|
/// immediately and emits [`TimelineEvent::WorkAreaPreview`] so the host
|
||||||
|
/// can apply the range live (not undoable). A plain click never reaches
|
||||||
|
/// here — it only seeks (the ruler's `on_mouse_down`).
|
||||||
|
fn update_ruler_drag(
|
||||||
|
&mut self,
|
||||||
|
event: &DragMoveEvent<Arc<RwLock<RulerDrag>>>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let drag = Arc::clone(event.drag(cx));
|
||||||
|
let press = cx
|
||||||
|
.active_drag
|
||||||
|
.as_ref()
|
||||||
|
.map(|drag| drag.cursor_offset)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let now = event.event.position - event.bounds.origin;
|
||||||
|
let mut drag = drag.write().expect("ruler drag lock is not poisoned");
|
||||||
|
let dx = (now.x.0 - press.x.0).abs();
|
||||||
|
if dx < 3.0 {
|
||||||
|
// Still within the click threshold; keep seeking-only behavior.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !drag.resizing {
|
||||||
|
// First real drag movement: pick what the gesture reshapes. A
|
||||||
|
// press within `WORK_AREA_EDGE_THRESHOLD_PX` of an existing band
|
||||||
|
// edge moves that edge; otherwise the whole band is replaced.
|
||||||
|
let edge = match self.state.work_area {
|
||||||
|
Some(band) => {
|
||||||
|
let in_edge = self.state.point_at_frame(band.start).0;
|
||||||
|
let out_edge = self.state.point_at_frame(band.end).0;
|
||||||
|
if (press.x.0 - in_edge).abs() <= Self::WORK_AREA_EDGE_THRESHOLD_PX {
|
||||||
|
EdgeKind::In
|
||||||
|
} else if (press.x.0 - out_edge).abs() <= Self::WORK_AREA_EDGE_THRESHOLD_PX {
|
||||||
|
EdgeKind::Out
|
||||||
|
} else {
|
||||||
|
EdgeKind::Whole
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => EdgeKind::Whole,
|
||||||
|
};
|
||||||
|
drag.edge = Some(edge);
|
||||||
|
drag.old_band = self.state.work_area;
|
||||||
|
drag.resizing = true;
|
||||||
|
}
|
||||||
|
let Some(edge) = drag.edge else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let seq_len = self.sequence_length(cx).0.max(1);
|
||||||
|
let frame = self.state.frame_at_point(now.x).0.clamp(0, seq_len);
|
||||||
|
let press_frame = self.state.frame_at_point(press.x).0.clamp(0, seq_len);
|
||||||
|
let band = reshape_work_area(drag.old_band, Frame(press_frame), Frame(frame), edge, seq_len);
|
||||||
|
self.state.work_area = Some(band);
|
||||||
|
drag.new_band = Some(band);
|
||||||
|
cx.emit(TimelineEvent::WorkAreaPreview {
|
||||||
|
start: band.start,
|
||||||
|
end: band.end,
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits [`TimelineEvent::WorkAreaCommitted`] for a finished ruler drag:
|
||||||
|
/// the host commits the new range as ONE undoable entry whose old side is
|
||||||
|
/// the band at drag start (`0..0` when the drag created a new band).
|
||||||
|
fn finish_ruler_drag(&mut self, drag: &Arc<RwLock<RulerDrag>>, cx: &mut Context<Self>) {
|
||||||
|
let (resizing, old_band, new_band) = {
|
||||||
|
let drag = drag.read().expect("ruler drag lock is not poisoned");
|
||||||
|
(drag.resizing, drag.old_band, drag.new_band)
|
||||||
|
};
|
||||||
|
if !resizing {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(new) = new_band else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// A drag that created a new band (no old one) reports the empty
|
||||||
|
// `0..0` range as its old side; the host's undoable commit restores
|
||||||
|
// disabled + the empty range on undo.
|
||||||
|
let old = old_band.unwrap_or(FrameRange::new(Frame::ZERO, Frame::ZERO));
|
||||||
|
if old != new {
|
||||||
|
cx.emit(TimelineEvent::WorkAreaCommitted {
|
||||||
|
start: new.start,
|
||||||
|
end: new.end,
|
||||||
|
old_start: old.start,
|
||||||
|
old_end: old.end,
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Emits [`TimelineEvent::SelectionChanged`] for a finished marquee.
|
/// Emits [`TimelineEvent::SelectionChanged`] for a finished marquee.
|
||||||
fn finish_marquee(&mut self, cx: &mut Context<Self>) {
|
fn finish_marquee(&mut self, cx: &mut Context<Self>) {
|
||||||
cx.emit(TimelineEvent::SelectionChanged);
|
cx.emit(TimelineEvent::SelectionChanged);
|
||||||
@@ -752,6 +875,18 @@ impl<D: TimelineDataSource> Render for TimelineView<D> {
|
|||||||
let playhead_x = state.point_at_frame(state.playhead).0;
|
let playhead_x = state.point_at_frame(state.playhead).0;
|
||||||
let decorator = self.decorator.clone();
|
let decorator = self.decorator.clone();
|
||||||
|
|
||||||
|
// Sequence markers → ruler paint data (the data source provides them
|
||||||
|
// sorted; the ruler colors them, falling back to the accent).
|
||||||
|
let marker_color = marker_accent_color();
|
||||||
|
let markers: Vec<RulerMarker> = source
|
||||||
|
.markers()
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| RulerMarker {
|
||||||
|
frame: m.frame,
|
||||||
|
color: m.color.unwrap_or(marker_color),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let ruler = div()
|
let ruler = div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_row()
|
.flex_row()
|
||||||
@@ -776,7 +911,29 @@ impl<D: TimelineDataSource> Render for TimelineView<D> {
|
|||||||
this.seek(frame, cx);
|
this.seek(frame, cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(TimelineRuler::new(state.clone(), frame_rate, seq_len)),
|
.on_drag(
|
||||||
|
Arc::new(RwLock::new(RulerDrag {
|
||||||
|
old_band: None,
|
||||||
|
new_band: None,
|
||||||
|
edge: None,
|
||||||
|
resizing: false,
|
||||||
|
})),
|
||||||
|
drag_ghost,
|
||||||
|
)
|
||||||
|
.on_drag_move(cx.listener(
|
||||||
|
|this, event: &DragMoveEvent<Arc<RwLock<RulerDrag>>>, _window, cx| {
|
||||||
|
this.update_ruler_drag(event, cx);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.on_drop(
|
||||||
|
cx.listener(|this, drag: &Arc<RwLock<RulerDrag>>, _window, cx| {
|
||||||
|
this.finish_ruler_drag(drag, cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
TimelineRuler::new(state.clone(), frame_rate, seq_len)
|
||||||
|
.markers(markers),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
let headers = div()
|
let headers = div()
|
||||||
@@ -1116,6 +1273,32 @@ impl<D: TimelineDataSource> Render for TimelineView<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which edge of a work-area band a ruler drag reshapes.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum EdgeKind {
|
||||||
|
/// The band's in (left) edge.
|
||||||
|
In,
|
||||||
|
/// The band's out (right) edge.
|
||||||
|
Out,
|
||||||
|
/// The whole band, replaced by the press-to-cursor range.
|
||||||
|
Whole,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared state for a ruler work-area drag. The drag starts on every ruler
|
||||||
|
/// press (alongside the seek), but only starts reshaping once the pointer
|
||||||
|
/// moves past the click threshold.
|
||||||
|
struct RulerDrag {
|
||||||
|
/// The work-area band at drag start; `None` when the drag creates a new
|
||||||
|
/// band.
|
||||||
|
old_band: Option<FrameRange>,
|
||||||
|
/// The band as the pointer moved.
|
||||||
|
new_band: Option<FrameRange>,
|
||||||
|
/// What the gesture reshapes, decided on the first real movement.
|
||||||
|
edge: Option<EdgeKind>,
|
||||||
|
/// Whether the pointer has passed the click threshold.
|
||||||
|
resizing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Shared state for a clip-move gesture (the clip wrapper starts it, the
|
/// Shared state for a clip-move gesture (the clip wrapper starts it, the
|
||||||
/// clip area updates and finishes it).
|
/// clip area updates and finishes it).
|
||||||
struct ClipDrag {
|
struct ClipDrag {
|
||||||
@@ -1205,6 +1388,81 @@ fn playhead_color() -> Hsla {
|
|||||||
hsla(0.0, 0.0, 0.9, 0.9)
|
hsla(0.0, 0.0, 0.9, 0.9)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reshapes the work-area band for a ruler drag move.
|
||||||
|
///
|
||||||
|
/// * `band` — the band at drag start (`None` = the drag creates a new one).
|
||||||
|
/// * `press_frame` / `current_frame` — the clamped press and pointer frames.
|
||||||
|
/// * `edge` — what the gesture reshapes (see [`EdgeKind`]).
|
||||||
|
///
|
||||||
|
/// `In`/`Out` keep at least one frame of band length; `Whole` replaces the
|
||||||
|
/// band with the press-to-pointer range, always `[start, end)` with
|
||||||
|
/// `end > start`.
|
||||||
|
fn reshape_work_area(
|
||||||
|
band: Option<FrameRange>,
|
||||||
|
press_frame: Frame,
|
||||||
|
current_frame: Frame,
|
||||||
|
edge: EdgeKind,
|
||||||
|
seq_len: i64,
|
||||||
|
) -> FrameRange {
|
||||||
|
let mut band = band.unwrap_or_else(|| FrameRange::new(Frame::ZERO, Frame(seq_len)));
|
||||||
|
match edge {
|
||||||
|
EdgeKind::In => band.start = Frame(current_frame.0.min((band.end.0 - 1).max(0))),
|
||||||
|
EdgeKind::Out => band.end = Frame(current_frame.0.max(band.start.0 + 1)),
|
||||||
|
EdgeKind::Whole => {
|
||||||
|
band = if press_frame.0 <= current_frame.0 {
|
||||||
|
FrameRange::new(
|
||||||
|
press_frame,
|
||||||
|
Frame(current_frame.0.max(press_frame.0 + 1)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
FrameRange::new(current_frame, Frame(press_frame.0.max(current_frame.0 + 1)))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
band
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default marker color (used when a marker carries no explicit color):
|
||||||
|
/// the theme-agnostic amber accent, distinct from the playhead and the
|
||||||
|
/// work-area band.
|
||||||
|
fn marker_accent_color() -> Hsla {
|
||||||
|
hsla(0.10, 0.85, 0.55, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reshape_work_area_moves_edges_within_bounds() {
|
||||||
|
let band = FrameRange::new(Frame(20), Frame(80));
|
||||||
|
// In edge: clamped so the band never collapses or inverts.
|
||||||
|
let reshaped = reshape_work_area(Some(band), Frame(20), Frame(50), EdgeKind::In, 1000);
|
||||||
|
assert_eq!(reshaped, FrameRange::new(Frame(50), Frame(80)));
|
||||||
|
let reshaped = reshape_work_area(Some(band), Frame(20), Frame(95), EdgeKind::In, 1000);
|
||||||
|
assert_eq!(reshaped, FrameRange::new(Frame(79), Frame(80)));
|
||||||
|
// Out edge.
|
||||||
|
let reshaped = reshape_work_area(Some(band), Frame(80), Frame(40), EdgeKind::Out, 1000);
|
||||||
|
assert_eq!(reshaped, FrameRange::new(Frame(20), Frame(40)));
|
||||||
|
let reshaped = reshape_work_area(Some(band), Frame(80), Frame(10), EdgeKind::Out, 1000);
|
||||||
|
assert_eq!(reshaped, FrameRange::new(Frame(20), Frame(21)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reshape_work_area_whole_replaces_or_creates_the_band() {
|
||||||
|
// Forward drag: press-to-cursor range.
|
||||||
|
let reshaped = reshape_work_area(Some(FrameRange::new(Frame(0), Frame(10))), Frame(30), Frame(70), EdgeKind::Whole, 1000);
|
||||||
|
assert_eq!(reshaped, FrameRange::new(Frame(30), Frame(70)));
|
||||||
|
// Backward drag: the range is normalized (start <= end).
|
||||||
|
let reshaped = reshape_work_area(Some(FrameRange::new(Frame(0), Frame(10))), Frame(70), Frame(30), EdgeKind::Whole, 1000);
|
||||||
|
assert_eq!(reshaped, FrameRange::new(Frame(30), Frame(70)));
|
||||||
|
// Creating a band from nothing uses the virtual 0..length band as the
|
||||||
|
// base for edge moves.
|
||||||
|
let reshaped = reshape_work_area(None, Frame(0), Frame(40), EdgeKind::Out, 1000);
|
||||||
|
assert_eq!(reshaped, FrameRange::new(Frame(0), Frame(40)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The default body color for clips on a track of `kind`.
|
/// The default body color for clips on a track of `kind`.
|
||||||
fn kind_color(kind: TrackKind) -> Hsla {
|
fn kind_color(kind: TrackKind) -> Hsla {
|
||||||
match kind {
|
match kind {
|
||||||
|
|||||||
Reference in New Issue
Block a user