Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,12 @@ build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
cargo-check *args:
cargo check --all-features {{args}}

# Runs clippy
# Runs clippy (used in CI - default warnings only)
clippy *args:
cargo clippy --all-features {{args}} -- -D warnings

# Runs clippy with pedantic warnings (for development)
clippy-pedantic *args:
cargo clippy --all-features {{args}} -- -W clippy::pedantic

# Runs clippy with JSON message format
Expand All @@ -69,8 +73,8 @@ fmt-check:
test *args:
cargo test {{args}}

# Run all checks (format, cargo check, test)
check: fmt-check cargo-check test
# Run all checks (format, clippy, cargo check, test)
check: fmt-check clippy cargo-check test

# ============================================================================
# Development
Expand Down
18 changes: 10 additions & 8 deletions src/app/camera_preview/widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,16 @@ impl AppModel {

let video_elem = video_widget::video_widget(
frame.clone(),
video_id,
content_fit,
filter_mode,
0.0,
should_mirror,
crop_uv,
zoom_level,
scroll_zoom_enabled,
video_widget::VideoWidgetConfig {
video_id,
content_fit,
filter_type: filter_mode,
corner_radius: 0.0,
mirror_horizontal: should_mirror,
crop_uv,
zoom_level,
scroll_zoom_enabled,
},
);

widget::container(video_elem)
Expand Down
18 changes: 10 additions & 8 deletions src/app/filter_picker/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,16 @@ impl AppModel {
// The video widget fills its container and handles aspect ratio via Cover mode
video_widget::video_widget(
Arc::clone(frame),
99, // Shared source texture ID for all filter previews
VideoContentFit::Cover,
filter_type,
corner_radius,
self.config.mirror_preview,
None, // No aspect ratio cropping in filter previews
1.0, // No zoom for filter previews
false, // No scroll zoom for filter previews
video_widget::VideoWidgetConfig {
video_id: 99, // Shared source texture ID for all filter previews
content_fit: VideoContentFit::Cover,
filter_type,
corner_radius,
mirror_horizontal: self.config.mirror_preview,
crop_uv: None, // No aspect ratio cropping in filter previews
zoom_level: 1.0, // No zoom for filter previews
scroll_zoom_enabled: false, // No scroll zoom for filter previews
},
)
} else {
// Fallback: colored placeholder when no camera frame
Expand Down
2 changes: 1 addition & 1 deletion src/app/handlers/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ impl AppModel {
},
|(controls, settings, color_settings)| {
cosmic::Action::App(Message::ExposureControlsQueried(
controls,
Box::new(controls),
settings,
color_settings,
))
Expand Down
25 changes: 13 additions & 12 deletions src/app/handlers/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ impl AppModel {
async move {
use crate::pipelines::video::{
AudioChannels, AudioQuality, EncoderConfig, VideoQuality, VideoRecorder,
VideoRecorderConfig,
};

let config = EncoderConfig {
Expand All @@ -768,21 +769,21 @@ impl AppModel {
bitrate_override_kbps: Some(bitrate_kbps),
};

let recorder = match VideoRecorder::new(
&device_path,
metadata_path.as_deref(),
let recorder = match VideoRecorder::new(VideoRecorderConfig {
device_path: &device_path,
metadata_path: metadata_path.as_deref(),
width,
height,
framerate,
&pixel_format,
output_path.clone(),
config,
audio_device.is_some(),
audio_device.as_deref(),
None,
selected_encoder.as_ref(),
sensor_rotation,
) {
pixel_format: &pixel_format,
output_path: output_path.clone(),
encoder_config: config,
enable_audio: audio_device.is_some(),
audio_device: audio_device.as_deref(),
preview_sender: None,
encoder_info: selected_encoder.as_ref(),
rotation: sensor_rotation,
}) {
Ok(r) => r,
Err(e) => return Err(e),
};
Expand Down
6 changes: 3 additions & 3 deletions src/app/handlers/exposure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ impl AppModel {

pub(crate) fn handle_exposure_controls_queried(
&mut self,
controls: AvailableExposureControls,
controls: Box<AvailableExposureControls>,
settings: ExposureSettings,
color_settings: ColorSettings,
) -> Task<cosmic::Action<Message>> {
Expand All @@ -505,7 +505,7 @@ impl AppModel {
has_iso = controls.iso.available,
"Exposure controls queried"
);
self.available_exposure_controls = controls;
self.available_exposure_controls = *controls;
self.exposure_settings = Some(settings);
self.color_settings = Some(color_settings);
Task::none()
Expand Down Expand Up @@ -562,7 +562,7 @@ impl AppModel {
},
|(controls, settings, color_settings)| {
cosmic::Action::App(Message::ExposureControlsQueried(
controls,
Box::new(controls),
settings,
color_settings,
))
Expand Down
16 changes: 8 additions & 8 deletions src/app/handlers/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ impl AppModel {
"Toggled record audio"
);

if let Some(handler) = self.config_handler.as_ref() {
if let Err(err) = self.config.write_entry(handler) {
error!(?err, "Failed to save record audio setting");
}
if let Some(handler) = self.config_handler.as_ref()
&& let Err(err) = self.config.write_entry(handler)
{
error!(?err, "Failed to save record audio setting");
}
Task::none()
}
Expand All @@ -185,10 +185,10 @@ impl AppModel {
info!(?encoder, "Selected audio encoder");
self.config.audio_encoder = encoder;

if let Some(handler) = self.config_handler.as_ref() {
if let Err(err) = self.config.write_entry(handler) {
error!(?err, "Failed to save audio encoder selection");
}
if let Some(handler) = self.config_handler.as_ref()
&& let Err(err) = self.config.write_entry(handler)
{
error!(?err, "Failed to save audio encoder selection");
}
}
Task::none()
Expand Down
9 changes: 6 additions & 3 deletions src/app/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,7 +972,6 @@ pub enum ContextPage {
/// - **Settings**: Configuration, audio/video encoder selection
/// - **System**: Bug reports, recovery, external URLs
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Message {
// ===== UI Navigation =====
/// Open external URL (repository, etc.)
Expand Down Expand Up @@ -1019,8 +1018,12 @@ pub enum Message {
SetMeteringMode(MeteringMode),
/// Toggle auto exposure priority (allow frame rate variation)
ToggleAutoExposurePriority,
/// Exposure controls queried from camera
ExposureControlsQueried(AvailableExposureControls, ExposureSettings, ColorSettings),
/// Exposure controls queried from camera (boxed to reduce enum size)
ExposureControlsQueried(
Box<AvailableExposureControls>,
ExposureSettings,
ColorSettings,
),
/// Exposure control change applied successfully
ExposureControlApplied,
/// White balance toggled, with optional temperature value when switching to manual
Expand Down
86 changes: 38 additions & 48 deletions src/app/video_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,27 @@ pub enum VideoContentFit {
Cover,
}

/// Configuration for creating a video widget
#[derive(Debug, Clone)]
pub struct VideoWidgetConfig {
/// Unique identifier for this video stream
pub video_id: u64,
/// How to scale content within bounds
pub content_fit: VideoContentFit,
/// Filter to apply to the video
pub filter_type: FilterType,
/// Corner radius for rounded corners (0.0 for sharp corners)
pub corner_radius: f32,
/// Whether to mirror the video horizontally
pub mirror_horizontal: bool,
/// Optional crop UV coordinates (u_min, v_min, u_max, v_max) in 0-1 range
pub crop_uv: Option<(f32, f32, f32, f32)>,
/// Zoom level (1.0 = no zoom, 2.0 = 2x zoom)
pub zoom_level: f32,
/// Whether scroll wheel zoom is enabled
pub scroll_zoom_enabled: bool,
}

/// Video widget that renders camera frames using a custom GPU primitive
pub struct VideoWidget {
primitive: VideoPrimitive,
Expand All @@ -44,30 +65,18 @@ impl VideoWidget {
/// Create a new video widget from a camera frame
///
/// # Arguments
/// * `crop_uv` - Optional crop UV coordinates (u_min, v_min, u_max, v_max) in 0-1 range
/// * `zoom_level` - Zoom level (1.0 = no zoom, 2.0 = 2x zoom)
/// * `scroll_zoom_enabled` - Whether scroll wheel zoom is enabled
#[allow(clippy::too_many_arguments)]
pub fn new(
frame: Arc<CameraFrame>,
video_id: u64,
content_fit: VideoContentFit,
filter_type: FilterType,
corner_radius: f32,
mirror_horizontal: bool,
crop_uv: Option<(f32, f32, f32, f32)>,
zoom_level: f32,
scroll_zoom_enabled: bool,
) -> Self {
let mut primitive = VideoPrimitive::new(video_id);
primitive.filter_type = filter_type;
primitive.corner_radius = corner_radius;
primitive.mirror_horizontal = mirror_horizontal;
primitive.crop_uv = crop_uv;
primitive.zoom_level = zoom_level;
/// * `frame` - The camera frame to display
/// * `config` - Widget configuration options
pub fn new(frame: Arc<CameraFrame>, config: VideoWidgetConfig) -> Self {
let mut primitive = VideoPrimitive::new(config.video_id);
primitive.filter_type = config.filter_type;
primitive.corner_radius = config.corner_radius;
primitive.mirror_horizontal = config.mirror_horizontal;
primitive.crop_uv = config.crop_uv;
primitive.zoom_level = config.zoom_level;

// Calculate aspect ratio from frame dimensions, adjusted for crop
let aspect_ratio = if let Some((u_min, v_min, u_max, v_max)) = crop_uv {
let aspect_ratio = if let Some((u_min, v_min, u_max, v_max)) = config.crop_uv {
// Use cropped region's aspect ratio
let crop_width = (u_max - u_min) * frame.width as f32;
let crop_height = (v_max - v_min) * frame.height as f32;
Expand Down Expand Up @@ -104,7 +113,7 @@ impl VideoWidget {
};

let video_frame = VideoFrame {
id: video_id,
id: config.video_id,
width: frame.width,
height: frame.height,
data: frame.data.clone(), // Clone FrameData - just refcount increment, no data copy
Expand All @@ -121,8 +130,8 @@ impl VideoWidget {
width: Length::Fill,
height: Length::Fill,
aspect_ratio,
content_fit,
scroll_zoom_enabled,
content_fit: config.content_fit,
scroll_zoom_enabled: config.scroll_zoom_enabled,
}
}
}
Expand Down Expand Up @@ -241,30 +250,11 @@ impl<'a> From<VideoWidget> for Element<'a, crate::app::Message, Theme, Renderer>
/// Create a video widget from a camera frame
///
/// # Arguments
/// * `crop_uv` - Optional crop UV coordinates (u_min, v_min, u_max, v_max) in 0-1 range
/// * `zoom_level` - Zoom level (1.0 = no zoom, 2.0 = 2x zoom)
/// * `scroll_zoom_enabled` - Whether scroll wheel zoom is enabled
#[allow(clippy::too_many_arguments)]
/// * `frame` - The camera frame to display
/// * `config` - Widget configuration options
pub fn video_widget<'a>(
frame: Arc<CameraFrame>,
video_id: u64,
content_fit: VideoContentFit,
filter_type: FilterType,
corner_radius: f32,
mirror_horizontal: bool,
crop_uv: Option<(f32, f32, f32, f32)>,
zoom_level: f32,
scroll_zoom_enabled: bool,
config: VideoWidgetConfig,
) -> Element<'a, crate::app::Message, Theme, Renderer> {
Element::new(VideoWidget::new(
frame,
video_id,
content_fit,
filter_type,
corner_radius,
mirror_horizontal,
crop_uv,
zoom_level,
scroll_zoom_enabled,
))
Element::new(VideoWidget::new(frame, config))
}
26 changes: 13 additions & 13 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use camera::backends::camera::pipewire::{
};
use camera::backends::camera::types::{CameraFormat, CameraFrame};
use camera::pipelines::photo::PhotoPipeline;
use camera::pipelines::video::{EncoderConfig, VideoRecorder};
use camera::pipelines::video::{EncoderConfig, VideoRecorder, VideoRecorderConfig};
use chrono::Local;
use futures::channel::mpsc;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -244,21 +244,21 @@ pub fn record_video(
let encoder_config = EncoderConfig::default();

// Create video recorder
let recorder = VideoRecorder::new(
&camera.path,
camera.metadata_path.as_deref(),
format.width,
format.height,
let recorder = VideoRecorder::new(VideoRecorderConfig {
device_path: &camera.path,
metadata_path: camera.metadata_path.as_deref(),
width: format.width,
height: format.height,
framerate,
&format.pixel_format,
output_path.clone(),
pixel_format: &format.pixel_format,
output_path: output_path.clone(),
encoder_config,
enable_audio,
None, // Use default audio device
None, // No preview sender needed for CLI
None, // Auto-select encoder
camera.rotation,
)?;
audio_device: None, // Use default audio device
preview_sender: None, // No preview sender needed for CLI
encoder_info: None, // Auto-select encoder
rotation: camera.rotation,
})?;

// Start recording
println!();
Expand Down
2 changes: 0 additions & 2 deletions src/pipelines/photo/burst_mode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1732,7 +1732,6 @@ impl BurstModeGpuPipeline {
// Align using pre-allocated buffers
let gpu_frame = self
.align_single_frame_pooled(
&ref_rgba_buffer,
&ref_pyramids,
frame,
width,
Expand Down Expand Up @@ -1906,7 +1905,6 @@ impl BurstModeGpuPipeline {
#[allow(clippy::too_many_arguments)]
async fn align_single_frame_pooled(
&self,
_ref_rgba_buffer: &wgpu::Buffer,
ref_pyramids: &ReferencePyramids,
comparison: &CameraFrame,
width: u32,
Expand Down
Loading
Loading