Merge branch 'rust-ui' of /home/skyler/Dev/Lightningbeam-2/. into rust-ui

This commit is contained in:
Skyler Lehmkuhl 2026-03-02 00:01:35 -05:00
commit a45d674ed7
6 changed files with 91 additions and 69 deletions

View File

@ -438,10 +438,38 @@ pub fn render_document_with_transform(
/// Draw the document background /// Draw the document background
fn render_background(document: &Document, scene: &mut Scene, base_transform: Affine) { fn render_background(document: &Document, scene: &mut Scene, base_transform: Affine) {
let background_rect = Rect::new(0.0, 0.0, document.width, document.height); let background_rect = Rect::new(0.0, 0.0, document.width, document.height);
let bg = &document.background_color;
// Convert our ShapeColor to vello's peniko Color // Draw checkerboard behind transparent backgrounds
let background_color = document.background_color.to_peniko(); if bg.a < 255 {
use vello::peniko::{Blob, Color, Extend, ImageAlphaType, ImageData, ImageQuality};
// 2x2 pixel checkerboard pattern: light/dark alternating
let light: [u8; 4] = [204, 204, 204, 255];
let dark: [u8; 4] = [170, 170, 170, 255];
let pixels: Vec<u8> = [light, dark, dark, light].concat();
let image_data = ImageData {
data: Blob::from(pixels),
format: ImageFormat::Rgba8,
width: 2,
height: 2,
alpha_type: ImageAlphaType::AlphaPremultiplied,
};
let brush = ImageBrush::new(image_data)
.with_extend(Extend::Repeat)
.with_quality(ImageQuality::Low);
// Scale each pixel to 16x16 document units
let brush_transform = Affine::scale(16.0);
scene.fill(
Fill::NonZero,
base_transform,
&brush,
Some(brush_transform),
&background_rect,
);
}
// Draw the background color on top (alpha-blended)
let background_color = bg.to_peniko();
scene.fill( scene.fill(
Fill::NonZero, Fill::NonZero,
base_transform, base_transform,

View File

@ -763,6 +763,7 @@ struct EditorApp {
brush_opacity: f32, // brush opacity 0.01.0 brush_opacity: f32, // brush opacity 0.01.0
brush_hardness: f32, // brush hardness 0.01.0 brush_hardness: f32, // brush hardness 0.01.0
brush_spacing: f32, // dabs_per_radius (fraction of radius per dab) brush_spacing: f32, // dabs_per_radius (fraction of radius per dab)
brush_use_fg: bool, // true = paint with FG (stroke) color, false = BG (fill) color
// Audio engine integration // Audio engine integration
#[allow(dead_code)] // Must be kept alive to maintain audio output #[allow(dead_code)] // Must be kept alive to maintain audio output
audio_stream: Option<cpal::Stream>, audio_stream: Option<cpal::Stream>,
@ -1043,6 +1044,7 @@ impl EditorApp {
brush_opacity: 1.0, brush_opacity: 1.0,
brush_hardness: 0.5, brush_hardness: 0.5,
brush_spacing: 0.1, brush_spacing: 0.1,
brush_use_fg: true,
audio_stream, audio_stream,
audio_controller, audio_controller,
audio_event_rx, audio_event_rx,
@ -5420,6 +5422,7 @@ impl eframe::App for EditorApp {
brush_opacity: &mut self.brush_opacity, brush_opacity: &mut self.brush_opacity,
brush_hardness: &mut self.brush_hardness, brush_hardness: &mut self.brush_hardness,
brush_spacing: &mut self.brush_spacing, brush_spacing: &mut self.brush_spacing,
brush_use_fg: &mut self.brush_use_fg,
audio_controller: self.audio_controller.as_ref(), audio_controller: self.audio_controller.as_ref(),
video_manager: &self.video_manager, video_manager: &self.video_manager,
playback_time: &mut self.playback_time, playback_time: &mut self.playback_time,

View File

@ -302,6 +302,14 @@ impl InfopanelPane {
// Raster paint tools // Raster paint tools
Tool::Draw | Tool::Erase | Tool::Smudge if is_raster_paint_tool => { Tool::Draw | Tool::Erase | Tool::Smudge if is_raster_paint_tool => {
// Color source toggle (Draw tool only)
if matches!(tool, Tool::Draw) {
ui.horizontal(|ui| {
ui.label("Color:");
ui.selectable_value(shared.brush_use_fg, true, "FG");
ui.selectable_value(shared.brush_use_fg, false, "BG");
});
}
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Size:"); ui.label("Size:");
ui.add( ui.add(
@ -374,20 +382,9 @@ impl InfopanelPane {
ui.label("Fill:"); ui.label("Fill:");
match info.fill_color { match info.fill_color {
Some(Some(color)) => { Some(Some(color)) => {
let mut egui_color = egui::Color32::from_rgba_unmultiplied( let mut rgba = [color.r, color.g, color.b, color.a];
color.r, color.g, color.b, color.a, if ui.color_edit_button_srgba_unmultiplied(&mut rgba).changed() {
); let new_color = ShapeColor::rgba(rgba[0], rgba[1], rgba[2], rgba[3]);
if egui::color_picker::color_edit_button_srgba(
ui,
&mut egui_color,
egui::color_picker::Alpha::OnlyBlend,
).changed() {
let new_color = ShapeColor {
r: egui_color.r(),
g: egui_color.g(),
b: egui_color.b(),
a: egui_color.a(),
};
let action = SetShapePropertiesAction::set_fill_color( let action = SetShapePropertiesAction::set_fill_color(
layer_id, time, face_ids.clone(), Some(new_color), layer_id, time, face_ids.clone(), Some(new_color),
); );
@ -408,20 +405,9 @@ impl InfopanelPane {
ui.label("Stroke:"); ui.label("Stroke:");
match info.stroke_color { match info.stroke_color {
Some(Some(color)) => { Some(Some(color)) => {
let mut egui_color = egui::Color32::from_rgba_unmultiplied( let mut rgba = [color.r, color.g, color.b, color.a];
color.r, color.g, color.b, color.a, if ui.color_edit_button_srgba_unmultiplied(&mut rgba).changed() {
); let new_color = ShapeColor::rgba(rgba[0], rgba[1], rgba[2], rgba[3]);
if egui::color_picker::color_edit_button_srgba(
ui,
&mut egui_color,
egui::color_picker::Alpha::OnlyBlend,
).changed() {
let new_color = ShapeColor {
r: egui_color.r(),
g: egui_color.g(),
b: egui_color.b(),
a: egui_color.a(),
};
let action = SetShapePropertiesAction::set_stroke_color( let action = SetShapePropertiesAction::set_stroke_color(
layer_id, time, edge_ids.clone(), Some(new_color), layer_id, time, edge_ids.clone(), Some(new_color),
); );
@ -538,14 +524,14 @@ impl InfopanelPane {
} }
}); });
// Background color // Background color (with alpha)
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Background:"); ui.label("Background:");
let bg = document.background_color; let bg = document.background_color;
let mut color = [bg.r, bg.g, bg.b]; let mut color = [bg.r, bg.g, bg.b, bg.a];
if ui.color_edit_button_srgb(&mut color).changed() { if ui.color_edit_button_srgba_unmultiplied(&mut color).changed() {
let action = SetDocumentPropertiesAction::set_background_color( let action = SetDocumentPropertiesAction::set_background_color(
ShapeColor::rgb(color[0], color[1], color[2]), ShapeColor::rgba(color[0], color[1], color[2], color[3]),
); );
shared.pending_actions.push(Box::new(action)); shared.pending_actions.push(Box::new(action));
} }

View File

@ -192,6 +192,8 @@ pub struct SharedPaneState<'a> {
pub brush_opacity: &'a mut f32, pub brush_opacity: &'a mut f32,
pub brush_hardness: &'a mut f32, pub brush_hardness: &'a mut f32,
pub brush_spacing: &'a mut f32, pub brush_spacing: &'a mut f32,
/// Whether the brush paints with the foreground (fill) color (true) or background (stroke) color (false)
pub brush_use_fg: &'a mut bool,
/// Audio engine controller for playback control (wrapped in Arc<Mutex<>> for thread safety) /// Audio engine controller for playback control (wrapped in Arc<Mutex<>> for thread safety)
pub audio_controller: Option<&'a std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>, pub audio_controller: Option<&'a std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
/// Video manager for video decoding and frame caching /// Video manager for video decoding and frame caching

View File

@ -4482,7 +4482,7 @@ impl StagePane {
let color = if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::Erase) { let color = if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::Erase) {
[1.0f32, 1.0, 1.0, 1.0] [1.0f32, 1.0, 1.0, 1.0]
} else { } else {
let c = *shared.stroke_color; let c = if *shared.brush_use_fg { *shared.stroke_color } else { *shared.fill_color };
[c.r() as f32 / 255.0, c.g() as f32 / 255.0, c.b() as f32 / 255.0, c.a() as f32 / 255.0] [c.r() as f32 / 255.0, c.g() as f32 / 255.0, c.b() as f32 / 255.0, c.a() as f32 / 255.0]
}; };

View File

@ -211,19 +211,49 @@ impl PaneRenderer for ToolbarPane {
let color_row_width = fill_label_width + color_button_size + button_spacing; let color_row_width = fill_label_width + color_button_size + button_spacing;
let color_x = rect.left() + (rect.width() - color_row_width) / 2.0; let color_x = rect.left() + (rect.width() - color_row_width) / 2.0;
// For raster layers show a single "Color" swatch (brush paint color = stroke_color). // Two color swatches:
// For vector layers show Fill + Stroke. // Stroke/FG always on top, Fill/BG always on bottom.
if !is_raster { // Raster layers label them "FG" / "BG"; vector layers label them "Stroke" / "Fill".
// Fill color label {
let stroke_label = if is_raster { "FG" } else { "Stroke" };
ui.painter().text( ui.painter().text(
egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0), egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0),
egui::Align2::CENTER_CENTER, egui::Align2::CENTER_CENTER,
"Fill", stroke_label,
egui::FontId::proportional(14.0),
egui::Color32::from_gray(200),
);
let stroke_button_rect = egui::Rect::from_min_size(
egui::pos2(color_x + fill_label_width + button_spacing, y),
egui::vec2(color_button_size, color_button_size),
);
let stroke_button_id = ui.id().with(("stroke_color_button", path));
let stroke_response = ui.interact(stroke_button_rect, stroke_button_id, egui::Sense::click());
draw_color_button(ui, stroke_button_rect, *shared.stroke_color);
egui::containers::Popup::from_toggle_button_response(&stroke_response)
.show(|ui| {
ui.spacing_mut().slider_width = 275.0;
let changed = egui::color_picker::color_picker_color32(ui, shared.stroke_color, egui::color_picker::Alpha::OnlyBlend);
if changed {
*shared.active_color_mode = super::ColorMode::Stroke;
}
});
y += color_button_size + button_spacing;
}
// Fill/BG color swatch
{
let fill_label = if is_raster { "BG" } else { "Fill" };
ui.painter().text(
egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0),
egui::Align2::CENTER_CENTER,
fill_label,
egui::FontId::proportional(14.0), egui::FontId::proportional(14.0),
egui::Color32::from_gray(200), egui::Color32::from_gray(200),
); );
// Fill color button
let fill_button_rect = egui::Rect::from_min_size( let fill_button_rect = egui::Rect::from_min_size(
egui::pos2(color_x + fill_label_width + button_spacing, y), egui::pos2(color_x + fill_label_width + button_spacing, y),
egui::vec2(color_button_size, color_button_size), egui::vec2(color_button_size, color_button_size),
@ -233,40 +263,13 @@ impl PaneRenderer for ToolbarPane {
draw_color_button(ui, fill_button_rect, *shared.fill_color); draw_color_button(ui, fill_button_rect, *shared.fill_color);
egui::containers::Popup::from_toggle_button_response(&fill_response) egui::containers::Popup::from_toggle_button_response(&fill_response)
.show(|ui| { .show(|ui| {
ui.spacing_mut().slider_width = 275.0;
let changed = egui::color_picker::color_picker_color32(ui, shared.fill_color, egui::color_picker::Alpha::OnlyBlend); let changed = egui::color_picker::color_picker_color32(ui, shared.fill_color, egui::color_picker::Alpha::OnlyBlend);
if changed { if changed {
*shared.active_color_mode = super::ColorMode::Fill; *shared.active_color_mode = super::ColorMode::Fill;
} }
}); });
y += color_button_size + button_spacing;
} }
// Stroke/brush color label
let stroke_label = if is_raster { "Color" } else { "Stroke" };
ui.painter().text(
egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0),
egui::Align2::CENTER_CENTER,
stroke_label,
egui::FontId::proportional(14.0),
egui::Color32::from_gray(200),
);
// Stroke color button
let stroke_button_rect = egui::Rect::from_min_size(
egui::pos2(color_x + fill_label_width + button_spacing, y),
egui::vec2(color_button_size, color_button_size),
);
let stroke_button_id = ui.id().with(("stroke_color_button", path));
let stroke_response = ui.interact(stroke_button_rect, stroke_button_id, egui::Sense::click());
draw_color_button(ui, stroke_button_rect, *shared.stroke_color);
egui::containers::Popup::from_toggle_button_response(&stroke_response)
.show(|ui| {
let changed = egui::color_picker::color_picker_color32(ui, shared.stroke_color, egui::color_picker::Alpha::OnlyBlend);
if changed {
*shared.active_color_mode = super::ColorMode::Stroke;
}
});
} // end color pickers } // end color pickers
} }