Clean up build warnings
This commit is contained in:
parent
82b58ae0dc
commit
777d3ef6be
|
|
@ -72,7 +72,7 @@ pub fn export_audio<P: AsRef<Path>>(
|
|||
midi_pool: &MidiClipPool,
|
||||
settings: &ExportSettings,
|
||||
output_path: P,
|
||||
mut event_tx: Option<&mut rtrb::Producer<AudioEvent>>,
|
||||
event_tx: Option<&mut rtrb::Producer<AudioEvent>>,
|
||||
) -> Result<(), String>
|
||||
{
|
||||
// Route to appropriate export implementation based on format
|
||||
|
|
@ -435,8 +435,6 @@ fn export_mp3<P: AsRef<Path>>(
|
|||
channel_layout,
|
||||
pts,
|
||||
)?;
|
||||
|
||||
frames_rendered += final_frame_size;
|
||||
}
|
||||
|
||||
// Flush encoder
|
||||
|
|
@ -602,8 +600,6 @@ fn export_aac<P: AsRef<Path>>(
|
|||
channel_layout,
|
||||
pts,
|
||||
)?;
|
||||
|
||||
frames_rendered += final_frame_size;
|
||||
}
|
||||
|
||||
// Flush encoder
|
||||
|
|
@ -617,35 +613,6 @@ fn export_aac<P: AsRef<Path>>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert interleaved f32 samples to planar i16 format
|
||||
fn convert_to_planar_i16(interleaved: &[f32], channels: u32) -> Vec<Vec<i16>> {
|
||||
let num_frames = interleaved.len() / channels as usize;
|
||||
let mut planar = vec![vec![0i16; num_frames]; channels as usize];
|
||||
|
||||
for (i, chunk) in interleaved.chunks(channels as usize).enumerate() {
|
||||
for (ch, &sample) in chunk.iter().enumerate() {
|
||||
let clamped = sample.max(-1.0).min(1.0);
|
||||
planar[ch][i] = (clamped * 32767.0) as i16;
|
||||
}
|
||||
}
|
||||
|
||||
planar
|
||||
}
|
||||
|
||||
/// Convert interleaved f32 samples to planar f32 format
|
||||
fn convert_to_planar_f32(interleaved: &[f32], channels: u32) -> Vec<Vec<f32>> {
|
||||
let num_frames = interleaved.len() / channels as usize;
|
||||
let mut planar = vec![vec![0.0f32; num_frames]; channels as usize];
|
||||
|
||||
for (i, chunk) in interleaved.chunks(channels as usize).enumerate() {
|
||||
for (ch, &sample) in chunk.iter().enumerate() {
|
||||
planar[ch][i] = sample;
|
||||
}
|
||||
}
|
||||
|
||||
planar
|
||||
}
|
||||
|
||||
/// Convert a chunk of interleaved f32 samples to planar i16 format
|
||||
fn convert_chunk_to_planar_i16(interleaved: &[f32], channels: u32) -> Vec<Vec<i16>> {
|
||||
let num_frames = interleaved.len() / channels as usize;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ pub struct WaveformCache {
|
|||
chunks: HashMap<WaveformChunkKey, Vec<WaveformPeak>>,
|
||||
|
||||
/// Maximum memory usage in MB (for future LRU eviction)
|
||||
max_memory_mb: usize,
|
||||
_max_memory_mb: usize,
|
||||
|
||||
/// Current memory usage estimate in bytes
|
||||
current_memory_bytes: usize,
|
||||
|
|
@ -75,7 +75,7 @@ impl WaveformCache {
|
|||
pub fn new(max_memory_mb: usize) -> Self {
|
||||
Self {
|
||||
chunks: HashMap::new(),
|
||||
max_memory_mb,
|
||||
_max_memory_mb: max_memory_mb,
|
||||
current_memory_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ impl Action for MoveClipInstancesAction {
|
|||
let mut expanded_moves = self.layer_moves.clone();
|
||||
let mut already_processed = std::collections::HashSet::new();
|
||||
|
||||
for (layer_id, moves) in &self.layer_moves {
|
||||
for (_layer_id, moves) in &self.layer_moves {
|
||||
for (instance_id, old_start, new_start) in moves {
|
||||
// Skip if already processed
|
||||
if already_processed.contains(instance_id) {
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ pub struct PaintBucketAction {
|
|||
fill_color: ShapeColor,
|
||||
|
||||
/// Tolerance for gap bridging (in pixels)
|
||||
tolerance: f64,
|
||||
_tolerance: f64,
|
||||
|
||||
/// Gap handling mode
|
||||
gap_mode: GapHandlingMode,
|
||||
_gap_mode: GapHandlingMode,
|
||||
|
||||
/// ID of the created shape (set after execution)
|
||||
created_shape_id: Option<Uuid>,
|
||||
|
|
@ -59,8 +59,8 @@ impl PaintBucketAction {
|
|||
layer_id,
|
||||
click_point,
|
||||
fill_color,
|
||||
tolerance,
|
||||
gap_mode,
|
||||
_tolerance: tolerance,
|
||||
_gap_mode: gap_mode,
|
||||
created_shape_id: None,
|
||||
created_shape_instance_id: None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,26 +68,6 @@ impl SetInstancePropertiesAction {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_instance_value(&self, document: &Document, instance_id: &Uuid) -> Option<f64> {
|
||||
if let Some(layer) = document.get_layer(&self.layer_id) {
|
||||
if let AnyLayer::Vector(vector_layer) = layer {
|
||||
if let Some(instance) = vector_layer.get_object(instance_id) {
|
||||
return Some(match &self.property {
|
||||
InstancePropertyChange::X(_) => instance.transform.x,
|
||||
InstancePropertyChange::Y(_) => instance.transform.y,
|
||||
InstancePropertyChange::Rotation(_) => instance.transform.rotation,
|
||||
InstancePropertyChange::ScaleX(_) => instance.transform.scale_x,
|
||||
InstancePropertyChange::ScaleY(_) => instance.transform.scale_y,
|
||||
InstancePropertyChange::SkewX(_) => instance.transform.skew_x,
|
||||
InstancePropertyChange::SkewY(_) => instance.transform.skew_y,
|
||||
InstancePropertyChange::Opacity(_) => instance.opacity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn apply_to_instance(&self, document: &mut Document, instance_id: &Uuid, value: f64) {
|
||||
if let Some(layer) = document.get_layer_mut(&self.layer_id) {
|
||||
if let AnyLayer::Vector(vector_layer) = layer {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ impl Action for TrimClipInstancesAction {
|
|||
let mut expanded_trims = self.layer_trims.clone();
|
||||
let mut already_processed = std::collections::HashSet::new();
|
||||
|
||||
for (layer_id, trims) in &self.layer_trims {
|
||||
for (_layer_id, trims) in &self.layer_trims {
|
||||
for (instance_id, trim_type, old, new) in trims {
|
||||
// Skip if already processed
|
||||
if already_processed.contains(instance_id) {
|
||||
|
|
@ -189,7 +189,7 @@ impl Action for TrimClipInstancesAction {
|
|||
|
||||
match trim_type {
|
||||
TrimType::TrimLeft => {
|
||||
if let (Some(old_trim), Some(new_trim), Some(old_timeline), Some(new_timeline)) =
|
||||
if let (Some(old_trim), Some(new_trim), Some(old_timeline), Some(_new_timeline)) =
|
||||
(old.trim_value, new.trim_value, old.timeline_start, new.timeline_start)
|
||||
{
|
||||
// If extending to the left (new_trim < old_trim)
|
||||
|
|
@ -365,7 +365,7 @@ impl Action for TrimClipInstancesAction {
|
|||
.ok_or_else(|| format!("Layer {} not mapped to backend track", layer_id))?;
|
||||
|
||||
// Process each clip instance trim
|
||||
for (instance_id, trim_type, _old, new) in trims {
|
||||
for (instance_id, _trim_type, _old, _new) in trims {
|
||||
// Get clip instances from the layer
|
||||
let clip_instances = match layer {
|
||||
AnyLayer::Audio(al) => &al.clip_instances,
|
||||
|
|
|
|||
|
|
@ -229,27 +229,6 @@ pub fn find_closest_approach(
|
|||
}
|
||||
}
|
||||
|
||||
/// Refine intersection parameters using Newton's method
|
||||
fn refine_intersection(
|
||||
curve1: &CubicBez,
|
||||
curve2: &CubicBez,
|
||||
mut t1: f64,
|
||||
mut t2: f64,
|
||||
) -> (f64, f64) {
|
||||
// Simple refinement: just find nearest points iteratively
|
||||
for _ in 0..5 {
|
||||
let p1 = curve1.eval(t1);
|
||||
let nearest2 = curve2.nearest(p1, 1e-6);
|
||||
t2 = nearest2.t;
|
||||
|
||||
let p2 = curve2.eval(t2);
|
||||
let nearest1 = curve1.nearest(p2, 1e-6);
|
||||
t1 = nearest1.t;
|
||||
}
|
||||
|
||||
(t1.clamp(0.0, 1.0), t2.clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
/// Refine self-intersection parameters
|
||||
fn refine_self_intersection(curve: &CubicBez, mut t1: f64, mut t2: f64) -> (f64, f64) {
|
||||
// Refine by moving parameters closer to where curves actually meet
|
||||
|
|
|
|||
|
|
@ -189,22 +189,6 @@ impl EffectLayer {
|
|||
self.clip_instances = new_order;
|
||||
}
|
||||
|
||||
// === MUTATION METHODS (pub(crate) - only accessible to action module) ===
|
||||
|
||||
/// Add a clip instance (internal, for actions only)
|
||||
pub(crate) fn add_clip_instance_internal(&mut self, instance: ClipInstance) -> Uuid {
|
||||
self.add_clip_instance(instance)
|
||||
}
|
||||
|
||||
/// Remove a clip instance (internal, for actions only)
|
||||
pub(crate) fn remove_clip_instance_internal(&mut self, id: &Uuid) -> Option<ClipInstance> {
|
||||
self.remove_clip_instance(id)
|
||||
}
|
||||
|
||||
/// Insert a clip instance at a specific index (internal, for actions only)
|
||||
pub(crate) fn insert_clip_instance_internal(&mut self, index: usize, instance: ClipInstance) -> Uuid {
|
||||
self.insert_clip_instance(index, instance)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -455,23 +455,23 @@ struct CurveIntersection {
|
|||
t_on_current: f64,
|
||||
|
||||
/// Parameter on other curve
|
||||
t_on_other: f64,
|
||||
_t_on_other: f64,
|
||||
|
||||
/// ID of the other curve
|
||||
other_curve_id: usize,
|
||||
_other_curve_id: usize,
|
||||
|
||||
/// Intersection point
|
||||
point: Point,
|
||||
|
||||
/// Whether this is a gap (within tolerance but not exact intersection)
|
||||
is_gap: bool,
|
||||
_is_gap: bool,
|
||||
}
|
||||
|
||||
/// Find all intersections on a given curve
|
||||
fn find_intersections_on_curve(
|
||||
curve_id: usize,
|
||||
curves: &[CubicBez],
|
||||
processed_curves: &HashSet<usize>,
|
||||
_processed_curves: &HashSet<usize>,
|
||||
quadtree: &ToleranceQuadtree,
|
||||
tolerance: f64,
|
||||
debug_info: &mut WalkDebugInfo,
|
||||
|
|
@ -489,10 +489,10 @@ fn find_intersections_on_curve(
|
|||
for int in self_ints {
|
||||
intersections.push(CurveIntersection {
|
||||
t_on_current: int.t1,
|
||||
t_on_other: int.t2.unwrap_or(int.t1),
|
||||
other_curve_id: curve_id,
|
||||
_t_on_other: int.t2.unwrap_or(int.t1),
|
||||
_other_curve_id: curve_id,
|
||||
point: int.point,
|
||||
is_gap: false,
|
||||
_is_gap: false,
|
||||
});
|
||||
debug_info.intersections_found += 1;
|
||||
}
|
||||
|
|
@ -504,10 +504,10 @@ fn find_intersections_on_curve(
|
|||
for int in exact_ints {
|
||||
intersections.push(CurveIntersection {
|
||||
t_on_current: int.t1,
|
||||
t_on_other: int.t2.unwrap_or(0.0),
|
||||
other_curve_id: other_id,
|
||||
_t_on_other: int.t2.unwrap_or(0.0),
|
||||
_other_curve_id: other_id,
|
||||
point: int.point,
|
||||
is_gap: false,
|
||||
_is_gap: false,
|
||||
});
|
||||
debug_info.intersections_found += 1;
|
||||
}
|
||||
|
|
@ -516,10 +516,10 @@ fn find_intersections_on_curve(
|
|||
if let Some(approach) = find_closest_approach(current_curve, other_curve, tolerance) {
|
||||
intersections.push(CurveIntersection {
|
||||
t_on_current: approach.t1,
|
||||
t_on_other: approach.t2,
|
||||
other_curve_id: other_id,
|
||||
_t_on_other: approach.t2,
|
||||
_other_curve_id: other_id,
|
||||
point: approach.p1,
|
||||
is_gap: true,
|
||||
_is_gap: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@ fn map_t_to_relative_distances(bez: &[Point; 4], b_parts: usize) -> Vec<f64> {
|
|||
}
|
||||
|
||||
/// Find t value for a given parameter distance
|
||||
fn find_t(bez: &[Point; 4], param: f64, t_dist_map: &[f64], b_parts: usize) -> f64 {
|
||||
fn find_t(_bez: &[Point; 4], param: f64, t_dist_map: &[f64], b_parts: usize) -> f64 {
|
||||
if param < 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ impl PlanarGraph {
|
|||
|
||||
// Initialize with endpoints for all curves
|
||||
for (i, curve) in curves.iter().enumerate() {
|
||||
let mut curve_intersections = vec![
|
||||
let curve_intersections = vec![
|
||||
(0.0, curve.p0),
|
||||
(1.0, curve.p3),
|
||||
];
|
||||
|
|
@ -202,7 +202,7 @@ impl PlanarGraph {
|
|||
|
||||
/// Build nodes and edges from curves and their intersections
|
||||
fn build_nodes_and_edges(
|
||||
curves: &[CubicBez],
|
||||
_curves: &[CubicBez],
|
||||
intersections: HashMap<usize, Vec<(f64, Point)>>,
|
||||
) -> (Vec<GraphNode>, Vec<GraphEdge>) {
|
||||
let mut nodes = Vec::new();
|
||||
|
|
@ -459,11 +459,6 @@ impl PlanarGraph {
|
|||
|
||||
// Get the end node of this half-edge
|
||||
let edge = &self.edges[current_edge];
|
||||
let start_node_this_edge = if current_forward {
|
||||
edge.start_node
|
||||
} else {
|
||||
edge.end_node
|
||||
};
|
||||
let end_node = if current_forward {
|
||||
edge.end_node
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ struct ExtractedSegment {
|
|||
/// Original curve index
|
||||
curve_index: usize,
|
||||
/// Minimum parameter value from boundary points
|
||||
t_min: f64,
|
||||
_t_min: f64,
|
||||
/// Maximum parameter value from boundary points
|
||||
t_max: f64,
|
||||
_t_max: f64,
|
||||
/// The curve segment (trimmed to [t_min, t_max])
|
||||
segment: CurveSegment,
|
||||
}
|
||||
|
|
@ -148,8 +148,8 @@ fn split_segments_at_intersections(segments: Vec<ExtractedSegment>) -> Vec<Extra
|
|||
|
||||
result.push(ExtractedSegment {
|
||||
curve_index: seg.curve_index,
|
||||
t_min: t_start,
|
||||
t_max: t_end,
|
||||
_t_min: t_start,
|
||||
_t_max: t_end,
|
||||
segment: subseg,
|
||||
});
|
||||
}
|
||||
|
|
@ -260,8 +260,8 @@ fn extract_segments(
|
|||
|
||||
segments.push(ExtractedSegment {
|
||||
curve_index: curve_idx,
|
||||
t_min,
|
||||
t_max,
|
||||
_t_min: t_min,
|
||||
_t_max: t_max,
|
||||
segment,
|
||||
});
|
||||
}
|
||||
|
|
@ -540,7 +540,7 @@ enum ConnectedSegment {
|
|||
Curve {
|
||||
segment: CurveSegment,
|
||||
start: Point,
|
||||
end: Point,
|
||||
_end: Point,
|
||||
},
|
||||
/// A line segment bridging a gap
|
||||
Line { start: Point, end: Point },
|
||||
|
|
@ -550,7 +550,7 @@ enum ConnectedSegment {
|
|||
fn connect_segments(
|
||||
extracted: &[ExtractedSegment],
|
||||
config: &SegmentBuilderConfig,
|
||||
click_point: Point,
|
||||
_click_point: Point,
|
||||
) -> Option<Vec<ConnectedSegment>> {
|
||||
if extracted.is_empty() {
|
||||
println!("connect_segments: No segments to connect");
|
||||
|
|
@ -575,7 +575,7 @@ fn connect_segments(
|
|||
connected.push(ConnectedSegment::Curve {
|
||||
segment: current.segment.clone(),
|
||||
start: current.segment.eval_at(0.0),
|
||||
end: current_end,
|
||||
_end: current_end,
|
||||
});
|
||||
|
||||
// Check if we need to connect to the next segment
|
||||
|
|
@ -794,7 +794,7 @@ mod tests {
|
|||
// If it found segments, verify they're valid
|
||||
assert!(!segments.is_empty());
|
||||
for seg in &segments {
|
||||
assert!(seg.t_min <= seg.t_max);
|
||||
assert!(seg._t_min <= seg._t_max);
|
||||
}
|
||||
}
|
||||
// If None, the algorithm couldn't form a cycle - that's okay for this test
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ pub struct VideoMetadata {
|
|||
/// Video decoder with LRU frame caching
|
||||
pub struct VideoDecoder {
|
||||
path: String,
|
||||
width: u32, // Original video width
|
||||
height: u32, // Original video height
|
||||
_width: u32, // Original video width
|
||||
_height: u32, // Original video height
|
||||
output_width: u32, // Scaled output width
|
||||
output_height: u32, // Scaled output height
|
||||
fps: f64,
|
||||
duration: f64,
|
||||
_duration: f64,
|
||||
time_base: f64,
|
||||
stream_index: usize,
|
||||
frame_cache: LruCache<i64, Vec<u8>>, // timestamp -> RGBA data
|
||||
|
|
@ -107,12 +107,12 @@ impl VideoDecoder {
|
|||
|
||||
Ok(Self {
|
||||
path,
|
||||
width,
|
||||
height,
|
||||
_width: width,
|
||||
_height: height,
|
||||
output_width,
|
||||
output_height,
|
||||
fps,
|
||||
duration,
|
||||
_duration: duration,
|
||||
time_base,
|
||||
stream_index,
|
||||
frame_cache: LruCache::new(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! using the actual WGSL shaders.
|
||||
|
||||
use lightningbeam_core::effect::{EffectDefinition, EffectInstance};
|
||||
use lightningbeam_core::gpu::effect_processor::{EffectProcessor, EffectUniforms};
|
||||
use lightningbeam_core::gpu::effect_processor::EffectProcessor;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ pub struct EffectThumbnailGenerator {
|
|||
/// Effect processor for compiling and applying shaders
|
||||
effect_processor: EffectProcessor,
|
||||
/// Source texture (still-life image scaled to thumbnail size)
|
||||
#[allow(dead_code)] // Must stay alive — source_view is a view into this texture
|
||||
source_texture: wgpu::Texture,
|
||||
/// View of the source texture
|
||||
source_view: wgpu::TextureView,
|
||||
|
|
@ -101,7 +102,7 @@ impl EffectThumbnailGenerator {
|
|||
let dest_view = dest_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
// Create readback buffer
|
||||
let buffer_size = (EFFECT_THUMBNAIL_SIZE * EFFECT_THUMBNAIL_SIZE * 4) as u64;
|
||||
let _buffer_size = (EFFECT_THUMBNAIL_SIZE * EFFECT_THUMBNAIL_SIZE * 4) as u64;
|
||||
// Align to 256 bytes for wgpu requirements
|
||||
let aligned_bytes_per_row = ((EFFECT_THUMBNAIL_SIZE * 4 + 255) / 256) * 256;
|
||||
let readback_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
|
|
@ -160,11 +161,13 @@ impl EffectThumbnailGenerator {
|
|||
}
|
||||
|
||||
/// Get a cached thumbnail, or None if not yet generated
|
||||
#[allow(dead_code)]
|
||||
pub fn get_thumbnail(&self, effect_id: &Uuid) -> Option<&Vec<u8>> {
|
||||
self.thumbnail_cache.get(effect_id)
|
||||
}
|
||||
|
||||
/// Check if a thumbnail is cached
|
||||
#[allow(dead_code)]
|
||||
pub fn has_thumbnail(&self, effect_id: &Uuid) -> bool {
|
||||
self.thumbnail_cache.contains_key(effect_id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(dead_code)]
|
||||
//! Audio export functionality
|
||||
//!
|
||||
//! Exports audio from the timeline to various formats:
|
||||
|
|
@ -168,7 +169,7 @@ fn export_audio_ffmpeg_mp3<P: AsRef<Path>>(
|
|||
// Step 3: Encode frames and write to output
|
||||
// Convert interleaved f32 samples to planar i16 format
|
||||
let num_frames = pcm_samples.len() / settings.channels as usize;
|
||||
let mut planar_samples = convert_to_planar_i16(&pcm_samples, settings.channels);
|
||||
let planar_samples = convert_to_planar_i16(&pcm_samples, settings.channels);
|
||||
|
||||
// Get encoder frame size
|
||||
let frame_size = encoder.frame_size();
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ impl ExportDialog {
|
|||
("Podcast AAC", AudioExportSettings::podcast_aac()),
|
||||
];
|
||||
|
||||
egui::ComboBox::from_id_source("export_preset")
|
||||
egui::ComboBox::from_id_salt("export_preset")
|
||||
.selected_text(presets[self.selected_audio_preset].0)
|
||||
.show_ui(ui, |ui| {
|
||||
for (i, (name, _)) in presets.iter().enumerate() {
|
||||
|
|
@ -207,7 +207,7 @@ impl ExportDialog {
|
|||
ui.heading("Format");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Format:");
|
||||
egui::ComboBox::from_id_source("audio_format")
|
||||
egui::ComboBox::from_id_salt("audio_format")
|
||||
.selected_text(self.audio_settings.format.name())
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.audio_settings.format, AudioFormat::Wav, "WAV (Uncompressed)");
|
||||
|
|
@ -222,7 +222,7 @@ impl ExportDialog {
|
|||
// Audio settings
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Sample Rate:");
|
||||
egui::ComboBox::from_id_source("sample_rate")
|
||||
egui::ComboBox::from_id_salt("sample_rate")
|
||||
.selected_text(format!("{} Hz", self.audio_settings.sample_rate))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.audio_settings.sample_rate, 44100, "44100 Hz");
|
||||
|
|
@ -251,7 +251,7 @@ impl ExportDialog {
|
|||
if self.audio_settings.format.uses_bitrate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Bitrate:");
|
||||
egui::ComboBox::from_id_source("bitrate")
|
||||
egui::ComboBox::from_id_salt("bitrate")
|
||||
.selected_text(format!("{} kbps", self.audio_settings.bitrate_kbps))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.audio_settings.bitrate_kbps, 128, "128 kbps");
|
||||
|
|
@ -269,7 +269,7 @@ impl ExportDialog {
|
|||
ui.heading("Codec");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Codec:");
|
||||
egui::ComboBox::from_id_source("video_codec")
|
||||
egui::ComboBox::from_id_salt("video_codec")
|
||||
.selected_text(format!("{:?}", self.video_settings.codec))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.video_settings.codec, VideoCodec::H264, "H.264 (Most Compatible)");
|
||||
|
|
@ -287,13 +287,13 @@ impl ExportDialog {
|
|||
ui.horizontal(|ui| {
|
||||
ui.label("Width:");
|
||||
let mut custom_width = self.video_settings.width.unwrap_or(1920);
|
||||
if ui.add(egui::DragValue::new(&mut custom_width).clamp_range(1..=7680)).changed() {
|
||||
if ui.add(egui::DragValue::new(&mut custom_width).range(1..=7680)).changed() {
|
||||
self.video_settings.width = Some(custom_width);
|
||||
}
|
||||
|
||||
ui.label("Height:");
|
||||
let mut custom_height = self.video_settings.height.unwrap_or(1080);
|
||||
if ui.add(egui::DragValue::new(&mut custom_height).clamp_range(1..=4320)).changed() {
|
||||
if ui.add(egui::DragValue::new(&mut custom_height).range(1..=4320)).changed() {
|
||||
self.video_settings.height = Some(custom_height);
|
||||
}
|
||||
});
|
||||
|
|
@ -320,7 +320,7 @@ impl ExportDialog {
|
|||
ui.heading("Framerate");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("FPS:");
|
||||
egui::ComboBox::from_id_source("framerate")
|
||||
egui::ComboBox::from_id_salt("framerate")
|
||||
.selected_text(format!("{}", self.video_settings.framerate as u32))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.video_settings.framerate, 24.0, "24");
|
||||
|
|
@ -335,7 +335,7 @@ impl ExportDialog {
|
|||
ui.heading("Quality");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Quality:");
|
||||
egui::ComboBox::from_id_source("video_quality")
|
||||
egui::ComboBox::from_id_salt("video_quality")
|
||||
.selected_text(self.video_settings.quality.name())
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.video_settings.quality, VideoQuality::Low, VideoQuality::Low.name());
|
||||
|
|
@ -363,13 +363,13 @@ impl ExportDialog {
|
|||
ui.label("Start:");
|
||||
ui.add(egui::DragValue::new(start_time)
|
||||
.speed(0.1)
|
||||
.clamp_range(0.0..=*end_time)
|
||||
.range(0.0..=*end_time)
|
||||
.suffix(" s"));
|
||||
|
||||
ui.label("End:");
|
||||
ui.add(egui::DragValue::new(end_time)
|
||||
.speed(0.1)
|
||||
.clamp_range(*start_time..=f64::MAX)
|
||||
.range(*start_time..=f64::MAX)
|
||||
.suffix(" s"));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ pub struct VideoExportState {
|
|||
/// Start time in seconds
|
||||
start_time: f64,
|
||||
/// End time in seconds
|
||||
#[allow(dead_code)]
|
||||
end_time: f64,
|
||||
/// Frames per second
|
||||
framerate: f64,
|
||||
|
|
@ -163,7 +164,7 @@ impl ExportOrchestrator {
|
|||
/// For parallel video+audio exports, returns combined progress.
|
||||
pub fn poll_progress(&mut self) -> Option<ExportProgress> {
|
||||
// Handle parallel video+audio export
|
||||
if let Some(ref mut parallel) = self.parallel_export {
|
||||
if let Some(ref mut _parallel) = self.parallel_export {
|
||||
return self.poll_parallel_progress();
|
||||
}
|
||||
|
||||
|
|
@ -461,6 +462,7 @@ impl ExportOrchestrator {
|
|||
/// Wait for the export to complete
|
||||
///
|
||||
/// This blocks until the export thread finishes.
|
||||
#[allow(dead_code)]
|
||||
pub fn wait_for_completion(&mut self) {
|
||||
if let Some(handle) = self.thread_handle.take() {
|
||||
handle.join().ok();
|
||||
|
|
@ -915,7 +917,7 @@ impl ExportOrchestrator {
|
|||
}
|
||||
|
||||
// Render to GPU (timed)
|
||||
let render_start = Instant::now();
|
||||
let _render_start = Instant::now();
|
||||
let encoder = video_exporter::render_frame_to_gpu_rgba(
|
||||
document, timestamp, width, height,
|
||||
device, queue, renderer, image_cache, video_manager,
|
||||
|
|
@ -1049,7 +1051,7 @@ impl ExportOrchestrator {
|
|||
// Determine dimensions from first frame
|
||||
let (width, height) = if let Some((_, _, ref y_plane, _, _)) = first_frame {
|
||||
// Calculate dimensions from Y plane size (full resolution, 1 byte per pixel)
|
||||
let pixel_count = y_plane.len();
|
||||
let _pixel_count = y_plane.len();
|
||||
// Use settings dimensions if provided, otherwise infer from buffer
|
||||
let w = settings.width.unwrap_or(1920); // Default to 1920 if not specified
|
||||
let h = settings.height.unwrap_or(1080); // Default to 1080 if not specified
|
||||
|
|
@ -1088,7 +1090,7 @@ impl ExportOrchestrator {
|
|||
println!("🧵 [ENCODER] Encoder initialized, ready to encode frames");
|
||||
|
||||
// Process first frame
|
||||
if let Some((frame_num, timestamp, y_plane, u_plane, v_plane)) = first_frame {
|
||||
if let Some((_frame_num, timestamp, y_plane, u_plane, v_plane)) = first_frame {
|
||||
Self::encode_frame(
|
||||
&mut encoder,
|
||||
&mut output,
|
||||
|
|
@ -1115,7 +1117,7 @@ impl ExportOrchestrator {
|
|||
}
|
||||
|
||||
match frame_rx.recv() {
|
||||
Ok(VideoFrameMessage::Frame { frame_num, timestamp, y_plane, u_plane, v_plane }) => {
|
||||
Ok(VideoFrameMessage::Frame { frame_num: _, timestamp, y_plane, u_plane, v_plane }) => {
|
||||
Self::encode_frame(
|
||||
&mut encoder,
|
||||
&mut output,
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ impl ReadbackPipeline {
|
|||
/// Call this frequently to process completed transfers.
|
||||
pub fn poll_nonblocking(&mut self) -> Vec<ReadbackResult> {
|
||||
// Poll GPU without blocking
|
||||
self.device.poll(wgpu::PollType::Poll);
|
||||
let _ = self.device.poll(wgpu::PollType::Poll);
|
||||
|
||||
// Collect all completed readbacks
|
||||
let mut results = Vec::new();
|
||||
|
|
@ -269,13 +269,14 @@ impl ReadbackPipeline {
|
|||
/// Flush pipeline and wait for all pending operations
|
||||
///
|
||||
/// Call this at the end of export to ensure all frames are processed
|
||||
#[allow(dead_code)]
|
||||
pub fn flush(&mut self) -> Vec<ReadbackResult> {
|
||||
let mut all_results = Vec::new();
|
||||
|
||||
// Keep polling until all buffers are Free
|
||||
loop {
|
||||
// Poll for new completions
|
||||
self.device.poll(wgpu::PollType::Poll);
|
||||
let _ = self.device.poll(wgpu::PollType::Poll);
|
||||
|
||||
while let Ok(result) = self.readback_rx.try_recv() {
|
||||
self.buffers[result.buffer_id].state = BufferState::Mapped;
|
||||
|
|
@ -310,8 +311,4 @@ impl ReadbackPipeline {
|
|||
all_results
|
||||
}
|
||||
|
||||
/// Get buffer count currently in flight (for monitoring)
|
||||
pub fn buffers_in_flight(&self) -> usize {
|
||||
self.buffers.iter().filter(|b| b.state != BufferState::Free).count()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(dead_code)]
|
||||
//! Video export functionality
|
||||
//!
|
||||
//! Exports video from the timeline using FFmpeg encoding:
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@ enum FileCommand {
|
|||
}
|
||||
|
||||
/// Progress updates from file operations worker
|
||||
#[allow(dead_code)] // EncodingAudio/DecodingAudio planned for granular progress reporting
|
||||
enum FileProgress {
|
||||
SerializingAudioPool,
|
||||
EncodingAudio { current: usize, total: usize },
|
||||
|
|
@ -428,6 +429,7 @@ enum FileOperation {
|
|||
|
||||
/// Information about an imported asset (for auto-placement)
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)] // name/duration populated for future import UX features
|
||||
struct ImportedAssetInfo {
|
||||
clip_id: uuid::Uuid,
|
||||
clip_type: panes::DragClipType,
|
||||
|
|
@ -619,6 +621,7 @@ enum RecordingArmMode {
|
|||
#[default]
|
||||
Auto,
|
||||
/// User explicitly arms tracks (multi-track recording workflow)
|
||||
#[allow(dead_code)]
|
||||
Manual,
|
||||
}
|
||||
|
||||
|
|
@ -650,12 +653,15 @@ struct EditorApp {
|
|||
rdp_tolerance: f64, // RDP simplification tolerance (default: 10.0)
|
||||
schneider_max_error: f64, // Schneider curve fitting max error (default: 30.0)
|
||||
// Audio engine integration
|
||||
audio_stream: Option<cpal::Stream>, // Audio stream (must be kept alive)
|
||||
audio_controller: Option<std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>, // Shared audio controller
|
||||
audio_event_rx: Option<rtrb::Consumer<daw_backend::AudioEvent>>, // Audio event receiver
|
||||
audio_events_pending: std::sync::Arc<std::sync::atomic::AtomicBool>, // Flag set when audio events arrive
|
||||
audio_sample_rate: u32, // Audio sample rate
|
||||
audio_channels: u32, // Audio channel count
|
||||
#[allow(dead_code)] // Must be kept alive to maintain audio output
|
||||
audio_stream: Option<cpal::Stream>,
|
||||
audio_controller: Option<std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
|
||||
audio_event_rx: Option<rtrb::Consumer<daw_backend::AudioEvent>>,
|
||||
audio_events_pending: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
#[allow(dead_code)] // Stored for future export/recording configuration
|
||||
audio_sample_rate: u32,
|
||||
#[allow(dead_code)]
|
||||
audio_channels: u32,
|
||||
// Video decoding and management
|
||||
video_manager: std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>, // Shared video manager
|
||||
// Track ID mapping (Document layer UUIDs <-> daw-backend TrackIds)
|
||||
|
|
@ -667,8 +673,10 @@ struct EditorApp {
|
|||
playback_time: f64, // Current playback position in seconds (persistent - save with document)
|
||||
is_playing: bool, // Whether playback is currently active (transient - don't save)
|
||||
// Recording state
|
||||
recording_arm_mode: RecordingArmMode, // How tracks are armed for recording
|
||||
armed_layers: HashSet<Uuid>, // Explicitly armed layers (used in Manual mode)
|
||||
#[allow(dead_code)] // Infrastructure for Manual recording mode
|
||||
recording_arm_mode: RecordingArmMode,
|
||||
#[allow(dead_code)]
|
||||
armed_layers: HashSet<Uuid>,
|
||||
is_recording: bool, // Whether recording is currently active
|
||||
recording_clips: HashMap<Uuid, u32>, // layer_id -> backend clip_id during recording
|
||||
recording_start_time: f64, // Playback time when recording started
|
||||
|
|
@ -942,7 +950,7 @@ impl EditorApp {
|
|||
egui::vec2(content_width, content_height),
|
||||
);
|
||||
|
||||
ui.allocate_ui_at_rect(content_rect, |ui| {
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(content_rect), |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
// Title
|
||||
ui.heading(egui::RichText::new("Welcome to Lightningbeam!")
|
||||
|
|
@ -1472,10 +1480,6 @@ impl EditorApp {
|
|||
self.pane_instances.clear();
|
||||
}
|
||||
|
||||
fn current_layout_def(&self) -> &LayoutDefinition {
|
||||
&self.layouts[self.current_layout_index]
|
||||
}
|
||||
|
||||
fn apply_layout_action(&mut self, action: LayoutAction) {
|
||||
match action {
|
||||
LayoutAction::SplitHorizontal(path, percent) => {
|
||||
|
|
@ -2511,7 +2515,7 @@ impl EditorApp {
|
|||
};
|
||||
|
||||
// Create video clip with real metadata
|
||||
let mut clip = VideoClip::new(
|
||||
let clip = VideoClip::new(
|
||||
&name,
|
||||
path_str.clone(),
|
||||
metadata.width as f64,
|
||||
|
|
@ -3776,12 +3780,11 @@ impl eframe::App for EditorApp {
|
|||
// Poll export orchestrator for progress
|
||||
if let Some(orchestrator) = &mut self.export_orchestrator {
|
||||
// Only log occasionally to avoid spam
|
||||
static mut POLL_COUNT: u32 = 0;
|
||||
unsafe {
|
||||
POLL_COUNT += 1;
|
||||
if POLL_COUNT % 60 == 0 {
|
||||
println!("🔍 [MAIN] Polling orchestrator (poll #{})...", POLL_COUNT);
|
||||
}
|
||||
use std::sync::atomic::{AtomicU32, Ordering as AtomicOrdering};
|
||||
static POLL_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||
let count = POLL_COUNT.fetch_add(1, AtomicOrdering::Relaxed) + 1;
|
||||
if count % 60 == 0 {
|
||||
println!("🔍 [MAIN] Polling orchestrator (poll #{})...", count);
|
||||
}
|
||||
if let Some(progress) = orchestrator.poll_progress() {
|
||||
match progress {
|
||||
|
|
@ -4275,12 +4278,12 @@ fn render_layout_node(
|
|||
|
||||
if ui.button("Split Horizontal ->").clicked() {
|
||||
*layout_action = Some(LayoutAction::EnterSplitPreviewHorizontal);
|
||||
ui.close_menu();
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if ui.button("Split Vertical |").clicked() {
|
||||
*layout_action = Some(LayoutAction::EnterSplitPreviewVertical);
|
||||
ui.close_menu();
|
||||
ui.close();
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
|
@ -4289,14 +4292,14 @@ fn render_layout_node(
|
|||
let mut path_keep_right = path.clone();
|
||||
path_keep_right.push(1); // Remove left, keep right child
|
||||
*layout_action = Some(LayoutAction::RemoveSplit(path_keep_right));
|
||||
ui.close_menu();
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if ui.button("Join Right >").clicked() {
|
||||
let mut path_keep_left = path.clone();
|
||||
path_keep_left.push(0); // Remove right, keep left child
|
||||
*layout_action = Some(LayoutAction::RemoveSplit(path_keep_left));
|
||||
ui.close_menu();
|
||||
ui.close();
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -4397,12 +4400,12 @@ fn render_layout_node(
|
|||
|
||||
if ui.button("Split Horizontal ->").clicked() {
|
||||
*layout_action = Some(LayoutAction::EnterSplitPreviewHorizontal);
|
||||
ui.close_menu();
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if ui.button("Split Vertical |").clicked() {
|
||||
*layout_action = Some(LayoutAction::EnterSplitPreviewVertical);
|
||||
ui.close_menu();
|
||||
ui.close();
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
|
@ -4411,14 +4414,14 @@ fn render_layout_node(
|
|||
let mut path_keep_bottom = path.clone();
|
||||
path_keep_bottom.push(1); // Remove top, keep bottom child
|
||||
*layout_action = Some(LayoutAction::RemoveSplit(path_keep_bottom));
|
||||
ui.close_menu();
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if ui.button("Join Down v").clicked() {
|
||||
let mut path_keep_top = path.clone();
|
||||
path_keep_top.push(0); // Remove bottom, keep top child
|
||||
*layout_action = Some(LayoutAction::RemoveSplit(path_keep_top));
|
||||
ui.close_menu();
|
||||
ui.close();
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -4828,100 +4831,6 @@ fn render_pane(
|
|||
}
|
||||
}
|
||||
|
||||
/// Render toolbar with tool buttons
|
||||
fn render_toolbar(
|
||||
ui: &mut egui::Ui,
|
||||
rect: egui::Rect,
|
||||
tool_icon_cache: &mut ToolIconCache,
|
||||
selected_tool: &mut Tool,
|
||||
path: &NodePath,
|
||||
) {
|
||||
let button_size = 60.0; // 50% bigger (was 40.0)
|
||||
let button_padding = 8.0;
|
||||
let button_spacing = 4.0;
|
||||
|
||||
// Calculate how many columns we can fit
|
||||
let available_width = rect.width() - (button_padding * 2.0);
|
||||
let columns = ((available_width + button_spacing) / (button_size + button_spacing)).floor() as usize;
|
||||
let columns = columns.max(1); // At least 1 column
|
||||
|
||||
let mut x = rect.left() + button_padding;
|
||||
let mut y = rect.top() + button_padding;
|
||||
let mut col = 0;
|
||||
|
||||
for tool in Tool::all() {
|
||||
let button_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(x, y),
|
||||
egui::vec2(button_size, button_size),
|
||||
);
|
||||
|
||||
// Check if this is the selected tool
|
||||
let is_selected = *selected_tool == *tool;
|
||||
|
||||
// Button background
|
||||
let bg_color = if is_selected {
|
||||
egui::Color32::from_rgb(70, 100, 150) // Highlighted blue
|
||||
} else {
|
||||
egui::Color32::from_rgb(50, 50, 50)
|
||||
};
|
||||
ui.painter().rect_filled(button_rect, 4.0, bg_color);
|
||||
|
||||
// Load and render tool icon
|
||||
if let Some(icon) = tool_icon_cache.get_or_load(*tool, ui.ctx()) {
|
||||
let icon_rect = button_rect.shrink(8.0); // Padding inside button
|
||||
ui.painter().image(
|
||||
icon.id(),
|
||||
icon_rect,
|
||||
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||
egui::Color32::WHITE,
|
||||
);
|
||||
}
|
||||
|
||||
// Make button interactive (include path to ensure unique IDs across panes)
|
||||
let button_id = ui.id().with(("tool_button", path, *tool as usize));
|
||||
let response = ui.interact(button_rect, button_id, egui::Sense::click());
|
||||
|
||||
// Check for click first
|
||||
if response.clicked() {
|
||||
*selected_tool = *tool;
|
||||
}
|
||||
|
||||
if response.hovered() {
|
||||
ui.painter().rect_stroke(
|
||||
button_rect,
|
||||
4.0,
|
||||
egui::Stroke::new(2.0, egui::Color32::from_gray(180)),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
}
|
||||
|
||||
// Show tooltip with tool name and shortcut (consumes response)
|
||||
response.on_hover_text(format!("{} ({})", tool.display_name(), tool.shortcut_hint()));
|
||||
|
||||
// Draw selection border
|
||||
if is_selected {
|
||||
ui.painter().rect_stroke(
|
||||
button_rect,
|
||||
4.0,
|
||||
egui::Stroke::new(2.0, egui::Color32::from_rgb(100, 150, 255)),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
}
|
||||
|
||||
// Move to next position in grid
|
||||
col += 1;
|
||||
if col >= columns {
|
||||
// Move to next row
|
||||
col = 0;
|
||||
x = rect.left() + button_padding;
|
||||
y += button_size + button_spacing;
|
||||
} else {
|
||||
// Move to next column
|
||||
x += button_size + button_spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a color for each pane type for visualization
|
||||
fn pane_color(pane_type: PaneType) -> egui::Color32 {
|
||||
match pane_type {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ pub enum ShortcutKey {
|
|||
// Numbers
|
||||
Num0,
|
||||
// Symbols
|
||||
Comma, Minus, Equals, Plus,
|
||||
Comma, Minus, Equals,
|
||||
#[allow(dead_code)] // Completes keyboard mapping set
|
||||
Plus,
|
||||
BracketLeft, BracketRight,
|
||||
// Special
|
||||
Delete,
|
||||
|
|
@ -189,6 +191,7 @@ pub enum MenuAction {
|
|||
RecenterView,
|
||||
NextLayout,
|
||||
PreviousLayout,
|
||||
#[allow(dead_code)] // Handler exists in main.rs, menu item not yet wired
|
||||
SwitchLayout(usize),
|
||||
|
||||
// Help menu
|
||||
|
|
@ -219,6 +222,7 @@ pub enum MenuDef {
|
|||
// Shortcut constants for clarity
|
||||
const CTRL: bool = true;
|
||||
const SHIFT: bool = true;
|
||||
#[allow(dead_code)]
|
||||
const ALT: bool = true;
|
||||
const NO_CTRL: bool = false;
|
||||
const NO_SHIFT: bool = false;
|
||||
|
|
@ -288,7 +292,9 @@ impl MenuItemDef {
|
|||
// macOS app menu items
|
||||
const SETTINGS: Self = Self { label: "Settings", action: MenuAction::Settings, shortcut: Some(Shortcut::new(ShortcutKey::Comma, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
const CLOSE_WINDOW: Self = Self { label: "Close Window", action: MenuAction::CloseWindow, shortcut: Some(Shortcut::new(ShortcutKey::W, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
#[allow(dead_code)] // Used in #[cfg(target_os = "macos")] block
|
||||
const QUIT_MACOS: Self = Self { label: "Quit Lightningbeam", action: MenuAction::Quit, shortcut: Some(Shortcut::new(ShortcutKey::Q, CTRL, NO_SHIFT, NO_ALT)) };
|
||||
#[allow(dead_code)]
|
||||
const ABOUT_MACOS: Self = Self { label: "About Lightningbeam", action: MenuAction::About, shortcut: None };
|
||||
|
||||
/// Get all menu items with shortcuts (for keyboard handling)
|
||||
|
|
@ -593,7 +599,7 @@ impl MenuSystem {
|
|||
pub fn render_egui_menu_bar(&self, ui: &mut egui::Ui, recent_files: &[std::path::PathBuf]) -> Option<MenuAction> {
|
||||
let mut action = None;
|
||||
|
||||
egui::menu::bar(ui, |ui| {
|
||||
egui::MenuBar::new().ui(ui, |ui| {
|
||||
for menu_def in MenuItemDef::menu_structure() {
|
||||
if let Some(a) = self.render_menu_def(ui, menu_def, recent_files) {
|
||||
action = Some(a);
|
||||
|
|
@ -632,7 +638,7 @@ impl MenuSystem {
|
|||
|
||||
if ui.button(display_name).clicked() {
|
||||
action = Some(MenuAction::OpenRecent(index));
|
||||
ui.close_menu();
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -643,14 +649,14 @@ impl MenuSystem {
|
|||
|
||||
if ui.button("Clear Recent Files").clicked() {
|
||||
action = Some(MenuAction::ClearRecentFiles);
|
||||
ui.close_menu();
|
||||
ui.close();
|
||||
}
|
||||
} else {
|
||||
// Normal submenu rendering
|
||||
for child in *children {
|
||||
if let Some(a) = self.render_menu_def(ui, child, recent_files) {
|
||||
action = Some(a);
|
||||
ui.close_menu();
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ const SEARCH_BAR_HEIGHT: f32 = 30.0;
|
|||
const CATEGORY_TAB_HEIGHT: f32 = 28.0;
|
||||
const BREADCRUMB_HEIGHT: f32 = 24.0;
|
||||
const ITEM_HEIGHT: f32 = 40.0;
|
||||
#[allow(dead_code)]
|
||||
const ITEM_PADDING: f32 = 4.0;
|
||||
const LIST_THUMBNAIL_SIZE: f32 = 32.0;
|
||||
const GRID_ITEM_SIZE: f32 = 80.0;
|
||||
|
|
@ -137,6 +138,7 @@ impl ThumbnailCache {
|
|||
}
|
||||
|
||||
/// Check if a thumbnail is already cached (and not dirty)
|
||||
#[allow(dead_code)]
|
||||
pub fn has(&self, asset_id: &Uuid) -> bool {
|
||||
self.textures.contains_key(asset_id) && !self.dirty.contains(asset_id)
|
||||
}
|
||||
|
|
@ -146,11 +148,6 @@ impl ThumbnailCache {
|
|||
self.dirty.insert(*asset_id);
|
||||
}
|
||||
|
||||
/// Clear all cached thumbnails
|
||||
pub fn clear(&mut self) {
|
||||
self.textures.clear();
|
||||
self.dirty.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -285,7 +282,7 @@ fn generate_waveform_thumbnail(
|
|||
|
||||
// Draw waveform
|
||||
let center_y = size / 2;
|
||||
let num_peaks = waveform_peaks.len().min(size);
|
||||
let _num_peaks = waveform_peaks.len().min(size);
|
||||
|
||||
for (x, &(min_val, max_val)) in waveform_peaks.iter().take(size).enumerate() {
|
||||
// Scale peaks to pixel range (center ± half height)
|
||||
|
|
@ -552,6 +549,7 @@ fn shape_color_to_tiny_skia(color: &ShapeColor) -> tiny_skia::Color {
|
|||
}
|
||||
|
||||
/// Generate a simple effect thumbnail with a pink gradient
|
||||
#[allow(dead_code)]
|
||||
fn generate_effect_thumbnail() -> Vec<u8> {
|
||||
let size = THUMBNAIL_SIZE as usize;
|
||||
let mut rgba = vec![0u8; size * size * 4];
|
||||
|
|
@ -628,6 +626,7 @@ fn generate_effect_thumbnail() -> Vec<u8> {
|
|||
}
|
||||
|
||||
/// Ellipsize a string to fit within a maximum character count
|
||||
#[allow(dead_code)]
|
||||
fn ellipsize(s: &str, max_chars: usize) -> String {
|
||||
if s.chars().count() <= max_chars {
|
||||
s.to_string()
|
||||
|
|
@ -706,6 +705,7 @@ pub struct AssetEntry {
|
|||
pub struct FolderEntry {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
#[allow(dead_code)]
|
||||
pub category: AssetCategory,
|
||||
pub item_count: usize,
|
||||
}
|
||||
|
|
@ -718,6 +718,7 @@ pub enum LibraryItem {
|
|||
}
|
||||
|
||||
impl LibraryItem {
|
||||
#[allow(dead_code)]
|
||||
pub fn id(&self) -> Uuid {
|
||||
match self {
|
||||
LibraryItem::Folder(f) => f.id,
|
||||
|
|
@ -810,6 +811,7 @@ pub struct AssetLibraryPane {
|
|||
current_folders: HashMap<u8, Option<Uuid>>,
|
||||
|
||||
/// Set of expanded folder IDs (for tree view - future enhancement)
|
||||
#[allow(dead_code)]
|
||||
expanded_folders: HashSet<Uuid>,
|
||||
|
||||
/// Cached folder icon texture
|
||||
|
|
@ -1283,6 +1285,7 @@ impl AssetLibraryPane {
|
|||
}
|
||||
|
||||
/// Filter assets based on current category and search text
|
||||
#[allow(dead_code)]
|
||||
fn filter_assets<'a>(&self, assets: &'a [AssetEntry]) -> Vec<&'a AssetEntry> {
|
||||
let search_lower = self.search_filter.to_lowercase();
|
||||
|
||||
|
|
@ -1727,6 +1730,7 @@ impl AssetLibraryPane {
|
|||
}
|
||||
|
||||
/// Render a section header for effect categories
|
||||
#[allow(dead_code)] // Part of List/Grid view rendering subsystem, not yet wired
|
||||
fn render_section_header(ui: &mut egui::Ui, label: &str, color: egui::Color32) {
|
||||
ui.add_space(4.0);
|
||||
let (header_rect, _) = ui.allocate_exact_size(
|
||||
|
|
@ -1744,7 +1748,7 @@ impl AssetLibraryPane {
|
|||
}
|
||||
|
||||
/// Render a grid of asset items
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments, dead_code)]
|
||||
fn render_grid_items(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
|
|
@ -1755,7 +1759,7 @@ impl AssetLibraryPane {
|
|||
shared: &mut SharedPaneState,
|
||||
document: &Document,
|
||||
text_color: egui::Color32,
|
||||
secondary_text_color: egui::Color32,
|
||||
_secondary_text_color: egui::Color32,
|
||||
) {
|
||||
if assets.is_empty() {
|
||||
return;
|
||||
|
|
@ -2003,7 +2007,7 @@ impl AssetLibraryPane {
|
|||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
rect: egui::Rect,
|
||||
path: &NodePath,
|
||||
_path: &NodePath,
|
||||
shared: &mut SharedPaneState,
|
||||
items: &[&LibraryItem],
|
||||
document: &Document,
|
||||
|
|
@ -2012,7 +2016,7 @@ impl AssetLibraryPane {
|
|||
let folder_icon = self.get_folder_icon(ui.ctx()).cloned();
|
||||
|
||||
let _scroll_area = egui::ScrollArea::vertical()
|
||||
.id_source("asset_library_scroll")
|
||||
.id_salt("asset_library_scroll")
|
||||
.show_viewport(ui, |ui, viewport| {
|
||||
ui.set_min_width(rect.width());
|
||||
|
||||
|
|
@ -2171,7 +2175,7 @@ impl AssetLibraryPane {
|
|||
// Load folder icon if needed
|
||||
let folder_icon = self.get_folder_icon(ui.ctx()).cloned();
|
||||
|
||||
ui.allocate_new_ui(egui::UiBuilder::new().max_rect(rect), |ui| {
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt(("asset_library_grid_scroll", path))
|
||||
.auto_shrink([false, false])
|
||||
|
|
@ -2661,6 +2665,7 @@ impl AssetLibraryPane {
|
|||
}
|
||||
|
||||
/// Render assets based on current view mode
|
||||
#[allow(dead_code)]
|
||||
fn render_assets(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
|
|
@ -2681,6 +2686,7 @@ impl AssetLibraryPane {
|
|||
}
|
||||
|
||||
/// Render the asset list view
|
||||
#[allow(dead_code)]
|
||||
fn render_asset_list_view(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
|
|
@ -2724,7 +2730,7 @@ impl AssetLibraryPane {
|
|||
|
||||
// Use egui's built-in ScrollArea for scrolling
|
||||
let scroll_area_rect = rect;
|
||||
ui.allocate_new_ui(egui::UiBuilder::new().max_rect(scroll_area_rect), |ui| {
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(scroll_area_rect), |ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt(("asset_list_scroll", path))
|
||||
.auto_shrink([false, false])
|
||||
|
|
@ -2757,7 +2763,7 @@ impl AssetLibraryPane {
|
|||
};
|
||||
let mut rendered_builtin_header = false;
|
||||
let mut rendered_custom_header = false;
|
||||
let mut builtin_rendered = 0;
|
||||
let mut _builtin_rendered = 0;
|
||||
|
||||
for asset in assets_to_render {
|
||||
// Render section headers for Effects tab
|
||||
|
|
@ -2781,7 +2787,7 @@ impl AssetLibraryPane {
|
|||
rendered_custom_header = true;
|
||||
}
|
||||
if asset.is_builtin {
|
||||
builtin_rendered += 1;
|
||||
_builtin_rendered += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3093,6 +3099,7 @@ impl AssetLibraryPane {
|
|||
}
|
||||
|
||||
/// Render the asset grid view
|
||||
#[allow(dead_code)]
|
||||
fn render_asset_grid_view(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
|
|
@ -3165,7 +3172,7 @@ impl AssetLibraryPane {
|
|||
0
|
||||
};
|
||||
|
||||
ui.allocate_new_ui(egui::UiBuilder::new().max_rect(rect), |ui| {
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt(("asset_grid_scroll", path))
|
||||
.auto_shrink([false, false])
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ pub struct DraggingAsset {
|
|||
/// Display name
|
||||
pub name: String,
|
||||
/// Duration in seconds
|
||||
#[allow(dead_code)] // Populated during drag, consumed when drag-and-drop features expand
|
||||
pub duration: f64,
|
||||
/// Dimensions (width, height) for vector/video clips, None for audio
|
||||
pub dimensions: Option<(f64, f64)>,
|
||||
|
|
@ -132,6 +133,7 @@ pub fn find_sampled_audio_track(document: &lightningbeam_core::document::Documen
|
|||
/// Shared state that all panes can access
|
||||
pub struct SharedPaneState<'a> {
|
||||
pub tool_icon_cache: &'a mut crate::ToolIconCache,
|
||||
#[allow(dead_code)] // Used by pane chrome rendering in main.rs
|
||||
pub icon_cache: &'a mut crate::IconCache,
|
||||
pub selected_tool: &'a mut Tool,
|
||||
pub fill_color: &'a mut egui::Color32,
|
||||
|
|
@ -220,7 +222,7 @@ pub trait PaneRenderer {
|
|||
/// Render the optional header section with controls
|
||||
///
|
||||
/// Returns true if a header was rendered, false if no header
|
||||
fn render_header(&mut self, ui: &mut egui::Ui, shared: &mut SharedPaneState) -> bool {
|
||||
fn render_header(&mut self, _ui: &mut egui::Ui, _shared: &mut SharedPaneState) -> bool {
|
||||
false // Default: no header
|
||||
}
|
||||
|
||||
|
|
@ -234,6 +236,7 @@ pub trait PaneRenderer {
|
|||
);
|
||||
|
||||
/// Get the display name of this pane
|
||||
#[allow(dead_code)] // Implemented by all panes, dispatch infrastructure complete
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use uuid::Uuid;
|
|||
pub enum NodeGraphAction {
|
||||
AddNode(AddNodeAction),
|
||||
RemoveNode(RemoveNodeAction),
|
||||
#[allow(dead_code)]
|
||||
MoveNode(MoveNodeAction),
|
||||
Connect(ConnectAction),
|
||||
Disconnect(DisconnectAction),
|
||||
|
|
@ -240,6 +241,7 @@ impl RemoveNodeAction {
|
|||
// MoveNodeAction
|
||||
// ============================================================================
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct MoveNodeAction {
|
||||
layer_id: Uuid,
|
||||
backend_node_id: BackendNodeId,
|
||||
|
|
@ -248,6 +250,7 @@ pub struct MoveNodeAction {
|
|||
}
|
||||
|
||||
impl MoveNodeAction {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(layer_id: Uuid, backend_node_id: BackendNodeId, new_position: (f32, f32)) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ pub struct AudioGraphBackend {
|
|||
audio_controller: Arc<Mutex<EngineController>>,
|
||||
|
||||
/// Maps backend NodeIndex to stable IDs for round-trip serialization
|
||||
node_index_to_stable: HashMap<NodeIndex, u32>,
|
||||
next_stable_id: u32,
|
||||
_node_index_to_stable: HashMap<NodeIndex, u32>,
|
||||
_next_stable_id: u32,
|
||||
}
|
||||
|
||||
impl AudioGraphBackend {
|
||||
|
|
@ -26,8 +26,8 @@ impl AudioGraphBackend {
|
|||
Self {
|
||||
track_id,
|
||||
audio_controller,
|
||||
node_index_to_stable: HashMap::new(),
|
||||
next_stable_id: 0,
|
||||
_node_index_to_stable: HashMap::new(),
|
||||
_next_stable_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -41,25 +41,23 @@ impl GraphBackend for AudioGraphBackend {
|
|||
|
||||
// Generate placeholder node ID
|
||||
// This will be replaced with actual backend NodeIndex from sync query
|
||||
let stable_id = self.next_stable_id;
|
||||
self.next_stable_id += 1;
|
||||
let stable_id = self._next_stable_id;
|
||||
self._next_stable_id += 1;
|
||||
|
||||
// Placeholder: use stable_id as backend index (will be wrong, but compiles)
|
||||
let node_idx = NodeIndex::new(stable_id as usize);
|
||||
self.node_index_to_stable.insert(node_idx, stable_id);
|
||||
self._node_index_to_stable.insert(node_idx, stable_id);
|
||||
|
||||
Ok(BackendNodeId::Audio(node_idx))
|
||||
}
|
||||
|
||||
fn remove_node(&mut self, backend_id: BackendNodeId) -> Result<(), String> {
|
||||
let BackendNodeId::Audio(node_idx) = backend_id else {
|
||||
return Err("Invalid backend node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(node_idx) = backend_id;
|
||||
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_remove_node(self.track_id, node_idx.index() as u32);
|
||||
|
||||
self.node_index_to_stable.remove(&node_idx);
|
||||
self._node_index_to_stable.remove(&node_idx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -71,12 +69,8 @@ impl GraphBackend for AudioGraphBackend {
|
|||
input_node: BackendNodeId,
|
||||
input_port: usize,
|
||||
) -> Result<(), String> {
|
||||
let BackendNodeId::Audio(from_idx) = output_node else {
|
||||
return Err("Invalid output node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(to_idx) = input_node else {
|
||||
return Err("Invalid input node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(from_idx) = output_node;
|
||||
let BackendNodeId::Audio(to_idx) = input_node;
|
||||
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_connect(
|
||||
|
|
@ -97,12 +91,8 @@ impl GraphBackend for AudioGraphBackend {
|
|||
input_node: BackendNodeId,
|
||||
input_port: usize,
|
||||
) -> Result<(), String> {
|
||||
let BackendNodeId::Audio(from_idx) = output_node else {
|
||||
return Err("Invalid output node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(to_idx) = input_node else {
|
||||
return Err("Invalid input node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(from_idx) = output_node;
|
||||
let BackendNodeId::Audio(to_idx) = input_node;
|
||||
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_disconnect(
|
||||
|
|
@ -122,9 +112,7 @@ impl GraphBackend for AudioGraphBackend {
|
|||
param_id: u32,
|
||||
value: f64,
|
||||
) -> Result<(), String> {
|
||||
let BackendNodeId::Audio(node_idx) = backend_id else {
|
||||
return Err("Invalid backend node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(node_idx) = backend_id;
|
||||
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_set_parameter(
|
||||
|
|
@ -180,9 +168,7 @@ impl GraphBackend for AudioGraphBackend {
|
|||
x: f32,
|
||||
y: f32,
|
||||
) -> Result<BackendNodeId, String> {
|
||||
let BackendNodeId::Audio(allocator_idx) = voice_allocator_id else {
|
||||
return Err("Invalid voice allocator node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(allocator_idx) = voice_allocator_id;
|
||||
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_add_node_to_template(
|
||||
|
|
@ -194,8 +180,8 @@ impl GraphBackend for AudioGraphBackend {
|
|||
);
|
||||
|
||||
// Placeholder return
|
||||
let stable_id = self.next_stable_id;
|
||||
self.next_stable_id += 1;
|
||||
let stable_id = self._next_stable_id;
|
||||
self._next_stable_id += 1;
|
||||
let node_idx = NodeIndex::new(stable_id as usize);
|
||||
|
||||
Ok(BackendNodeId::Audio(node_idx))
|
||||
|
|
@ -209,15 +195,9 @@ impl GraphBackend for AudioGraphBackend {
|
|||
input_node: BackendNodeId,
|
||||
input_port: usize,
|
||||
) -> Result<(), String> {
|
||||
let BackendNodeId::Audio(allocator_idx) = voice_allocator_id else {
|
||||
return Err("Invalid voice allocator node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(from_idx) = output_node else {
|
||||
return Err("Invalid output node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(to_idx) = input_node else {
|
||||
return Err("Invalid input node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(allocator_idx) = voice_allocator_id;
|
||||
let BackendNodeId::Audio(from_idx) = output_node;
|
||||
let BackendNodeId::Audio(to_idx) = input_node;
|
||||
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_connect_in_template(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ pub enum BackendNodeId {
|
|||
/// Implementations:
|
||||
/// - AudioGraphBackend: Wraps daw_backend::AudioGraph via EngineController
|
||||
/// - VfxGraphBackend (future): GPU-based shader graph
|
||||
#[allow(dead_code)]
|
||||
pub trait GraphBackend: Send {
|
||||
/// Add a node to the backend graph
|
||||
fn add_node(&mut self, node_type: &str, x: f32, y: f32) -> Result<BackendNodeId, String>;
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ impl NodeGraphPane {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_track_id(
|
||||
track_id: Uuid,
|
||||
audio_controller: std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>,
|
||||
|
|
@ -207,7 +208,7 @@ impl NodeGraphPane {
|
|||
// Set parameter values
|
||||
for (¶m_id, &value) in &node.parameters {
|
||||
// Find the input param in the graph and set its value
|
||||
if let Some(node_data) = self.state.graph.nodes.get_mut(frontend_id) {
|
||||
if let Some(_node_data) = self.state.graph.nodes.get_mut(frontend_id) {
|
||||
// TODO: Set parameter values on the node's input params
|
||||
// This requires matching param_id to the input param by index
|
||||
let _ = (param_id, value); // Silence unused warning for now
|
||||
|
|
@ -428,25 +429,25 @@ impl NodeGraphPane {
|
|||
|
||||
fn check_parameter_changes(&mut self) {
|
||||
// Check all input parameters for value changes
|
||||
let mut checked_count = 0;
|
||||
let mut connection_only_count = 0;
|
||||
let mut non_float_count = 0;
|
||||
let mut _checked_count = 0;
|
||||
let mut _connection_only_count = 0;
|
||||
let mut _non_float_count = 0;
|
||||
|
||||
for (input_id, input_param) in &self.state.graph.inputs {
|
||||
// Only check parameters that can have constant values (not ConnectionOnly)
|
||||
if matches!(input_param.kind, InputParamKind::ConnectionOnly) {
|
||||
connection_only_count += 1;
|
||||
_connection_only_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get current value
|
||||
let current_value = match &input_param.value {
|
||||
ValueType::Float { value } => {
|
||||
checked_count += 1;
|
||||
_checked_count += 1;
|
||||
*value
|
||||
},
|
||||
other => {
|
||||
non_float_count += 1;
|
||||
_non_float_count += 1;
|
||||
eprintln!("[DEBUG] Non-float parameter type: {:?}", std::mem::discriminant(other));
|
||||
continue;
|
||||
}
|
||||
|
|
@ -572,7 +573,7 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
|||
// Check if track is MIDI or Audio
|
||||
if let Some(audio_controller) = &shared.audio_controller {
|
||||
let is_valid_track = {
|
||||
let controller = audio_controller.lock().unwrap();
|
||||
let _controller = audio_controller.lock().unwrap();
|
||||
// TODO: Query track type from backend
|
||||
// For now, assume it's valid if we have a track ID mapping
|
||||
true
|
||||
|
|
@ -624,7 +625,7 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
|||
let grid_color = grid_style.background_color.unwrap_or(egui::Color32::from_gray(55));
|
||||
|
||||
// Allocate the rect and render the graph editor within it
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {
|
||||
// Check for scroll input to override library's default zoom behavior
|
||||
// Only handle scroll when mouse is over the node graph area
|
||||
let pointer_over_graph = ui.rect_contains_pointer(rect);
|
||||
|
|
@ -705,8 +706,8 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
|||
|
||||
// Draw menu button in top-left corner
|
||||
let button_pos = rect.min + egui::vec2(8.0, 8.0);
|
||||
ui.allocate_ui_at_rect(
|
||||
egui::Rect::from_min_size(button_pos, egui::vec2(100.0, 24.0)),
|
||||
ui.scope_builder(
|
||||
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(button_pos, egui::vec2(100.0, 24.0))),
|
||||
|ui| {
|
||||
if ui.button("➕ Add Node").clicked() {
|
||||
// Open node finder at button's top-left position
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(dead_code)]
|
||||
//! Node Type Registry
|
||||
//!
|
||||
//! Defines metadata for all available node types
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ impl NodePalette {
|
|||
.rect_filled(rect, 0.0, egui::Color32::from_rgb(30, 30, 30));
|
||||
|
||||
// Create UI within the palette rect
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(8.0);
|
||||
|
||||
|
|
|
|||
|
|
@ -219,6 +219,7 @@ pub struct ShaderEditorPane {
|
|||
/// The shader source code being edited
|
||||
shader_code: String,
|
||||
/// Whether to show the template selector
|
||||
#[allow(dead_code)]
|
||||
show_templates: bool,
|
||||
/// Error message from last compilation attempt (if any)
|
||||
compile_error: Option<String>,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
use eframe::egui;
|
||||
use lightningbeam_core::action::Action;
|
||||
use lightningbeam_core::clip::ClipInstance;
|
||||
use lightningbeam_core::gpu::{BufferPool, BufferFormat, BufferSpec, Compositor, EffectProcessor, HDR_FORMAT, SrgbToLinearConverter};
|
||||
use lightningbeam_core::layer::{AnyLayer, AudioLayer, AudioLayerType, VideoLayer, VectorLayer};
|
||||
use lightningbeam_core::gpu::{BufferPool, BufferFormat, BufferSpec, Compositor, EffectProcessor, SrgbToLinearConverter};
|
||||
use lightningbeam_core::layer::{AnyLayer, AudioLayer};
|
||||
use lightningbeam_core::renderer::RenderedLayerType;
|
||||
use super::{DragClipType, NodePath, PaneRenderer, SharedPaneState};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
|
@ -872,7 +872,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
|||
}
|
||||
|
||||
// Also draw selection outlines for clip instances
|
||||
let clip_instance_count = self.selection.clip_instances().len();
|
||||
let _clip_instance_count = self.selection.clip_instances().len();
|
||||
for &clip_id in self.selection.clip_instances() {
|
||||
if let Some(clip_instance) = vector_layer.clip_instances.iter().find(|ci| ci.id == clip_id) {
|
||||
// Calculate clip-local time
|
||||
|
|
@ -1865,7 +1865,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
|||
// Clamp to texture bounds
|
||||
if tex_x < width && tex_y < height {
|
||||
// Create a staging buffer to read back the pixel
|
||||
let bytes_per_pixel = 4; // RGBA8
|
||||
let _bytes_per_pixel = 4; // RGBA8
|
||||
// Align bytes_per_row to 256 (wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
|
||||
let bytes_per_row_alignment = 256u32;
|
||||
let bytes_per_row = bytes_per_row_alignment; // Single pixel, use minimum alignment
|
||||
|
|
@ -2128,7 +2128,6 @@ impl StagePane {
|
|||
use lightningbeam_core::tool::ToolState;
|
||||
use lightningbeam_core::layer::AnyLayer;
|
||||
use lightningbeam_core::hit_test::{self, hit_test_vector_editing, EditingHitTolerance, VectorEditHit};
|
||||
use lightningbeam_core::bezpath_editing::{extract_editable_curves, mold_curve};
|
||||
use vello::kurbo::{Point, Rect as KurboRect, Affine};
|
||||
|
||||
// Check if we have an active vector layer
|
||||
|
|
@ -2618,9 +2617,8 @@ impl StagePane {
|
|||
mouse_pos: vello::kurbo::Point,
|
||||
shared: &mut SharedPaneState,
|
||||
) {
|
||||
use lightningbeam_core::bezpath_editing::{mold_curve, rebuild_bezpath};
|
||||
use lightningbeam_core::bezpath_editing::mold_curve;
|
||||
use lightningbeam_core::tool::ToolState;
|
||||
use vello::kurbo::Point;
|
||||
|
||||
// Clone tool state to get owned values
|
||||
let tool_state = shared.tool_state.clone();
|
||||
|
|
@ -2799,12 +2797,12 @@ impl StagePane {
|
|||
ui: &mut egui::Ui,
|
||||
response: &egui::Response,
|
||||
world_pos: egui::Vec2,
|
||||
shift_held: bool,
|
||||
_shift_held: bool,
|
||||
shared: &mut SharedPaneState,
|
||||
) {
|
||||
use lightningbeam_core::tool::ToolState;
|
||||
use lightningbeam_core::layer::AnyLayer;
|
||||
use lightningbeam_core::hit_test::{self, hit_test_vector_editing, EditingHitTolerance, VectorEditHit};
|
||||
use lightningbeam_core::hit_test::{hit_test_vector_editing, EditingHitTolerance, VectorEditHit};
|
||||
use vello::kurbo::{Point, Affine};
|
||||
|
||||
// Check if we have an active vector layer
|
||||
|
|
@ -2897,7 +2895,7 @@ impl StagePane {
|
|||
shape_instance_id: uuid::Uuid,
|
||||
curve_index: usize,
|
||||
point_index: u8,
|
||||
mouse_pos: vello::kurbo::Point,
|
||||
_mouse_pos: vello::kurbo::Point,
|
||||
active_layer_id: uuid::Uuid,
|
||||
shared: &mut SharedPaneState,
|
||||
) {
|
||||
|
|
@ -3606,7 +3604,7 @@ impl StagePane {
|
|||
|
||||
// Mouse drag: add points to path
|
||||
if response.dragged() {
|
||||
if let ToolState::DrawingPath { points, simplify_mode } = &mut *shared.tool_state {
|
||||
if let ToolState::DrawingPath { points, simplify_mode: _ } = &mut *shared.tool_state {
|
||||
// Only add point if it's far enough from the last point (reduce noise)
|
||||
const MIN_POINT_DISTANCE: f64 = 2.0;
|
||||
|
||||
|
|
@ -3760,63 +3758,12 @@ impl StagePane {
|
|||
}
|
||||
}
|
||||
|
||||
/// Decompose an affine matrix into transform components
|
||||
/// Returns (translation_x, translation_y, rotation_deg, scale_x, scale_y, skew_x_deg, skew_y_deg)
|
||||
fn decompose_affine(affine: kurbo::Affine) -> (f64, f64, f64, f64, f64, f64, f64) {
|
||||
let coeffs = affine.as_coeffs();
|
||||
let a = coeffs[0];
|
||||
let b = coeffs[1];
|
||||
let c = coeffs[2];
|
||||
let d = coeffs[3];
|
||||
let e = coeffs[4]; // translation_x
|
||||
let f = coeffs[5]; // translation_y
|
||||
|
||||
// Extract translation
|
||||
let tx = e;
|
||||
let ty = f;
|
||||
|
||||
// Decompose linear part [[a, c], [b, d]] into rotate * scale * skew
|
||||
// Using QR-like decomposition
|
||||
|
||||
// Extract rotation
|
||||
let rotation_rad = b.atan2(a);
|
||||
let cos_r = rotation_rad.cos();
|
||||
let sin_r = rotation_rad.sin();
|
||||
|
||||
// Remove rotation to get scale * skew
|
||||
// R^(-1) * M where M = [[a, c], [b, d]]
|
||||
let m11 = a * cos_r + b * sin_r;
|
||||
let m12 = c * cos_r + d * sin_r;
|
||||
let m21 = -a * sin_r + b * cos_r;
|
||||
let m22 = -c * sin_r + d * cos_r;
|
||||
|
||||
// Now [[m11, m12], [m21, m22]] = scale * skew
|
||||
// scale * skew = [[sx, 0], [0, sy]] * [[1, tan(skew_y)], [tan(skew_x), 1]]
|
||||
// = [[sx, sx*tan(skew_y)], [sy*tan(skew_x), sy]]
|
||||
|
||||
let scale_x = m11;
|
||||
let scale_y = m22;
|
||||
|
||||
let skew_x_rad = if scale_y.abs() > 0.001 { (m21 / scale_y).atan() } else { 0.0 };
|
||||
let skew_y_rad = if scale_x.abs() > 0.001 { (m12 / scale_x).atan() } else { 0.0 };
|
||||
|
||||
(
|
||||
tx,
|
||||
ty,
|
||||
rotation_rad.to_degrees(),
|
||||
scale_x,
|
||||
scale_y,
|
||||
skew_x_rad.to_degrees(),
|
||||
skew_y_rad.to_degrees(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Apply transform preview to objects based on current mouse position
|
||||
fn apply_transform_preview(
|
||||
vector_layer: &mut lightningbeam_core::layer::VectorLayer,
|
||||
mode: &lightningbeam_core::tool::TransformMode,
|
||||
original_transforms: &std::collections::HashMap<uuid::Uuid, lightningbeam_core::object::Transform>,
|
||||
pivot: vello::kurbo::Point,
|
||||
_pivot: vello::kurbo::Point,
|
||||
start_mouse: vello::kurbo::Point,
|
||||
current_mouse: vello::kurbo::Point,
|
||||
original_bbox: vello::kurbo::Rect,
|
||||
|
|
@ -4784,7 +4731,6 @@ impl StagePane {
|
|||
_ => egui::CursorIcon::Default,
|
||||
};
|
||||
ui.ctx().set_cursor_icon(cursor);
|
||||
hovering_handle = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -5219,7 +5165,6 @@ impl StagePane {
|
|||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else if let AnyLayer::Video(video_layer) = layer {
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ impl TimelinePane {
|
|||
|
||||
/// Execute a view action with the given parameters
|
||||
/// Called from main.rs after determining this is the best handler
|
||||
#[allow(dead_code)] // Mirrors StagePane; wiring in main.rs pending (see TODO at view action dispatch)
|
||||
pub fn execute_view_action(&mut self, action: &crate::menu::MenuAction, zoom_center: egui::Vec2) {
|
||||
use crate::menu::MenuAction;
|
||||
match action {
|
||||
|
|
@ -781,7 +782,7 @@ impl TimelinePane {
|
|||
|
||||
// Mute button
|
||||
// TODO: Replace with SVG icon (volume-up-fill.svg / volume-mute.svg)
|
||||
let mute_response = ui.allocate_new_ui(egui::UiBuilder::new().max_rect(mute_button_rect), |ui| {
|
||||
let mute_response = ui.scope_builder(egui::UiBuilder::new().max_rect(mute_button_rect), |ui| {
|
||||
let mute_text = if is_muted { "🔇" } else { "🔊" };
|
||||
let button = egui::Button::new(mute_text)
|
||||
.fill(if is_muted {
|
||||
|
|
@ -805,7 +806,7 @@ impl TimelinePane {
|
|||
|
||||
// Solo button
|
||||
// TODO: Replace with SVG headphones icon
|
||||
let solo_response = ui.allocate_new_ui(egui::UiBuilder::new().max_rect(solo_button_rect), |ui| {
|
||||
let solo_response = ui.scope_builder(egui::UiBuilder::new().max_rect(solo_button_rect), |ui| {
|
||||
let button = egui::Button::new("🎧")
|
||||
.fill(if is_soloed {
|
||||
egui::Color32::from_rgba_unmultiplied(100, 200, 100, 100)
|
||||
|
|
@ -828,7 +829,7 @@ impl TimelinePane {
|
|||
|
||||
// Lock button
|
||||
// TODO: Replace with SVG lock/lock-open icons
|
||||
let lock_response = ui.allocate_new_ui(egui::UiBuilder::new().max_rect(lock_button_rect), |ui| {
|
||||
let lock_response = ui.scope_builder(egui::UiBuilder::new().max_rect(lock_button_rect), |ui| {
|
||||
let lock_text = if is_locked { "🔒" } else { "🔓" };
|
||||
let button = egui::Button::new(lock_text)
|
||||
.fill(if is_locked {
|
||||
|
|
@ -851,7 +852,7 @@ impl TimelinePane {
|
|||
}
|
||||
|
||||
// Volume slider (nonlinear: 0-70% slider = 0-100% volume, 70-100% slider = 100-200% volume)
|
||||
let volume_response = ui.allocate_new_ui(egui::UiBuilder::new().max_rect(volume_slider_rect), |ui| {
|
||||
let volume_response = ui.scope_builder(egui::UiBuilder::new().max_rect(volume_slider_rect), |ui| {
|
||||
// Map volume (0.0-2.0) to slider position (0.0-1.0)
|
||||
let slider_value = if current_volume <= 1.0 {
|
||||
// 0.0-1.0 volume maps to 0.0-0.7 slider (70%)
|
||||
|
|
@ -1217,7 +1218,7 @@ impl TimelinePane {
|
|||
if let Some((samples, sr, ch)) = raw_audio_cache.get(audio_pool_index) {
|
||||
let total_frames = samples.len() / (*ch).max(1) as usize;
|
||||
let audio_file_duration = total_frames as f64 / *sr as f64;
|
||||
let screen_size = ui.ctx().screen_rect().size();
|
||||
let screen_size = ui.ctx().content_rect().size();
|
||||
|
||||
let pending_upload = if waveform_gpu_dirty.contains(audio_pool_index) {
|
||||
waveform_gpu_dirty.remove(audio_pool_index);
|
||||
|
|
@ -1347,7 +1348,7 @@ impl TimelinePane {
|
|||
fn handle_input(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
full_timeline_rect: egui::Rect,
|
||||
_full_timeline_rect: egui::Rect,
|
||||
ruler_rect: egui::Rect,
|
||||
content_rect: egui::Rect,
|
||||
header_rect: egui::Rect,
|
||||
|
|
@ -1357,7 +1358,7 @@ impl TimelinePane {
|
|||
selection: &mut lightningbeam_core::selection::Selection,
|
||||
pending_actions: &mut Vec<Box<dyn lightningbeam_core::action::Action>>,
|
||||
playback_time: &mut f64,
|
||||
is_playing: &mut bool,
|
||||
_is_playing: &mut bool,
|
||||
audio_controller: Option<&std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
|
||||
) {
|
||||
// Don't allocate the header area for input - let widgets handle it directly
|
||||
|
|
@ -2296,7 +2297,7 @@ impl PaneRenderer for TimelinePane {
|
|||
*shared.dragging_asset = None;
|
||||
} else {
|
||||
// Get document dimensions for centering and create clip instance
|
||||
let (center_x, center_y, mut clip_instance) = {
|
||||
let (_center_x, _center_y, clip_instance) = {
|
||||
let doc = shared.action_executor.document();
|
||||
let center_x = doc.width / 2.0;
|
||||
let center_y = doc.height / 2.0;
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ impl VirtualPianoPane {
|
|||
|
||||
// Handle interaction (skip if a black key is being interacted with)
|
||||
let key_id = ui.id().with(("white_key", note));
|
||||
let response = ui.interact(key_rect, key_id, egui::Sense::click_and_drag());
|
||||
let _response = ui.interact(key_rect, key_id, egui::Sense::click_and_drag());
|
||||
|
||||
// Visual feedback for pressed keys (check both pressed_notes and current pointer state)
|
||||
let pointer_over_key = ui.input(|i| {
|
||||
|
|
@ -298,7 +298,7 @@ impl VirtualPianoPane {
|
|||
|
||||
// Handle interaction (same as white keys)
|
||||
let key_id = ui.id().with(("black_key", note));
|
||||
let response = ui.interact(key_rect, key_id, egui::Sense::click_and_drag());
|
||||
let _response = ui.interact(key_rect, key_id, egui::Sense::click_and_drag());
|
||||
|
||||
// Visual feedback for pressed keys (check both pressed_notes and current pointer state)
|
||||
let pointer_over_key = ui.input(|i| {
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ impl PreferencesDialog {
|
|||
ui.label("Default BPM:");
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.working_prefs.bpm)
|
||||
.clamp_range(20..=300)
|
||||
.range(20..=300)
|
||||
.speed(1.0),
|
||||
);
|
||||
});
|
||||
|
|
@ -201,7 +201,7 @@ impl PreferencesDialog {
|
|||
ui.label("Default Framerate:");
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.working_prefs.framerate)
|
||||
.clamp_range(1..=120)
|
||||
.range(1..=120)
|
||||
.speed(1.0)
|
||||
.suffix(" fps"),
|
||||
);
|
||||
|
|
@ -211,7 +211,7 @@ impl PreferencesDialog {
|
|||
ui.label("Default File Width:");
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.working_prefs.file_width)
|
||||
.clamp_range(100..=10000)
|
||||
.range(100..=10000)
|
||||
.speed(10.0)
|
||||
.suffix(" px"),
|
||||
);
|
||||
|
|
@ -221,7 +221,7 @@ impl PreferencesDialog {
|
|||
ui.label("Default File Height:");
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.working_prefs.file_height)
|
||||
.clamp_range(100..=10000)
|
||||
.range(100..=10000)
|
||||
.speed(10.0)
|
||||
.suffix(" px"),
|
||||
);
|
||||
|
|
@ -231,7 +231,7 @@ impl PreferencesDialog {
|
|||
ui.label("Scroll Speed:");
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.working_prefs.scroll_speed)
|
||||
.clamp_range(0.1..=10.0)
|
||||
.range(0.1..=10.0)
|
||||
.speed(0.1),
|
||||
);
|
||||
});
|
||||
|
|
@ -245,7 +245,7 @@ impl PreferencesDialog {
|
|||
ui.horizontal(|ui| {
|
||||
ui.label("Audio Buffer Size:");
|
||||
|
||||
egui::ComboBox::from_id_source("audio_buffer_size")
|
||||
egui::ComboBox::from_id_salt("audio_buffer_size")
|
||||
.selected_text(format!("{} samples", self.working_prefs.audio_buffer_size))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(
|
||||
|
|
@ -292,7 +292,7 @@ impl PreferencesDialog {
|
|||
ui.horizontal(|ui| {
|
||||
ui.label("Theme:");
|
||||
|
||||
egui::ComboBox::from_id_source("theme_mode")
|
||||
egui::ComboBox::from_id_salt("theme_mode")
|
||||
.selected_text(format!("{:?}", self.working_prefs.theme_mode))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(
|
||||
|
|
|
|||
|
|
@ -4,4 +4,3 @@
|
|||
|
||||
pub mod dialog;
|
||||
|
||||
pub use dialog::{PreferencesDialog, PreferencesSaveResult};
|
||||
|
|
|
|||
|
|
@ -46,27 +46,6 @@ pub struct Style {
|
|||
// Add more properties as needed
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Merge another style into this one (other's properties override if present)
|
||||
pub fn merge(&mut self, other: &Style) {
|
||||
if other.background_color.is_some() {
|
||||
self.background_color = other.background_color;
|
||||
}
|
||||
if other.border_color.is_some() {
|
||||
self.border_color = other.border_color;
|
||||
}
|
||||
if other.text_color.is_some() {
|
||||
self.text_color = other.text_color;
|
||||
}
|
||||
if other.width.is_some() {
|
||||
self.width = other.width;
|
||||
}
|
||||
if other.height.is_some() {
|
||||
self.height = other.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Theme {
|
||||
light_variables: HashMap<String, String>,
|
||||
|
|
@ -229,21 +208,13 @@ impl Theme {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get a CSS variable value and parse as color (backward compatibility helper)
|
||||
/// This allows old code using theme.color("variable-name") to work
|
||||
pub fn color(&self, var_name: &str) -> Option<egui::Color32> {
|
||||
// Try light variables first, then dark variables
|
||||
let value = self.light_variables.get(var_name)
|
||||
.or_else(|| self.dark_variables.get(var_name))?;
|
||||
parse_hex_color(value)
|
||||
}
|
||||
|
||||
/// Get the number of loaded selectors
|
||||
pub fn len(&self) -> usize {
|
||||
self.light_styles.len()
|
||||
}
|
||||
|
||||
/// Check if theme has no styles
|
||||
#[allow(dead_code)] // Used in tests
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.light_styles.is_empty()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ use wgpu::util::DeviceExt;
|
|||
/// Fixed texture width (power of 2) for all waveform textures
|
||||
const TEX_WIDTH: u32 = 2048;
|
||||
|
||||
/// Maximum number of texture segments per audio clip
|
||||
const MAX_SEGMENTS: u32 = 16;
|
||||
|
||||
/// GPU resources for all waveform textures, stored in CallbackResources
|
||||
pub struct WaveformGpuResources {
|
||||
/// Per-audio-pool-index GPU data
|
||||
|
|
@ -34,6 +31,7 @@ pub struct WaveformGpuResources {
|
|||
}
|
||||
|
||||
/// GPU data for a single audio file
|
||||
#[allow(dead_code)] // textures/texture_views must stay alive to back bind groups; metadata for future use
|
||||
pub struct WaveformGpuEntry {
|
||||
/// One texture per segment (for long audio split across multiple textures)
|
||||
pub textures: Vec<wgpu::Texture>,
|
||||
|
|
@ -612,12 +610,6 @@ fn compute_mip_count(width: u32, height: u32) -> u32 {
|
|||
(max_dim as f32).log2().floor() as u32 + 1
|
||||
}
|
||||
|
||||
/// Calculate how many texture segments are needed for a given frame count
|
||||
pub fn segment_count_for_frames(total_frames: u64, max_texture_height: u32) -> u32 {
|
||||
let max_frames_per_segment = TEX_WIDTH as u64 * max_texture_height as u64;
|
||||
((total_frames + max_frames_per_segment - 1) / max_frames_per_segment) as u32
|
||||
}
|
||||
|
||||
/// Get the fixed texture width used for all waveform textures
|
||||
pub fn tex_width() -> u32 {
|
||||
TEX_WIDTH
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ fn key_to_char(key: egui::Key, shift: bool) -> Option<char> {
|
|||
}
|
||||
|
||||
/// Response from the IME text field widget
|
||||
#[allow(dead_code)] // Standard widget response fields; callers will use as features expand
|
||||
pub struct ImeTextFieldResponse {
|
||||
/// The egui response for the text field area
|
||||
pub response: egui::Response,
|
||||
|
|
|
|||
Loading…
Reference in New Issue