Fix looping bugs
This commit is contained in:
parent
042dd50db3
commit
ce40147efa
|
|
@ -1,7 +1,7 @@
|
||||||
//! Loop clip instances action
|
//! Loop clip instances action
|
||||||
//!
|
//!
|
||||||
//! Handles extending clip instances beyond their content duration to enable looping,
|
//! Handles extending clip instances beyond their content duration to enable looping,
|
||||||
//! by setting timeline_duration on the ClipInstance.
|
//! by setting timeline_duration and/or loop_before on the ClipInstance.
|
||||||
|
|
||||||
use crate::action::Action;
|
use crate::action::Action;
|
||||||
use crate::document::Document;
|
use crate::document::Document;
|
||||||
|
|
@ -9,14 +9,17 @@ use crate::layer::AnyLayer;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Per-instance loop change: (instance_id, old_timeline_duration, new_timeline_duration, old_loop_before, new_loop_before)
|
||||||
|
pub type LoopEntry = (Uuid, Option<f64>, Option<f64>, Option<f64>, Option<f64>);
|
||||||
|
|
||||||
/// Action that changes the loop duration of clip instances
|
/// Action that changes the loop duration of clip instances
|
||||||
pub struct LoopClipInstancesAction {
|
pub struct LoopClipInstancesAction {
|
||||||
/// Map of layer IDs to vectors of (instance_id, old_timeline_duration, new_timeline_duration)
|
/// Map of layer IDs to vectors of loop entries
|
||||||
layer_loops: HashMap<Uuid, Vec<(Uuid, Option<f64>, Option<f64>)>>,
|
layer_loops: HashMap<Uuid, Vec<LoopEntry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoopClipInstancesAction {
|
impl LoopClipInstancesAction {
|
||||||
pub fn new(layer_loops: HashMap<Uuid, Vec<(Uuid, Option<f64>, Option<f64>)>>) -> Self {
|
pub fn new(layer_loops: HashMap<Uuid, Vec<LoopEntry>>) -> Self {
|
||||||
Self { layer_loops }
|
Self { layer_loops }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,9 +37,10 @@ impl Action for LoopClipInstancesAction {
|
||||||
AnyLayer::Effect(el) => &mut el.clip_instances,
|
AnyLayer::Effect(el) => &mut el.clip_instances,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (instance_id, _old, new) in loops {
|
for (instance_id, _old_dur, new_dur, _old_lb, new_lb) in loops {
|
||||||
if let Some(instance) = clip_instances.iter_mut().find(|ci| ci.id == *instance_id) {
|
if let Some(instance) = clip_instances.iter_mut().find(|ci| ci.id == *instance_id) {
|
||||||
instance.timeline_duration = *new;
|
instance.timeline_duration = *new_dur;
|
||||||
|
instance.loop_before = *new_lb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,9 +59,10 @@ impl Action for LoopClipInstancesAction {
|
||||||
AnyLayer::Effect(el) => &mut el.clip_instances,
|
AnyLayer::Effect(el) => &mut el.clip_instances,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (instance_id, old, _new) in loops {
|
for (instance_id, old_dur, _new_dur, old_lb, _new_lb) in loops {
|
||||||
if let Some(instance) = clip_instances.iter_mut().find(|ci| ci.id == *instance_id) {
|
if let Some(instance) = clip_instances.iter_mut().find(|ci| ci.id == *instance_id) {
|
||||||
instance.timeline_duration = *old;
|
instance.timeline_duration = *old_dur;
|
||||||
|
instance.loop_before = *old_lb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +107,7 @@ impl LoopClipInstancesAction {
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (instance_id, old, new) in loops {
|
for (instance_id, old_dur, new_dur, old_lb, new_lb) in loops {
|
||||||
let instance = clip_instances.iter()
|
let instance = clip_instances.iter()
|
||||||
.find(|ci| ci.id == *instance_id)
|
.find(|ci| ci.id == *instance_id)
|
||||||
.ok_or_else(|| format!("Clip instance {} not found", instance_id))?;
|
.ok_or_else(|| format!("Clip instance {} not found", instance_id))?;
|
||||||
|
|
@ -110,32 +115,39 @@ impl LoopClipInstancesAction {
|
||||||
let clip = document.get_audio_clip(&instance.clip_id)
|
let clip = document.get_audio_clip(&instance.clip_id)
|
||||||
.ok_or_else(|| format!("Audio clip {} not found", instance.clip_id))?;
|
.ok_or_else(|| format!("Audio clip {} not found", instance.clip_id))?;
|
||||||
|
|
||||||
// Determine which duration to send: on rollback use old, otherwise use new (current)
|
let (target_duration, target_loop_before) = if rollback {
|
||||||
let target_duration = if rollback { old } else { new };
|
(old_dur, old_lb)
|
||||||
|
} else {
|
||||||
|
(new_dur, new_lb)
|
||||||
|
};
|
||||||
|
|
||||||
// If timeline_duration is None, the external duration equals the content window
|
|
||||||
let content_window = {
|
let content_window = {
|
||||||
let trim_end = instance.trim_end.unwrap_or(clip.duration);
|
let trim_end = instance.trim_end.unwrap_or(clip.duration);
|
||||||
(trim_end - instance.trim_start).max(0.0)
|
(trim_end - instance.trim_start).max(0.0)
|
||||||
};
|
};
|
||||||
let external_duration = target_duration.unwrap_or(content_window);
|
let right_duration = target_duration.unwrap_or(content_window);
|
||||||
|
let left_duration = target_loop_before.unwrap_or(0.0);
|
||||||
|
let external_duration = left_duration + right_duration;
|
||||||
|
let external_start = instance.timeline_start - left_duration;
|
||||||
|
|
||||||
match &clip.clip_type {
|
let get_backend_clip_id = |inst_id: &Uuid| -> Result<u32, String> {
|
||||||
AudioClipType::Midi { midi_clip_id } => {
|
match &clip.clip_type {
|
||||||
controller.extend_clip(*track_id, *midi_clip_id, external_duration);
|
AudioClipType::Midi { midi_clip_id } => Ok(*midi_clip_id),
|
||||||
}
|
AudioClipType::Sampled { .. } => {
|
||||||
AudioClipType::Sampled { .. } => {
|
let backend_id = backend.clip_instance_to_backend_map.get(inst_id)
|
||||||
let backend_instance_id = backend.clip_instance_to_backend_map.get(instance_id)
|
.ok_or_else(|| format!("Clip instance {} not mapped to backend", inst_id))?;
|
||||||
.ok_or_else(|| format!("Clip instance {} not mapped to backend", instance_id))?;
|
match backend_id {
|
||||||
|
crate::action::BackendClipInstanceId::Audio(audio_id) => Ok(*audio_id),
|
||||||
match backend_instance_id {
|
_ => Err("Expected audio instance ID for sampled clip".to_string()),
|
||||||
crate::action::BackendClipInstanceId::Audio(audio_id) => {
|
|
||||||
controller.extend_clip(*track_id, *audio_id, external_duration);
|
|
||||||
}
|
}
|
||||||
_ => return Err("Expected audio instance ID for sampled clip".to_string()),
|
|
||||||
}
|
}
|
||||||
|
AudioClipType::Recording => Err("Cannot sync recording clip".to_string()),
|
||||||
}
|
}
|
||||||
AudioClipType::Recording => {}
|
};
|
||||||
|
|
||||||
|
if let Ok(backend_clip_id) = get_backend_clip_id(instance_id) {
|
||||||
|
controller.move_clip(*track_id, backend_clip_id, external_start);
|
||||||
|
controller.extend_clip(*track_id, backend_clip_id, external_duration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -573,6 +573,12 @@ pub struct ClipInstance {
|
||||||
/// Compatible with daw-backend's Clip.gain
|
/// Compatible with daw-backend's Clip.gain
|
||||||
/// Default: 1.0
|
/// Default: 1.0
|
||||||
pub gain: f32,
|
pub gain: f32,
|
||||||
|
|
||||||
|
/// How far (in seconds) the looped content extends before timeline_start.
|
||||||
|
/// When set, loop iterations are drawn/played before the content start.
|
||||||
|
/// Default: None (no pre-loop)
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub loop_before: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClipInstance {
|
impl ClipInstance {
|
||||||
|
|
@ -590,6 +596,7 @@ impl ClipInstance {
|
||||||
trim_end: None,
|
trim_end: None,
|
||||||
playback_speed: 1.0,
|
playback_speed: 1.0,
|
||||||
gain: 1.0,
|
gain: 1.0,
|
||||||
|
loop_before: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -607,6 +614,7 @@ impl ClipInstance {
|
||||||
trim_end: None,
|
trim_end: None,
|
||||||
playback_speed: 1.0,
|
playback_speed: 1.0,
|
||||||
gain: 1.0,
|
gain: 1.0,
|
||||||
|
loop_before: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -683,6 +691,17 @@ impl ClipInstance {
|
||||||
(end - self.trim_start).max(0.0)
|
(end - self.trim_start).max(0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the effective start position on the timeline, accounting for loop_before.
|
||||||
|
/// This is the left edge of the clip's visual extent.
|
||||||
|
pub fn effective_start(&self) -> f64 {
|
||||||
|
self.timeline_start - self.loop_before.unwrap_or(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the total visual duration including both loop_before and effective_duration.
|
||||||
|
pub fn total_duration(&self, clip_duration: f64) -> f64 {
|
||||||
|
self.loop_before.unwrap_or(0.0) + self.effective_duration(clip_duration)
|
||||||
|
}
|
||||||
|
|
||||||
/// Remap timeline time to clip content time
|
/// Remap timeline time to clip content time
|
||||||
///
|
///
|
||||||
/// Takes a global timeline time and returns the corresponding time within this
|
/// Takes a global timeline time and returns the corresponding time within this
|
||||||
|
|
|
||||||
|
|
@ -604,15 +604,13 @@ impl Document {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate instance end time
|
// Calculate instance extent (accounting for loop_before)
|
||||||
let Some(clip_duration) = self.get_clip_duration(&instance.clip_id) else {
|
let Some(clip_duration) = self.get_clip_duration(&instance.clip_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let instance_start = instance.timeline_start;
|
let instance_start = instance.effective_start();
|
||||||
let trim_start = instance.trim_start;
|
let instance_end = instance.timeline_start + instance.effective_duration(clip_duration);
|
||||||
let trim_end = instance.trim_end.unwrap_or(clip_duration);
|
|
||||||
let instance_end = instance_start + (trim_end - trim_start);
|
|
||||||
|
|
||||||
// Check overlap: start_a < end_b AND start_b < end_a
|
// Check overlap: start_a < end_b AND start_b < end_a
|
||||||
if start_time < instance_end && instance_start < end_time {
|
if start_time < instance_end && instance_start < end_time {
|
||||||
|
|
@ -667,10 +665,8 @@ impl Document {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(clip_dur) = self.get_clip_duration(&instance.clip_id) {
|
if let Some(clip_dur) = self.get_clip_duration(&instance.clip_id) {
|
||||||
let inst_start = instance.timeline_start;
|
let inst_start = instance.effective_start();
|
||||||
let trim_start = instance.trim_start;
|
let inst_end = instance.timeline_start + instance.effective_duration(clip_dur);
|
||||||
let trim_end = instance.trim_end.unwrap_or(clip_dur);
|
|
||||||
let inst_end = inst_start + (trim_end - trim_start);
|
|
||||||
occupied_ranges.push((inst_start, inst_end, instance.id));
|
occupied_ranges.push((inst_start, inst_end, instance.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -762,8 +758,9 @@ impl Document {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(dur) = self.get_clip_duration(&inst.clip_id) {
|
if let Some(dur) = self.get_clip_duration(&inst.clip_id) {
|
||||||
let end = inst.timeline_start + (inst.trim_end.unwrap_or(dur) - inst.trim_start);
|
let start = inst.effective_start();
|
||||||
non_group.push((inst.timeline_start, end));
|
let end = inst.timeline_start + inst.effective_duration(dur);
|
||||||
|
non_group.push((start, end));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -828,10 +825,9 @@ impl Document {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate other clip's end time
|
// Calculate other clip's extent (accounting for loop_before)
|
||||||
if let Some(clip_duration) = self.get_clip_duration(&other.clip_id) {
|
if let Some(clip_duration) = self.get_clip_duration(&other.clip_id) {
|
||||||
let trim_end = other.trim_end.unwrap_or(clip_duration);
|
let other_end = other.timeline_start + other.effective_duration(clip_duration);
|
||||||
let other_end = other.timeline_start + (trim_end - other.trim_start);
|
|
||||||
|
|
||||||
// If this clip is to the left and closer than current nearest
|
// If this clip is to the left and closer than current nearest
|
||||||
if other_end <= current_timeline_start && other_end > nearest_end {
|
if other_end <= current_timeline_start && other_end > nearest_end {
|
||||||
|
|
@ -878,9 +874,10 @@ impl Document {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this clip is to the right and closer than current nearest
|
// Use effective_start to account for loop_before on the other clip
|
||||||
if other.timeline_start >= current_end && other.timeline_start < nearest_start {
|
let other_start = other.effective_start();
|
||||||
nearest_start = other.timeline_start;
|
if other_start >= current_end && other_start < nearest_start {
|
||||||
|
nearest_start = other_start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -890,6 +887,48 @@ impl Document {
|
||||||
(nearest_start - current_end).max(0.0) // Gap between our end and next clip's start
|
(nearest_start - current_end).max(0.0) // Gap between our end and next clip's start
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Find the maximum amount we can extend loop_before to the left without overlapping.
|
||||||
|
///
|
||||||
|
/// Returns the max additional loop_before distance (from the current effective start).
|
||||||
|
pub fn find_max_loop_extend_left(
|
||||||
|
&self,
|
||||||
|
layer_id: &Uuid,
|
||||||
|
instance_id: &Uuid,
|
||||||
|
current_effective_start: f64,
|
||||||
|
) -> f64 {
|
||||||
|
let Some(layer) = self.get_layer(layer_id) else {
|
||||||
|
return current_effective_start;
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches!(layer, AnyLayer::Vector(_)) {
|
||||||
|
return current_effective_start;
|
||||||
|
}
|
||||||
|
|
||||||
|
let instances: &[ClipInstance] = match layer {
|
||||||
|
AnyLayer::Audio(audio) => &audio.clip_instances,
|
||||||
|
AnyLayer::Video(video) => &video.clip_instances,
|
||||||
|
AnyLayer::Effect(effect) => &effect.clip_instances,
|
||||||
|
AnyLayer::Vector(vector) => &vector.clip_instances,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut nearest_end = 0.0;
|
||||||
|
|
||||||
|
for other in instances {
|
||||||
|
if &other.id == instance_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(clip_duration) = self.get_clip_duration(&other.clip_id) {
|
||||||
|
let other_end = other.timeline_start + other.effective_duration(clip_duration);
|
||||||
|
|
||||||
|
if other_end <= current_effective_start && other_end > nearest_end {
|
||||||
|
nearest_end = other_end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_effective_start - nearest_end
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
//! Custom cursor system
|
||||||
|
//!
|
||||||
|
//! Provides SVG-based custom cursors beyond egui's built-in system cursors.
|
||||||
|
//! When a custom cursor is active, the system cursor is hidden and the SVG
|
||||||
|
//! cursor image is drawn at the pointer position.
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
use egui::TextureHandle;
|
||||||
|
use lightningbeam_core::tool::Tool;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Custom cursor identifiers
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum CustomCursor {
|
||||||
|
// Stage tool cursors
|
||||||
|
Select,
|
||||||
|
Draw,
|
||||||
|
Transform,
|
||||||
|
Rectangle,
|
||||||
|
Ellipse,
|
||||||
|
PaintBucket,
|
||||||
|
Eyedropper,
|
||||||
|
Line,
|
||||||
|
Polygon,
|
||||||
|
BezierEdit,
|
||||||
|
Text,
|
||||||
|
// Timeline cursors
|
||||||
|
LoopExtend,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomCursor {
|
||||||
|
/// Convert a Tool enum to the corresponding custom cursor
|
||||||
|
pub fn from_tool(tool: Tool) -> Self {
|
||||||
|
match tool {
|
||||||
|
Tool::Select => CustomCursor::Select,
|
||||||
|
Tool::Draw => CustomCursor::Draw,
|
||||||
|
Tool::Transform => CustomCursor::Transform,
|
||||||
|
Tool::Rectangle => CustomCursor::Rectangle,
|
||||||
|
Tool::Ellipse => CustomCursor::Ellipse,
|
||||||
|
Tool::PaintBucket => CustomCursor::PaintBucket,
|
||||||
|
Tool::Eyedropper => CustomCursor::Eyedropper,
|
||||||
|
Tool::Line => CustomCursor::Line,
|
||||||
|
Tool::Polygon => CustomCursor::Polygon,
|
||||||
|
Tool::BezierEdit => CustomCursor::BezierEdit,
|
||||||
|
Tool::Text => CustomCursor::Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hotspot offset — the "click point" relative to the image top-left
|
||||||
|
pub fn hotspot(&self) -> egui::Vec2 {
|
||||||
|
match self {
|
||||||
|
// Select cursor: pointer tip at top-left
|
||||||
|
CustomCursor::Select => egui::vec2(3.0, 1.0),
|
||||||
|
// Drawing tools: tip at bottom-left
|
||||||
|
CustomCursor::Draw => egui::vec2(1.0, 23.0),
|
||||||
|
// Transform: center
|
||||||
|
CustomCursor::Transform => egui::vec2(12.0, 12.0),
|
||||||
|
// Shape tools: crosshair at center
|
||||||
|
CustomCursor::Rectangle
|
||||||
|
| CustomCursor::Ellipse
|
||||||
|
| CustomCursor::Line
|
||||||
|
| CustomCursor::Polygon => egui::vec2(12.0, 12.0),
|
||||||
|
// Paint bucket: tip at bottom-left
|
||||||
|
CustomCursor::PaintBucket => egui::vec2(2.0, 21.0),
|
||||||
|
// Eyedropper: tip at bottom
|
||||||
|
CustomCursor::Eyedropper => egui::vec2(4.0, 22.0),
|
||||||
|
// Bezier edit: tip at top-left
|
||||||
|
CustomCursor::BezierEdit => egui::vec2(3.0, 1.0),
|
||||||
|
// Text: I-beam center
|
||||||
|
CustomCursor::Text => egui::vec2(12.0, 12.0),
|
||||||
|
// Loop extend: center of circular arrow
|
||||||
|
CustomCursor::LoopExtend => egui::vec2(12.0, 12.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the embedded SVG data for this cursor
|
||||||
|
fn svg_data(&self) -> &'static [u8] {
|
||||||
|
match self {
|
||||||
|
CustomCursor::Select => include_bytes!("../../../src/assets/select.svg"),
|
||||||
|
CustomCursor::Draw => include_bytes!("../../../src/assets/draw.svg"),
|
||||||
|
CustomCursor::Transform => include_bytes!("../../../src/assets/transform.svg"),
|
||||||
|
CustomCursor::Rectangle => include_bytes!("../../../src/assets/rectangle.svg"),
|
||||||
|
CustomCursor::Ellipse => include_bytes!("../../../src/assets/ellipse.svg"),
|
||||||
|
CustomCursor::PaintBucket => include_bytes!("../../../src/assets/paint_bucket.svg"),
|
||||||
|
CustomCursor::Eyedropper => include_bytes!("../../../src/assets/eyedropper.svg"),
|
||||||
|
CustomCursor::Line => include_bytes!("../../../src/assets/line.svg"),
|
||||||
|
CustomCursor::Polygon => include_bytes!("../../../src/assets/polygon.svg"),
|
||||||
|
CustomCursor::BezierEdit => include_bytes!("../../../src/assets/bezier_edit.svg"),
|
||||||
|
CustomCursor::Text => include_bytes!("../../../src/assets/text.svg"),
|
||||||
|
CustomCursor::LoopExtend => include_bytes!("../../../src/assets/arrow-counterclockwise.svg"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cache of rasterized cursor textures (black fill + white outline version)
|
||||||
|
pub struct CursorCache {
|
||||||
|
/// Black cursor for the main image
|
||||||
|
textures: HashMap<CustomCursor, TextureHandle>,
|
||||||
|
/// White cursor for the outline
|
||||||
|
outline_textures: HashMap<CustomCursor, TextureHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorCache {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
textures: HashMap::new(),
|
||||||
|
outline_textures: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get or lazily load the black (fill) cursor texture
|
||||||
|
pub fn get_or_load(&mut self, cursor: CustomCursor, ctx: &egui::Context) -> &TextureHandle {
|
||||||
|
self.textures.entry(cursor).or_insert_with(|| {
|
||||||
|
let svg_data = cursor.svg_data();
|
||||||
|
let svg_string = String::from_utf8_lossy(svg_data);
|
||||||
|
let svg_with_color = svg_string.replace("currentColor", "#000000");
|
||||||
|
rasterize_cursor_svg(svg_with_color.as_bytes(), &format!("cursor_{:?}", cursor), CURSOR_SIZE, ctx)
|
||||||
|
.expect("Failed to rasterize cursor SVG")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get or lazily load the white (outline) cursor texture
|
||||||
|
pub fn get_or_load_outline(&mut self, cursor: CustomCursor, ctx: &egui::Context) -> &TextureHandle {
|
||||||
|
self.outline_textures.entry(cursor).or_insert_with(|| {
|
||||||
|
let svg_data = cursor.svg_data();
|
||||||
|
let svg_string = String::from_utf8_lossy(svg_data);
|
||||||
|
// Replace all colors with white for the outline
|
||||||
|
let svg_white = svg_string
|
||||||
|
.replace("currentColor", "#ffffff")
|
||||||
|
.replace("#000000", "#ffffff")
|
||||||
|
.replace("#000", "#ffffff");
|
||||||
|
rasterize_cursor_svg(svg_white.as_bytes(), &format!("cursor_{:?}_outline", cursor), CURSOR_SIZE, ctx)
|
||||||
|
.expect("Failed to rasterize cursor SVG outline")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CURSOR_SIZE: u32 = 24;
|
||||||
|
const OUTLINE_OFFSET: f32 = 1.0;
|
||||||
|
|
||||||
|
/// Rasterize an SVG into an egui texture (same approach as main.rs rasterize_svg)
|
||||||
|
fn rasterize_cursor_svg(
|
||||||
|
svg_data: &[u8],
|
||||||
|
name: &str,
|
||||||
|
render_size: u32,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
) -> Option<TextureHandle> {
|
||||||
|
let tree = resvg::usvg::Tree::from_data(svg_data, &resvg::usvg::Options::default()).ok()?;
|
||||||
|
let pixmap_size = tree.size().to_int_size();
|
||||||
|
let scale_x = render_size as f32 / pixmap_size.width() as f32;
|
||||||
|
let scale_y = render_size as f32 / pixmap_size.height() as f32;
|
||||||
|
let mut pixmap = resvg::tiny_skia::Pixmap::new(render_size, render_size)?;
|
||||||
|
resvg::render(
|
||||||
|
&tree,
|
||||||
|
resvg::tiny_skia::Transform::from_scale(scale_x, scale_y),
|
||||||
|
&mut pixmap.as_mut(),
|
||||||
|
);
|
||||||
|
let rgba_data = pixmap.data().to_vec();
|
||||||
|
let color_image = egui::ColorImage::from_rgba_unmultiplied(
|
||||||
|
[render_size as usize, render_size as usize],
|
||||||
|
&rgba_data,
|
||||||
|
);
|
||||||
|
Some(ctx.load_texture(name, color_image, egui::TextureOptions::LINEAR))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Per-frame cursor slot using egui context data ---
|
||||||
|
|
||||||
|
/// Key for storing the active custom cursor in egui's per-frame data
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct ActiveCustomCursor(CustomCursor);
|
||||||
|
|
||||||
|
/// Set the custom cursor for this frame. Call from any pane during rendering.
|
||||||
|
/// This hides the system cursor and draws the SVG cursor at pointer position.
|
||||||
|
pub fn set(ctx: &egui::Context, cursor: CustomCursor) {
|
||||||
|
ctx.data_mut(|d| d.insert_temp(egui::Id::new("active_custom_cursor"), ActiveCustomCursor(cursor)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the custom cursor overlay. Call at the end of the main update loop.
|
||||||
|
pub fn render_overlay(ctx: &egui::Context, cache: &mut CursorCache) {
|
||||||
|
// Take and remove the cursor so it doesn't persist to the next frame
|
||||||
|
let id = egui::Id::new("active_custom_cursor");
|
||||||
|
let cursor = ctx.data_mut(|d| {
|
||||||
|
let val = d.get_temp::<ActiveCustomCursor>(id);
|
||||||
|
d.remove::<ActiveCustomCursor>(id);
|
||||||
|
val
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ActiveCustomCursor(cursor)) = cursor {
|
||||||
|
// If a system cursor was explicitly set (resize handles, text inputs, etc.),
|
||||||
|
// let it take priority over the custom cursor
|
||||||
|
let system_cursor = ctx.output(|o| o.cursor_icon);
|
||||||
|
if system_cursor != egui::CursorIcon::Default {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the system cursor
|
||||||
|
ctx.set_cursor_icon(egui::CursorIcon::None);
|
||||||
|
|
||||||
|
if let Some(pos) = ctx.input(|i| i.pointer.latest_pos()) {
|
||||||
|
let hotspot = cursor.hotspot();
|
||||||
|
let size = egui::vec2(CURSOR_SIZE as f32, CURSOR_SIZE as f32);
|
||||||
|
let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
|
||||||
|
let painter = ctx.debug_painter();
|
||||||
|
|
||||||
|
// Draw white outline: render white version offset in 8 directions
|
||||||
|
let outline_tex = cache.get_or_load_outline(cursor, ctx);
|
||||||
|
let outline_id = outline_tex.id();
|
||||||
|
for &(dx, dy) in &[
|
||||||
|
(-OUTLINE_OFFSET, 0.0), (OUTLINE_OFFSET, 0.0),
|
||||||
|
(0.0, -OUTLINE_OFFSET), (0.0, OUTLINE_OFFSET),
|
||||||
|
(-OUTLINE_OFFSET, -OUTLINE_OFFSET), (OUTLINE_OFFSET, -OUTLINE_OFFSET),
|
||||||
|
(-OUTLINE_OFFSET, OUTLINE_OFFSET), (OUTLINE_OFFSET, OUTLINE_OFFSET),
|
||||||
|
] {
|
||||||
|
let offset_rect = egui::Rect::from_min_size(
|
||||||
|
pos - hotspot + egui::vec2(dx, dy),
|
||||||
|
size,
|
||||||
|
);
|
||||||
|
painter.image(outline_id, offset_rect, uv, egui::Color32::WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw black fill on top
|
||||||
|
let fill_tex = cache.get_or_load(cursor, ctx);
|
||||||
|
let cursor_rect = egui::Rect::from_min_size(pos - hotspot, size);
|
||||||
|
painter.image(fill_tex.id(), cursor_rect, uv, egui::Color32::WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ mod notifications;
|
||||||
mod effect_thumbnails;
|
mod effect_thumbnails;
|
||||||
use effect_thumbnails::EffectThumbnailGenerator;
|
use effect_thumbnails::EffectThumbnailGenerator;
|
||||||
|
|
||||||
|
mod custom_cursor;
|
||||||
mod debug_overlay;
|
mod debug_overlay;
|
||||||
|
|
||||||
mod sample_import;
|
mod sample_import;
|
||||||
|
|
@ -732,6 +733,8 @@ struct EditorApp {
|
||||||
/// GPU-rendered effect thumbnail generator
|
/// GPU-rendered effect thumbnail generator
|
||||||
effect_thumbnail_generator: Option<EffectThumbnailGenerator>,
|
effect_thumbnail_generator: Option<EffectThumbnailGenerator>,
|
||||||
|
|
||||||
|
/// Custom cursor cache for SVG cursors
|
||||||
|
cursor_cache: custom_cursor::CursorCache,
|
||||||
/// Debug overlay (F3) state
|
/// Debug overlay (F3) state
|
||||||
debug_overlay_visible: bool,
|
debug_overlay_visible: bool,
|
||||||
debug_stats_collector: debug_overlay::DebugStatsCollector,
|
debug_stats_collector: debug_overlay::DebugStatsCollector,
|
||||||
|
|
@ -926,6 +929,7 @@ impl EditorApp {
|
||||||
effect_thumbnail_generator: None, // Initialized when GPU available
|
effect_thumbnail_generator: None, // Initialized when GPU available
|
||||||
|
|
||||||
// Debug overlay (F3)
|
// Debug overlay (F3)
|
||||||
|
cursor_cache: custom_cursor::CursorCache::new(),
|
||||||
debug_overlay_visible: false,
|
debug_overlay_visible: false,
|
||||||
debug_stats_collector: debug_overlay::DebugStatsCollector::new(),
|
debug_stats_collector: debug_overlay::DebugStatsCollector::new(),
|
||||||
gpu_info,
|
gpu_info,
|
||||||
|
|
@ -4696,6 +4700,9 @@ impl eframe::App for EditorApp {
|
||||||
debug_overlay::render_debug_overlay(ctx, &stats);
|
debug_overlay::render_debug_overlay(ctx, &stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render custom cursor overlay (on top of everything including debug overlay)
|
||||||
|
custom_cursor::render_overlay(ctx, &mut self.cursor_cache);
|
||||||
|
|
||||||
let frame_ms = _frame_start.elapsed().as_secs_f64() * 1000.0;
|
let frame_ms = _frame_start.elapsed().as_secs_f64() * 1000.0;
|
||||||
if frame_ms > 50.0 {
|
if frame_ms > 50.0 {
|
||||||
eprintln!("[TIMING] SLOW FRAME: {:.1}ms (pre-events={:.1}, events={:.1}, post-events={:.1})",
|
eprintln!("[TIMING] SLOW FRAME: {:.1}ms (pre-events={:.1}, events={:.1}, post-events={:.1})",
|
||||||
|
|
|
||||||
|
|
@ -6314,6 +6314,17 @@ impl PaneRenderer for StagePane {
|
||||||
|
|
||||||
// Render vector editing overlays (vertices, control points, etc.)
|
// Render vector editing overlays (vertices, control points, etc.)
|
||||||
self.render_vector_editing_overlays(ui, rect, shared);
|
self.render_vector_editing_overlays(ui, rect, shared);
|
||||||
|
|
||||||
|
// Set custom tool cursor when pointer is over the stage canvas
|
||||||
|
// (system cursors from transform handles take priority via render_overlay check)
|
||||||
|
if let Some(pos) = ui.input(|i| i.pointer.hover_pos()) {
|
||||||
|
if rect.contains(pos) {
|
||||||
|
crate::custom_cursor::set(
|
||||||
|
ui.ctx(),
|
||||||
|
crate::custom_cursor::CustomCursor::from_tool(*shared.selected_tool),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ enum ClipDragType {
|
||||||
Move,
|
Move,
|
||||||
TrimLeft,
|
TrimLeft,
|
||||||
TrimRight,
|
TrimRight,
|
||||||
LoopExtend,
|
LoopExtendRight,
|
||||||
|
LoopExtendLeft,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TimelinePane {
|
pub struct TimelinePane {
|
||||||
|
|
@ -194,7 +195,7 @@ impl TimelinePane {
|
||||||
match layer_type {
|
match layer_type {
|
||||||
AudioLayerType::Midi => {
|
AudioLayerType::Midi => {
|
||||||
// Create backend MIDI clip and start recording
|
// Create backend MIDI clip and start recording
|
||||||
let clip_id = controller.create_midi_clip(track_id, start_time, 4.0);
|
let clip_id = controller.create_midi_clip(track_id, start_time, 0.0);
|
||||||
controller.start_midi_recording(track_id, clip_id, start_time);
|
controller.start_midi_recording(track_id, clip_id, start_time);
|
||||||
shared.recording_clips.insert(active_layer_id, clip_id);
|
shared.recording_clips.insert(active_layer_id, clip_id);
|
||||||
println!("🎹 Started MIDI recording on track {:?} at {:.2}s, clip_id={}",
|
println!("🎹 Started MIDI recording on track {:?} at {:.2}s, clip_id={}",
|
||||||
|
|
@ -204,7 +205,7 @@ impl TimelinePane {
|
||||||
drop(controller);
|
drop(controller);
|
||||||
|
|
||||||
// Create document clip + clip instance immediately (clip_id is known synchronously)
|
// Create document clip + clip instance immediately (clip_id is known synchronously)
|
||||||
let doc_clip = AudioClip::new_midi("Recording...", clip_id, 4.0);
|
let doc_clip = AudioClip::new_midi("Recording...", clip_id, 0.0);
|
||||||
let doc_clip_id = shared.action_executor.document_mut().add_audio_clip(doc_clip);
|
let doc_clip_id = shared.action_executor.document_mut().add_audio_clip(doc_clip);
|
||||||
|
|
||||||
let clip_instance = ClipInstance::new(doc_clip_id)
|
let clip_instance = ClipInstance::new(doc_clip_id)
|
||||||
|
|
@ -339,8 +340,8 @@ impl TimelinePane {
|
||||||
}
|
}
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let instance_duration = clip_instance.effective_duration(clip_duration);
|
let instance_start = clip_instance.effective_start();
|
||||||
let instance_start = clip_instance.timeline_start;
|
let instance_duration = clip_instance.total_duration(clip_duration);
|
||||||
let instance_end = instance_start + instance_duration;
|
let instance_end = instance_start + instance_duration;
|
||||||
|
|
||||||
if hover_time >= instance_start && hover_time <= instance_end {
|
if hover_time >= instance_start && hover_time <= instance_end {
|
||||||
|
|
@ -353,12 +354,20 @@ impl TimelinePane {
|
||||||
let layer_top = header_rect.min.y + (hovered_layer_index as f32 * LAYER_HEIGHT) - self.viewport_scroll_y;
|
let layer_top = header_rect.min.y + (hovered_layer_index as f32 * LAYER_HEIGHT) - self.viewport_scroll_y;
|
||||||
let mouse_in_top_corner = pointer_pos.y < layer_top + LOOP_CORNER_SIZE;
|
let mouse_in_top_corner = pointer_pos.y < layer_top + LOOP_CORNER_SIZE;
|
||||||
|
|
||||||
|
let is_looping = clip_instance.timeline_duration.is_some() || clip_instance.loop_before.is_some();
|
||||||
let drag_type = if (mouse_x - start_x).abs() <= EDGE_DETECTION_PIXELS {
|
let drag_type = if (mouse_x - start_x).abs() <= EDGE_DETECTION_PIXELS {
|
||||||
ClipDragType::TrimLeft
|
// Left edge: loop extend left for audio clips that are looping or top-left corner
|
||||||
|
let mouse_in_top_left_corner = pointer_pos.y < layer_top + LOOP_CORNER_SIZE;
|
||||||
|
if is_audio_layer && (is_looping || mouse_in_top_left_corner) {
|
||||||
|
ClipDragType::LoopExtendLeft
|
||||||
|
} else {
|
||||||
|
ClipDragType::TrimLeft
|
||||||
|
}
|
||||||
} else if (end_x - mouse_x).abs() <= EDGE_DETECTION_PIXELS {
|
} else if (end_x - mouse_x).abs() <= EDGE_DETECTION_PIXELS {
|
||||||
// Top-right corner of audio clips = loop extend
|
// If already looping, right edge is always loop extend
|
||||||
if is_audio_layer && mouse_in_top_corner {
|
// Otherwise, top-right corner of audio clips = loop extend
|
||||||
ClipDragType::LoopExtend
|
if is_audio_layer && (is_looping || mouse_in_top_corner) {
|
||||||
|
ClipDragType::LoopExtendRight
|
||||||
} else {
|
} else {
|
||||||
ClipDragType::TrimRight
|
ClipDragType::TrimRight
|
||||||
}
|
}
|
||||||
|
|
@ -1026,7 +1035,7 @@ impl TimelinePane {
|
||||||
.filter(|ci| selection.contains_clip_instance(&ci.id))
|
.filter(|ci| selection.contains_clip_instance(&ci.id))
|
||||||
.filter_map(|ci| {
|
.filter_map(|ci| {
|
||||||
let dur = document.get_clip_duration(&ci.clip_id)?;
|
let dur = document.get_clip_duration(&ci.clip_id)?;
|
||||||
Some((ci.id, ci.timeline_start, ci.effective_duration(dur)))
|
Some((ci.id, ci.effective_start(), ci.total_duration(dur)))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if !group.is_empty() {
|
if !group.is_empty() {
|
||||||
|
|
@ -1060,13 +1069,13 @@ impl TimelinePane {
|
||||||
|
|
||||||
if let Some(clip_duration) = clip_duration {
|
if let Some(clip_duration) = clip_duration {
|
||||||
// Calculate effective duration accounting for trimming
|
// Calculate effective duration accounting for trimming
|
||||||
let mut instance_duration = clip_instance.effective_duration(clip_duration);
|
let mut instance_duration = clip_instance.total_duration(clip_duration);
|
||||||
|
|
||||||
// Instance positioned on the layer's timeline using timeline_start
|
// Instance positioned on the layer's timeline using timeline_start
|
||||||
// The layer itself has start_time, so the absolute timeline position is:
|
// The layer itself has start_time, so the absolute timeline position is:
|
||||||
// layer.start_time + instance.timeline_start
|
// layer.start_time + instance.timeline_start
|
||||||
let _layer_data = layer.layer();
|
let _layer_data = layer.layer();
|
||||||
let mut instance_start = clip_instance.timeline_start;
|
let mut instance_start = clip_instance.effective_start();
|
||||||
|
|
||||||
// Apply drag offset preview for selected clips with snapping
|
// Apply drag offset preview for selected clips with snapping
|
||||||
let is_selected = selection.contains_clip_instance(&clip_instance.id);
|
let is_selected = selection.contains_clip_instance(&clip_instance.id);
|
||||||
|
|
@ -1085,6 +1094,10 @@ impl TimelinePane {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Content origin: where the first "real" content iteration starts
|
||||||
|
// Loop iterations tile outward from this point
|
||||||
|
let mut content_origin = clip_instance.timeline_start;
|
||||||
|
|
||||||
// Track preview trim values for waveform rendering
|
// Track preview trim values for waveform rendering
|
||||||
let mut preview_trim_start = clip_instance.trim_start;
|
let mut preview_trim_start = clip_instance.trim_start;
|
||||||
let mut preview_clip_duration = clip_duration;
|
let mut preview_clip_duration = clip_duration;
|
||||||
|
|
@ -1094,7 +1107,8 @@ impl TimelinePane {
|
||||||
match drag_type {
|
match drag_type {
|
||||||
ClipDragType::Move => {
|
ClipDragType::Move => {
|
||||||
if let Some(offset) = group_move_offset {
|
if let Some(offset) = group_move_offset {
|
||||||
instance_start = (clip_instance.timeline_start + offset).max(0.0);
|
instance_start = (clip_instance.effective_start() + offset).max(0.0);
|
||||||
|
content_origin = instance_start + clip_instance.loop_before.unwrap_or(0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClipDragType::TrimLeft => {
|
ClipDragType::TrimLeft => {
|
||||||
|
|
@ -1108,7 +1122,7 @@ impl TimelinePane {
|
||||||
let max_extend = document.find_max_trim_extend_left(
|
let max_extend = document.find_max_trim_extend_left(
|
||||||
&layer.id(),
|
&layer.id(),
|
||||||
&clip_instance.id,
|
&clip_instance.id,
|
||||||
clip_instance.timeline_start,
|
clip_instance.effective_start(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let desired_extend = clip_instance.trim_start - desired_trim_start;
|
let desired_extend = clip_instance.trim_start - desired_trim_start;
|
||||||
|
|
@ -1124,6 +1138,7 @@ impl TimelinePane {
|
||||||
// Move start and reduce duration by actual clamped offset
|
// Move start and reduce duration by actual clamped offset
|
||||||
instance_start = (clip_instance.timeline_start + actual_offset)
|
instance_start = (clip_instance.timeline_start + actual_offset)
|
||||||
.max(0.0);
|
.max(0.0);
|
||||||
|
|
||||||
instance_duration = (clip_duration - new_trim_start).max(0.0);
|
instance_duration = (clip_duration - new_trim_start).max(0.0);
|
||||||
|
|
||||||
// Adjust for existing trim_end
|
// Adjust for existing trim_end
|
||||||
|
|
@ -1166,22 +1181,62 @@ impl TimelinePane {
|
||||||
// (the waveform system uses clip_duration to determine visible range)
|
// (the waveform system uses clip_duration to determine visible range)
|
||||||
preview_clip_duration = new_trim_end - preview_trim_start;
|
preview_clip_duration = new_trim_end - preview_trim_start;
|
||||||
}
|
}
|
||||||
ClipDragType::LoopExtend => {
|
ClipDragType::LoopExtendRight => {
|
||||||
// Loop extend: extend clip beyond content window
|
// Loop extend right: extend clip beyond content window
|
||||||
// Use trimmed content window, NOT effective_duration (which includes loop extension)
|
|
||||||
let trim_end = clip_instance.trim_end.unwrap_or(clip_duration);
|
let trim_end = clip_instance.trim_end.unwrap_or(clip_duration);
|
||||||
let content_window = (trim_end - clip_instance.trim_start).max(0.0);
|
let content_window = (trim_end - clip_instance.trim_start).max(0.0);
|
||||||
let desired_total = (content_window + self.drag_offset).max(content_window * 0.25);
|
let current_right = clip_instance.timeline_duration.unwrap_or(content_window);
|
||||||
|
let desired_right = (current_right + self.drag_offset).max(content_window);
|
||||||
|
|
||||||
// Check for adjacent clips
|
let new_right = if desired_right > current_right {
|
||||||
let max_extend = document.find_max_trim_extend_right(
|
let max_extend = document.find_max_trim_extend_right(
|
||||||
&layer.id(),
|
&layer.id(),
|
||||||
&clip_instance.id,
|
&clip_instance.id,
|
||||||
clip_instance.timeline_start,
|
clip_instance.timeline_start,
|
||||||
content_window,
|
current_right,
|
||||||
);
|
);
|
||||||
let extend_amount = (desired_total - content_window).min(max_extend).max(0.0);
|
let extend_amount = (desired_right - current_right).min(max_extend);
|
||||||
instance_duration = content_window + extend_amount;
|
current_right + extend_amount
|
||||||
|
} else {
|
||||||
|
desired_right
|
||||||
|
};
|
||||||
|
|
||||||
|
// Total duration = loop_before + right duration
|
||||||
|
let loop_before = clip_instance.loop_before.unwrap_or(0.0);
|
||||||
|
instance_duration = loop_before + new_right;
|
||||||
|
}
|
||||||
|
ClipDragType::LoopExtendLeft => {
|
||||||
|
// Loop extend left: extend loop_before (pre-loop region)
|
||||||
|
// Snap to multiples of content_window so iterations align with backend
|
||||||
|
let trim_end = clip_instance.trim_end.unwrap_or(clip_duration);
|
||||||
|
let content_window = (trim_end - clip_instance.trim_start).max(0.001);
|
||||||
|
let current_loop_before = clip_instance.loop_before.unwrap_or(0.0);
|
||||||
|
// Invert: dragging left (negative offset) = extend
|
||||||
|
let desired_loop_before = (current_loop_before - self.drag_offset).max(0.0);
|
||||||
|
// Snap to whole iterations
|
||||||
|
let desired_iters = (desired_loop_before / content_window).round();
|
||||||
|
let snapped_loop_before = desired_iters * content_window;
|
||||||
|
|
||||||
|
let new_loop_before = if snapped_loop_before > current_loop_before {
|
||||||
|
// Extending left - check for adjacent clips
|
||||||
|
let max_extend = document.find_max_loop_extend_left(
|
||||||
|
&layer.id(),
|
||||||
|
&clip_instance.id,
|
||||||
|
clip_instance.effective_start(),
|
||||||
|
);
|
||||||
|
let extend_amount = (snapped_loop_before - current_loop_before).min(max_extend);
|
||||||
|
// Re-snap after clamping
|
||||||
|
let clamped = current_loop_before + extend_amount;
|
||||||
|
(clamped / content_window).floor() * content_window
|
||||||
|
} else {
|
||||||
|
snapped_loop_before
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recompute instance_start and instance_duration
|
||||||
|
let right_duration = clip_instance.effective_duration(clip_duration);
|
||||||
|
instance_start = clip_instance.timeline_start - new_loop_before;
|
||||||
|
instance_duration = new_loop_before + right_duration;
|
||||||
|
content_origin = clip_instance.timeline_start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1237,14 +1292,33 @@ impl TimelinePane {
|
||||||
let is_looping_bg = instance_duration > content_window_for_bg + 0.001 && content_window_for_bg > 0.0;
|
let is_looping_bg = instance_duration > content_window_for_bg + 0.001 && content_window_for_bg > 0.0;
|
||||||
|
|
||||||
if is_looping_bg {
|
if is_looping_bg {
|
||||||
let num_bg_iters = ((instance_duration / content_window_for_bg).ceil() as usize).max(1);
|
// Compute iterations aligned to content_origin
|
||||||
|
let loop_before_val = content_origin - instance_start;
|
||||||
|
let pre_iters = if loop_before_val > 0.001 {
|
||||||
|
(loop_before_val / content_window_for_bg).ceil() as usize
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let right_duration = instance_duration - loop_before_val;
|
||||||
|
let post_iters = if right_duration > 0.001 {
|
||||||
|
(right_duration / content_window_for_bg).ceil() as usize
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
let total_iters = pre_iters + post_iters;
|
||||||
|
|
||||||
let faded_color = egui::Color32::from_rgba_unmultiplied(
|
let faded_color = egui::Color32::from_rgba_unmultiplied(
|
||||||
clip_color.r(), clip_color.g(), clip_color.b(),
|
clip_color.r(), clip_color.g(), clip_color.b(),
|
||||||
(clip_color.a() as f32 * 0.55) as u8,
|
(clip_color.a() as f32 * 0.55) as u8,
|
||||||
);
|
);
|
||||||
for i in 0..num_bg_iters {
|
for i in 0..total_iters {
|
||||||
let iter_time_start = instance_start + i as f64 * content_window_for_bg;
|
let signed_i = i as i64 - pre_iters as i64;
|
||||||
let iter_time_end = (iter_time_start + content_window_for_bg).min(instance_start + instance_duration);
|
let iter_time_start_raw = content_origin + signed_i as f64 * content_window_for_bg;
|
||||||
|
let iter_time_end_raw = iter_time_start_raw + content_window_for_bg;
|
||||||
|
let iter_time_start = iter_time_start_raw.max(instance_start);
|
||||||
|
let iter_time_end = iter_time_end_raw.min(instance_start + instance_duration);
|
||||||
|
if iter_time_end <= iter_time_start { continue; }
|
||||||
|
|
||||||
let ix0 = (rect.min.x + ((iter_time_start - self.viewport_start_time) * self.pixels_per_second as f64) as f32).max(clip_rect.min.x);
|
let ix0 = (rect.min.x + ((iter_time_start - self.viewport_start_time) * self.pixels_per_second as f64) as f32).max(clip_rect.min.x);
|
||||||
let ix1 = (rect.min.x + ((iter_time_end - self.viewport_start_time) * self.pixels_per_second as f64) as f32).min(clip_rect.max.x);
|
let ix1 = (rect.min.x + ((iter_time_end - self.viewport_start_time) * self.pixels_per_second as f64) as f32).min(clip_rect.max.x);
|
||||||
if ix1 > ix0 {
|
if ix1 > ix0 {
|
||||||
|
|
@ -1252,7 +1326,7 @@ impl TimelinePane {
|
||||||
egui::pos2(ix0, clip_rect.min.y),
|
egui::pos2(ix0, clip_rect.min.y),
|
||||||
egui::pos2(ix1, clip_rect.max.y),
|
egui::pos2(ix1, clip_rect.max.y),
|
||||||
);
|
);
|
||||||
let color = if i == 0 { clip_color } else { faded_color };
|
let color = if signed_i == 0 { clip_color } else { faded_color };
|
||||||
painter.rect_filled(iter_rect, 3.0, color);
|
painter.rect_filled(iter_rect, 3.0, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1275,35 +1349,52 @@ impl TimelinePane {
|
||||||
let preview_trim_end = clip_instance.trim_end.unwrap_or(clip_duration);
|
let preview_trim_end = clip_instance.trim_end.unwrap_or(clip_duration);
|
||||||
let content_window = (preview_trim_end - preview_trim_start).max(0.0);
|
let content_window = (preview_trim_end - preview_trim_start).max(0.0);
|
||||||
let is_looping = instance_duration > content_window + 0.001;
|
let is_looping = instance_duration > content_window + 0.001;
|
||||||
let num_iterations = if is_looping && content_window > 0.0 {
|
|
||||||
((instance_duration / content_window).ceil() as usize).max(1)
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
};
|
|
||||||
|
|
||||||
for iteration in 0..num_iterations {
|
if is_looping && content_window > 0.0 {
|
||||||
let iter_offset = iteration as f64 * content_window;
|
// Compute iterations aligned to content_origin
|
||||||
let iter_start = instance_start + iter_offset;
|
let lb_val = content_origin - instance_start;
|
||||||
let iter_end = (iter_start + content_window).min(instance_start + instance_duration);
|
let pre = if lb_val > 0.001 { (lb_val / content_window).ceil() as usize } else { 0 };
|
||||||
let iter_duration = iter_end - iter_start;
|
let right_dur = instance_duration - lb_val;
|
||||||
|
let post = if right_dur > 0.001 { (right_dur / content_window).ceil() as usize } else { 1 };
|
||||||
|
|
||||||
if iter_duration <= 0.0 {
|
for i in 0..(pre + post) {
|
||||||
break;
|
let si = i as i64 - pre as i64;
|
||||||
|
let iter_start_raw = content_origin + si as f64 * content_window;
|
||||||
|
let iter_end_raw = iter_start_raw + content_window;
|
||||||
|
let iter_start = iter_start_raw.max(instance_start);
|
||||||
|
let iter_end = iter_end_raw.min(instance_start + instance_duration);
|
||||||
|
let iter_duration = iter_end - iter_start;
|
||||||
|
if iter_duration <= 0.0 { continue; }
|
||||||
|
|
||||||
|
Self::render_midi_piano_roll(
|
||||||
|
painter,
|
||||||
|
clip_rect,
|
||||||
|
rect.min.x,
|
||||||
|
events,
|
||||||
|
clip_instance.trim_start,
|
||||||
|
iter_duration,
|
||||||
|
iter_start,
|
||||||
|
self.viewport_start_time,
|
||||||
|
self.pixels_per_second,
|
||||||
|
theme,
|
||||||
|
ui.ctx(),
|
||||||
|
si != 0, // fade non-content iterations
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
Self::render_midi_piano_roll(
|
Self::render_midi_piano_roll(
|
||||||
painter,
|
painter,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
rect.min.x,
|
rect.min.x,
|
||||||
events,
|
events,
|
||||||
clip_instance.trim_start,
|
clip_instance.trim_start,
|
||||||
iter_duration,
|
instance_duration,
|
||||||
iter_start,
|
instance_start,
|
||||||
self.viewport_start_time,
|
self.viewport_start_time,
|
||||||
self.pixels_per_second,
|
self.pixels_per_second,
|
||||||
theme,
|
theme,
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
iteration > 0, // fade subsequent iterations
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1338,21 +1429,31 @@ impl TimelinePane {
|
||||||
let preview_trim_end = clip_instance.trim_end.unwrap_or(clip_duration);
|
let preview_trim_end = clip_instance.trim_end.unwrap_or(clip_duration);
|
||||||
let content_window = (preview_trim_end - preview_trim_start).max(0.0);
|
let content_window = (preview_trim_end - preview_trim_start).max(0.0);
|
||||||
let is_looping = instance_duration > content_window + 0.001;
|
let is_looping = instance_duration > content_window + 0.001;
|
||||||
let num_iterations = if is_looping && content_window > 0.0 {
|
|
||||||
((instance_duration / content_window).ceil() as usize).max(1)
|
// Compute iterations aligned to content_origin
|
||||||
|
let lb_val = content_origin - instance_start;
|
||||||
|
let pre_w = if is_looping && lb_val > 0.001 { (lb_val / content_window).ceil() as usize } else { 0 };
|
||||||
|
let right_dur_w = instance_duration - lb_val;
|
||||||
|
let post_w = if is_looping && content_window > 0.0 {
|
||||||
|
(right_dur_w / content_window).ceil() as usize
|
||||||
} else {
|
} else {
|
||||||
1
|
1
|
||||||
};
|
};
|
||||||
|
let total_w = pre_w + post_w;
|
||||||
|
|
||||||
for iteration in 0..num_iterations {
|
for wi in 0..total_w {
|
||||||
let iter_offset = iteration as f64 * content_window;
|
let si_w = wi as i64 - pre_w as i64;
|
||||||
let iter_start = instance_start + iter_offset;
|
let (iter_start, iter_duration) = if is_looping {
|
||||||
let iter_end = (iter_start + content_window).min(instance_start + instance_duration);
|
let raw_start = content_origin + si_w as f64 * content_window;
|
||||||
let iter_duration = iter_end - iter_start;
|
let raw_end = raw_start + content_window;
|
||||||
|
let s = raw_start.max(instance_start);
|
||||||
|
let e = raw_end.min(instance_start + instance_duration);
|
||||||
|
(s, (e - s).max(0.0))
|
||||||
|
} else {
|
||||||
|
(instance_start, instance_duration)
|
||||||
|
};
|
||||||
|
|
||||||
if iter_duration <= 0.0 {
|
if iter_duration <= 0.0 { continue; }
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let iter_screen_start = rect.min.x + ((iter_start - self.viewport_start_time) * self.pixels_per_second as f64) as f32;
|
let iter_screen_start = rect.min.x + ((iter_start - self.viewport_start_time) * self.pixels_per_second as f64) as f32;
|
||||||
let iter_screen_end = iter_screen_start + (iter_duration * self.pixels_per_second as f64) as f32;
|
let iter_screen_end = iter_screen_start + (iter_duration * self.pixels_per_second as f64) as f32;
|
||||||
|
|
@ -1362,7 +1463,8 @@ impl TimelinePane {
|
||||||
);
|
);
|
||||||
|
|
||||||
if waveform_rect.width() > 0.0 && waveform_rect.height() > 0.0 {
|
if waveform_rect.width() > 0.0 && waveform_rect.height() > 0.0 {
|
||||||
let instance_id = clip_instance.id.as_u128() as u64 + iteration as u64;
|
let instance_id = clip_instance.id.as_u128() as u64 + wi as u64;
|
||||||
|
let is_loop_iter = si_w != 0;
|
||||||
let callback = crate::waveform_gpu::WaveformCallback {
|
let callback = crate::waveform_gpu::WaveformCallback {
|
||||||
pool_index: *audio_pool_index,
|
pool_index: *audio_pool_index,
|
||||||
segment_index: 0,
|
segment_index: 0,
|
||||||
|
|
@ -1379,7 +1481,7 @@ impl TimelinePane {
|
||||||
segment_start_frame: 0.0,
|
segment_start_frame: 0.0,
|
||||||
display_mode: if waveform_stereo { 1.0 } else { 0.0 },
|
display_mode: if waveform_stereo { 1.0 } else { 0.0 },
|
||||||
_pad1: [0.0, 0.0],
|
_pad1: [0.0, 0.0],
|
||||||
tint_color: if iteration > 0 {
|
tint_color: if is_loop_iter {
|
||||||
[tint[0], tint[1], tint[2], tint[3] * 0.5]
|
[tint[0], tint[1], tint[2], tint[3] * 0.5]
|
||||||
} else {
|
} else {
|
||||||
tint
|
tint
|
||||||
|
|
@ -1388,7 +1490,7 @@ impl TimelinePane {
|
||||||
_pad: [0.0, 0.0],
|
_pad: [0.0, 0.0],
|
||||||
},
|
},
|
||||||
target_format,
|
target_format,
|
||||||
pending_upload: if iteration == 0 { pending_upload.clone() } else { None },
|
pending_upload: if wi == 0 { pending_upload.clone() } else { None },
|
||||||
instance_id,
|
instance_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1494,10 +1596,18 @@ impl TimelinePane {
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_looping_bg {
|
if is_looping_bg {
|
||||||
let num_border_iters = ((instance_duration / content_window_for_bg).ceil() as usize).max(1);
|
// Aligned to content_origin (same as bg rendering)
|
||||||
for i in 0..num_border_iters {
|
let lb_border = content_origin - instance_start;
|
||||||
let iter_time_start = instance_start + i as f64 * content_window_for_bg;
|
let pre_b = if lb_border > 0.001 { (lb_border / content_window_for_bg).ceil() as usize } else { 0 };
|
||||||
let iter_time_end = (iter_time_start + content_window_for_bg).min(instance_start + instance_duration);
|
let right_b = instance_duration - lb_border;
|
||||||
|
let post_b = if right_b > 0.001 { (right_b / content_window_for_bg).ceil() as usize } else { 1 };
|
||||||
|
for i in 0..(pre_b + post_b) {
|
||||||
|
let si_b = i as i64 - pre_b as i64;
|
||||||
|
let iter_time_start_raw = content_origin + si_b as f64 * content_window_for_bg;
|
||||||
|
let iter_time_end_raw = iter_time_start_raw + content_window_for_bg;
|
||||||
|
let iter_time_start = iter_time_start_raw.max(instance_start);
|
||||||
|
let iter_time_end = iter_time_end_raw.min(instance_start + instance_duration);
|
||||||
|
if iter_time_end <= iter_time_start { continue; }
|
||||||
let ix0 = (rect.min.x + ((iter_time_start - self.viewport_start_time) * self.pixels_per_second as f64) as f32).max(clip_rect.min.x);
|
let ix0 = (rect.min.x + ((iter_time_start - self.viewport_start_time) * self.pixels_per_second as f64) as f32).max(clip_rect.min.x);
|
||||||
let ix1 = (rect.min.x + ((iter_time_end - self.viewport_start_time) * self.pixels_per_second as f64) as f32).min(clip_rect.max.x);
|
let ix1 = (rect.min.x + ((iter_time_end - self.viewport_start_time) * self.pixels_per_second as f64) as f32).min(clip_rect.max.x);
|
||||||
if ix1 > ix0 {
|
if ix1 > ix0 {
|
||||||
|
|
@ -1637,8 +1747,8 @@ impl TimelinePane {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(clip_duration) = clip_duration {
|
if let Some(clip_duration) = clip_duration {
|
||||||
let instance_duration = clip_instance.effective_duration(clip_duration);
|
let instance_duration = clip_instance.total_duration(clip_duration);
|
||||||
let instance_start = clip_instance.timeline_start;
|
let instance_start = clip_instance.effective_start();
|
||||||
let instance_end = instance_start + instance_duration;
|
let instance_end = instance_start + instance_duration;
|
||||||
|
|
||||||
// Check if click is within this clip instance's time range
|
// Check if click is within this clip instance's time range
|
||||||
|
|
@ -1945,8 +2055,8 @@ impl TimelinePane {
|
||||||
pending_actions.push(action);
|
pending_actions.push(action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClipDragType::LoopExtend => {
|
ClipDragType::LoopExtendRight => {
|
||||||
let mut layer_loops: HashMap<uuid::Uuid, Vec<(uuid::Uuid, Option<f64>, Option<f64>)>> = HashMap::new();
|
let mut layer_loops: HashMap<uuid::Uuid, Vec<lightningbeam_core::actions::loop_clip_instances::LoopEntry>> = HashMap::new();
|
||||||
|
|
||||||
for layer in &document.root.children {
|
for layer in &document.root.children {
|
||||||
let layer_id = layer.id();
|
let layer_id = layer.id();
|
||||||
|
|
@ -1967,25 +2077,27 @@ impl TimelinePane {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(clip_duration) = clip_duration {
|
if let Some(clip_duration) = clip_duration {
|
||||||
// Use trimmed content window, NOT effective_duration (which includes loop extension)
|
|
||||||
let trim_end = clip_instance.trim_end.unwrap_or(clip_duration);
|
let trim_end = clip_instance.trim_end.unwrap_or(clip_duration);
|
||||||
let content_window = (trim_end - clip_instance.trim_start).max(0.0);
|
let content_window = (trim_end - clip_instance.trim_start).max(0.0);
|
||||||
let desired_total = content_window + self.drag_offset;
|
let current_right = clip_instance.timeline_duration.unwrap_or(content_window);
|
||||||
|
let desired_right = current_right + self.drag_offset;
|
||||||
|
|
||||||
// Check for adjacent clips
|
let new_right = if desired_right > current_right {
|
||||||
let max_extend = document.find_max_trim_extend_right(
|
let max_extend = document.find_max_trim_extend_right(
|
||||||
&layer_id,
|
&layer_id,
|
||||||
&clip_instance.id,
|
&clip_instance.id,
|
||||||
clip_instance.timeline_start,
|
clip_instance.timeline_start,
|
||||||
content_window,
|
current_right,
|
||||||
);
|
);
|
||||||
let extend_amount = (desired_total - content_window).min(max_extend).max(0.0);
|
let extend_amount = (desired_right - current_right).min(max_extend);
|
||||||
let new_total = content_window + extend_amount;
|
current_right + extend_amount
|
||||||
|
} else {
|
||||||
|
desired_right
|
||||||
|
};
|
||||||
|
|
||||||
let old_timeline_duration = clip_instance.timeline_duration;
|
let old_timeline_duration = clip_instance.timeline_duration;
|
||||||
// Only set timeline_duration if extending beyond content
|
let new_timeline_duration = if new_right > content_window + 0.001 {
|
||||||
let new_timeline_duration = if new_total > content_window + 0.001 {
|
Some(new_right)
|
||||||
Some(new_total)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -1994,7 +2106,89 @@ impl TimelinePane {
|
||||||
layer_loops
|
layer_loops
|
||||||
.entry(layer_id)
|
.entry(layer_id)
|
||||||
.or_insert_with(Vec::new)
|
.or_insert_with(Vec::new)
|
||||||
.push((clip_instance.id, old_timeline_duration, new_timeline_duration));
|
.push((
|
||||||
|
clip_instance.id,
|
||||||
|
old_timeline_duration,
|
||||||
|
new_timeline_duration,
|
||||||
|
clip_instance.loop_before,
|
||||||
|
clip_instance.loop_before, // loop_before unchanged
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !layer_loops.is_empty() {
|
||||||
|
let action = Box::new(
|
||||||
|
lightningbeam_core::actions::LoopClipInstancesAction::new(layer_loops),
|
||||||
|
);
|
||||||
|
pending_actions.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClipDragType::LoopExtendLeft => {
|
||||||
|
// Extend loop_before (pre-loop region)
|
||||||
|
let mut layer_loops: HashMap<uuid::Uuid, Vec<lightningbeam_core::actions::loop_clip_instances::LoopEntry>> = HashMap::new();
|
||||||
|
|
||||||
|
for layer in &document.root.children {
|
||||||
|
let layer_id = layer.id();
|
||||||
|
let clip_instances = match layer {
|
||||||
|
lightningbeam_core::layer::AnyLayer::Vector(vl) => &vl.clip_instances,
|
||||||
|
lightningbeam_core::layer::AnyLayer::Audio(al) => &al.clip_instances,
|
||||||
|
lightningbeam_core::layer::AnyLayer::Video(vl) => &vl.clip_instances,
|
||||||
|
lightningbeam_core::layer::AnyLayer::Effect(el) => &el.clip_instances,
|
||||||
|
};
|
||||||
|
|
||||||
|
for clip_instance in clip_instances {
|
||||||
|
if selection.contains_clip_instance(&clip_instance.id) {
|
||||||
|
let clip_duration = match layer {
|
||||||
|
lightningbeam_core::layer::AnyLayer::Audio(_) => {
|
||||||
|
document.get_audio_clip(&clip_instance.clip_id).map(|c| c.duration)
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(clip_duration) = clip_duration {
|
||||||
|
let trim_end = clip_instance.trim_end.unwrap_or(clip_duration);
|
||||||
|
let content_window = (trim_end - clip_instance.trim_start).max(0.001);
|
||||||
|
let current_loop_before = clip_instance.loop_before.unwrap_or(0.0);
|
||||||
|
// Invert: dragging left (negative offset) = extend
|
||||||
|
let desired_loop_before = (current_loop_before - self.drag_offset).max(0.0);
|
||||||
|
// Snap to whole iterations so backend modulo aligns
|
||||||
|
let desired_iters = (desired_loop_before / content_window).round();
|
||||||
|
let snapped = desired_iters * content_window;
|
||||||
|
|
||||||
|
let new_loop_before = if snapped > current_loop_before {
|
||||||
|
let max_extend = document.find_max_loop_extend_left(
|
||||||
|
&layer_id,
|
||||||
|
&clip_instance.id,
|
||||||
|
clip_instance.effective_start(),
|
||||||
|
);
|
||||||
|
let extend_amount = (snapped - current_loop_before).min(max_extend);
|
||||||
|
let clamped = current_loop_before + extend_amount;
|
||||||
|
(clamped / content_window).floor() * content_window
|
||||||
|
} else {
|
||||||
|
snapped
|
||||||
|
};
|
||||||
|
|
||||||
|
let old_loop_before = clip_instance.loop_before;
|
||||||
|
let new_lb = if new_loop_before > 0.001 {
|
||||||
|
Some(new_loop_before)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if old_loop_before != new_lb {
|
||||||
|
layer_loops
|
||||||
|
.entry(layer_id)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push((
|
||||||
|
clip_instance.id,
|
||||||
|
clip_instance.timeline_duration,
|
||||||
|
clip_instance.timeline_duration, // timeline_duration unchanged
|
||||||
|
old_loop_before,
|
||||||
|
new_lb,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2161,8 +2355,8 @@ impl TimelinePane {
|
||||||
ClipDragType::TrimLeft | ClipDragType::TrimRight => {
|
ClipDragType::TrimLeft | ClipDragType::TrimRight => {
|
||||||
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
|
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
|
||||||
}
|
}
|
||||||
ClipDragType::LoopExtend => {
|
ClipDragType::LoopExtendRight | ClipDragType::LoopExtendLeft => {
|
||||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Alias);
|
crate::custom_cursor::set(ui.ctx(), crate::custom_cursor::CustomCursor::LoopExtend);
|
||||||
}
|
}
|
||||||
ClipDragType::Move => {}
|
ClipDragType::Move => {}
|
||||||
}
|
}
|
||||||
|
|
@ -2178,8 +2372,8 @@ impl TimelinePane {
|
||||||
ClipDragType::TrimLeft | ClipDragType::TrimRight => {
|
ClipDragType::TrimLeft | ClipDragType::TrimRight => {
|
||||||
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
|
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
|
||||||
}
|
}
|
||||||
ClipDragType::LoopExtend => {
|
ClipDragType::LoopExtendRight | ClipDragType::LoopExtendLeft => {
|
||||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Alias);
|
crate::custom_cursor::set(ui.ctx(), crate::custom_cursor::CustomCursor::LoopExtend);
|
||||||
}
|
}
|
||||||
ClipDragType::Move => {}
|
ClipDragType::Move => {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue