Refactor and enhance various components across the application
- Removed obsolete plans for stroke tuning, commit regression gate, and wxPlainSlider port. - Updated scrollable panel functionality to include optional scrollbar visibility. - Enhanced brush panel to filter brush styles by category for improved user experience. - Adjusted canvas rendering logic to prevent UI freezes during active drawing. - Modified stroke brush logic to allow single dab drawing at the current position when no movement occurs.
This commit is contained in:
@@ -40,6 +40,17 @@ pub fn scrollable_panel<R>(
|
||||
ui: &mut egui::Ui,
|
||||
id: &str,
|
||||
contents: impl FnOnce(&mut egui::Ui) -> R,
|
||||
) -> R {
|
||||
scrollable_panel_with_scrollbar(ui, id, false, contents)
|
||||
}
|
||||
|
||||
/// Wrap `contents` in a vertically scrollable panel with optional visible scrollbar,
|
||||
/// mouse-wheel support, and left-drag panning.
|
||||
pub fn scrollable_panel_with_scrollbar<R>(
|
||||
ui: &mut egui::Ui,
|
||||
id: &str,
|
||||
show_scrollbar: bool,
|
||||
contents: impl FnOnce(&mut egui::Ui) -> R,
|
||||
) -> R {
|
||||
let panel_id = ui.id().with(id);
|
||||
let state_id = panel_id.with("scroll_state_v2");
|
||||
@@ -71,11 +82,17 @@ pub fn scrollable_panel<R>(
|
||||
.layout(*ui.layout()),
|
||||
);
|
||||
|
||||
let scrollbar_visibility = if show_scrollbar {
|
||||
egui::scroll_area::ScrollBarVisibility::VisibleWhenNeeded
|
||||
} else {
|
||||
egui::scroll_area::ScrollBarVisibility::AlwaysHidden
|
||||
};
|
||||
|
||||
let scroll_response = egui::ScrollArea::vertical()
|
||||
.id_salt(panel_id)
|
||||
.drag_to_scroll(false)
|
||||
.vertical_scroll_offset(state.offset_y)
|
||||
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
|
||||
.scroll_bar_visibility(scrollbar_visibility)
|
||||
.max_height(visible_h)
|
||||
.max_width(visible_rect.width().max(1.0))
|
||||
.show(&mut child_ui, |ui| contents(ui));
|
||||
|
||||
@@ -29,6 +29,60 @@ fn matches_category(active_cat: &str, preset_cat: &str) -> bool {
|
||||
mapped.iter().any(|c| c.eq_ignore_ascii_case(preset_cat))
|
||||
}
|
||||
|
||||
/// Check if a brush style belongs to the given category for the Styles grid view.
|
||||
fn matches_style_category(active_cat: &str, style: hcie_engine_api::tools::BrushStyle) -> bool {
|
||||
if active_cat == "All" || active_cat == "Custom" || active_cat == "Imported" {
|
||||
return true;
|
||||
}
|
||||
let matches = match active_cat {
|
||||
"Drawing" => matches!(
|
||||
style,
|
||||
hcie_engine_api::tools::BrushStyle::Default
|
||||
| hcie_engine_api::tools::BrushStyle::Round
|
||||
| hcie_engine_api::tools::BrushStyle::HardRound
|
||||
| hcie_engine_api::tools::BrushStyle::SoftRound
|
||||
| hcie_engine_api::tools::BrushStyle::Square
|
||||
| hcie_engine_api::tools::BrushStyle::Pencil
|
||||
| hcie_engine_api::tools::BrushStyle::Pen
|
||||
| hcie_engine_api::tools::BrushStyle::InkPen
|
||||
| hcie_engine_api::tools::BrushStyle::Calligraphy
|
||||
| hcie_engine_api::tools::BrushStyle::Marker
|
||||
| hcie_engine_api::tools::BrushStyle::Sketch
|
||||
| hcie_engine_api::tools::BrushStyle::Hatch
|
||||
| hcie_engine_api::tools::BrushStyle::Star
|
||||
),
|
||||
"Painting" => matches!(
|
||||
style,
|
||||
hcie_engine_api::tools::BrushStyle::Oil
|
||||
| hcie_engine_api::tools::BrushStyle::Charcoal
|
||||
| hcie_engine_api::tools::BrushStyle::Watercolor
|
||||
| hcie_engine_api::tools::BrushStyle::Crayon
|
||||
| hcie_engine_api::tools::BrushStyle::WetPaint
|
||||
| hcie_engine_api::tools::BrushStyle::Airbrush
|
||||
| hcie_engine_api::tools::BrushStyle::Bristle
|
||||
| hcie_engine_api::tools::BrushStyle::Mixer
|
||||
| hcie_engine_api::tools::BrushStyle::Blender
|
||||
),
|
||||
"Effects" => matches!(
|
||||
style,
|
||||
hcie_engine_api::tools::BrushStyle::Noise
|
||||
| hcie_engine_api::tools::BrushStyle::Texture
|
||||
| hcie_engine_api::tools::BrushStyle::Spray
|
||||
| hcie_engine_api::tools::BrushStyle::Leaf
|
||||
| hcie_engine_api::tools::BrushStyle::Rock
|
||||
| hcie_engine_api::tools::BrushStyle::Meadow
|
||||
| hcie_engine_api::tools::BrushStyle::Wood
|
||||
| hcie_engine_api::tools::BrushStyle::Glow
|
||||
| hcie_engine_api::tools::BrushStyle::Clouds
|
||||
| hcie_engine_api::tools::BrushStyle::Dirt
|
||||
| hcie_engine_api::tools::BrushStyle::Tree
|
||||
| hcie_engine_api::tools::BrushStyle::Bitmap
|
||||
),
|
||||
_ => false,
|
||||
};
|
||||
matches
|
||||
}
|
||||
|
||||
fn preset_thumb_hash(preset: &BrushPreset) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
preset.style.hash(&mut hasher);
|
||||
@@ -1167,7 +1221,7 @@ pub fn show_brushes_ui(app: &mut HcieApp, ui: &mut egui::Ui) {
|
||||
state.primary_color[3],
|
||||
);
|
||||
|
||||
crate::app::shell::scrollable_panel::scrollable_panel(ui, "brush_preset_list", |ui| {
|
||||
crate::app::shell::scrollable_panel::scrollable_panel_with_scrollbar(ui, "brush_preset_list", true, |ui| {
|
||||
match view_mode {
|
||||
2 => {
|
||||
// CARD VIEW — Premium rounded cards with glow shadows and contrasting background
|
||||
@@ -1575,6 +1629,13 @@ pub fn show_brushes_ui(app: &mut HcieApp, ui: &mut egui::Ui) {
|
||||
let cell_w = 56.0_f32; // compact square cell
|
||||
let cell_h = 44.0_f32;
|
||||
|
||||
// Filter styles by active category
|
||||
let filtered_styles: Vec<_> = all_styles
|
||||
.iter()
|
||||
.filter(|&&(style, _)| matches_style_category(&active_cat, style))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Explicit row layout: compute how many cells fit in the current
|
||||
// panel width, then lay each row out as a separate horizontal so
|
||||
// reflow always follows the actual width on every frame.
|
||||
@@ -1586,12 +1647,12 @@ pub fn show_brushes_ui(app: &mut HcieApp, ui: &mut egui::Ui) {
|
||||
let cols = ((panel_w + spacing) / total_cell_w).floor().max(1.0) as usize;
|
||||
|
||||
let mut idx = 0;
|
||||
while idx < all_styles.len() {
|
||||
let end = (idx + cols).min(all_styles.len());
|
||||
while idx < filtered_styles.len() {
|
||||
let end = (idx + cols).min(filtered_styles.len());
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = spacing;
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
for &(style, name) in &all_styles[idx..end] {
|
||||
for &(style, name) in &filtered_styles[idx..end] {
|
||||
let is_active = current_style == style;
|
||||
|
||||
let (cell_rect, cell_resp) = ui.allocate_exact_size(
|
||||
|
||||
@@ -1434,11 +1434,11 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
if self.state.active_tool.shows_pen_tips() {
|
||||
let screen_x = canvas_rect.min.x + cx as f32 * zoom;
|
||||
let screen_y = canvas_rect.min.y + cy as f32 * zoom;
|
||||
let color = egui::Color32::from_rgba_unmultiplied(
|
||||
let cursor_color = egui::Color32::from_rgba_unmultiplied(
|
||||
self.state.primary_color[0],
|
||||
self.state.primary_color[1],
|
||||
self.state.primary_color[2],
|
||||
180,
|
||||
120,
|
||||
);
|
||||
let active_tip = self
|
||||
.state
|
||||
@@ -1447,69 +1447,134 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
.find(|p| p.id == self.state.active_brush_preset_id)
|
||||
.map(|p| &p.tip);
|
||||
if let Some(tip) = active_tip {
|
||||
if tip.style == BrushStyle::Bitmap
|
||||
&& !tip.bitmap_pixels.is_empty()
|
||||
&& tip.bitmap_w > 0
|
||||
&& tip.bitmap_h > 0
|
||||
{
|
||||
let bw = tip.bitmap_w as f32;
|
||||
let bh = tip.bitmap_h as f32;
|
||||
let stamp_size = self.state.active_size() * zoom;
|
||||
let scale = (stamp_size / bw).min(stamp_size / bh);
|
||||
let draw_w = bw * scale;
|
||||
let draw_h = bh * scale;
|
||||
let origin_x = screen_x - draw_w * 0.5;
|
||||
let origin_y = screen_y - draw_h * 0.5;
|
||||
let px_size = scale.max(1.0);
|
||||
let mut shapes = Vec::new();
|
||||
let mut py = 0.0;
|
||||
let mut sy = 0u32;
|
||||
while sy < tip.bitmap_h {
|
||||
let mut px = 0.0;
|
||||
let mut sx = 0u32;
|
||||
while sx < tip.bitmap_w {
|
||||
let mask =
|
||||
tip.bitmap_pixels[(sy * tip.bitmap_w + sx) as usize] as f32
|
||||
match tip.style {
|
||||
BrushStyle::Bitmap
|
||||
if !tip.bitmap_pixels.is_empty()
|
||||
&& tip.bitmap_w > 0
|
||||
&& tip.bitmap_h > 0 =>
|
||||
{
|
||||
let bw = tip.bitmap_w as f32;
|
||||
let bh = tip.bitmap_h as f32;
|
||||
let stamp_size = self.state.active_size() * zoom;
|
||||
let scale = (stamp_size / bw).min(stamp_size / bh);
|
||||
let draw_w = bw * scale;
|
||||
let draw_h = bh * scale;
|
||||
let origin_x = screen_x - draw_w * 0.5;
|
||||
let origin_y = screen_y - draw_h * 0.5;
|
||||
let px_size = scale.max(1.0);
|
||||
let mut shapes = Vec::new();
|
||||
let mut py = 0.0;
|
||||
let mut sy = 0u32;
|
||||
while sy < tip.bitmap_h {
|
||||
let mut px = 0.0;
|
||||
let mut sx = 0u32;
|
||||
while sx < tip.bitmap_w {
|
||||
let mask = tip.bitmap_pixels
|
||||
[(sy * tip.bitmap_w + sx) as usize]
|
||||
as f32
|
||||
/ 255.0;
|
||||
if mask > 0.01 {
|
||||
let a = (mask * 180.0).round() as u8;
|
||||
if mask > 0.01 {
|
||||
let a = (mask * 120.0).round() as u8;
|
||||
let c = egui::Color32::from_rgba_unmultiplied(
|
||||
cursor_color.r(),
|
||||
cursor_color.g(),
|
||||
cursor_color.b(),
|
||||
a,
|
||||
);
|
||||
let p = egui::pos2(origin_x + px, origin_y + py);
|
||||
shapes.push(egui::Shape::rect_filled(
|
||||
egui::Rect::from_min_size(
|
||||
p,
|
||||
egui::vec2(px_size, px_size),
|
||||
),
|
||||
0.0,
|
||||
c,
|
||||
));
|
||||
}
|
||||
px += px_size;
|
||||
sx += 1;
|
||||
}
|
||||
py += px_size;
|
||||
sy += 1;
|
||||
}
|
||||
painter.add(egui::Shape::Vec(shapes));
|
||||
}
|
||||
BrushStyle::Round
|
||||
| BrushStyle::HardRound
|
||||
| BrushStyle::SoftRound => {
|
||||
let radius = self.state.active_size() * zoom * 0.5;
|
||||
let hardness = tip.hardness;
|
||||
if hardness >= 0.95 {
|
||||
painter.circle_stroke(
|
||||
egui::pos2(screen_x, screen_y),
|
||||
radius,
|
||||
egui::Stroke::new(1.5, cursor_color),
|
||||
);
|
||||
} else {
|
||||
let softness = 1.0 - hardness;
|
||||
let layers = 5;
|
||||
for i in 0..layers {
|
||||
let frac = i as f32 / layers as f32;
|
||||
let r = radius * (1.0 + softness * 0.3 * (1.0 - frac));
|
||||
let a = (120.0 * (1.0 - frac * 0.6)) as u8;
|
||||
let c = egui::Color32::from_rgba_unmultiplied(
|
||||
color.r(),
|
||||
color.g(),
|
||||
color.b(),
|
||||
cursor_color.r(),
|
||||
cursor_color.g(),
|
||||
cursor_color.b(),
|
||||
a,
|
||||
);
|
||||
let p = egui::pos2(origin_x + px, origin_y + py);
|
||||
shapes.push(egui::Shape::rect_filled(
|
||||
egui::Rect::from_min_size(
|
||||
p,
|
||||
egui::vec2(px_size, px_size),
|
||||
),
|
||||
0.0,
|
||||
c,
|
||||
));
|
||||
painter.circle_stroke(
|
||||
egui::pos2(screen_x, screen_y),
|
||||
r,
|
||||
egui::Stroke::new(1.0, c),
|
||||
);
|
||||
}
|
||||
px += px_size;
|
||||
sx += 1;
|
||||
}
|
||||
py += px_size;
|
||||
sy += 1;
|
||||
}
|
||||
painter.add(egui::Shape::Vec(shapes));
|
||||
} else {
|
||||
let radius = self.state.active_size() * zoom * 0.5;
|
||||
painter.circle_stroke(
|
||||
egui::pos2(screen_x, screen_y),
|
||||
radius,
|
||||
egui::Stroke::new(1.5, color),
|
||||
);
|
||||
BrushStyle::Square => {
|
||||
let radius = self.state.active_size() * zoom * 0.5;
|
||||
let rect = egui::Rect::from_center_size(
|
||||
egui::pos2(screen_x, screen_y),
|
||||
egui::vec2(radius * 2.0, radius * 2.0),
|
||||
);
|
||||
painter.rect_stroke(
|
||||
rect,
|
||||
0.0,
|
||||
egui::Stroke::new(1.5, cursor_color),
|
||||
egui::StrokeKind::Outside,
|
||||
);
|
||||
}
|
||||
BrushStyle::Star | BrushStyle::Spray => {
|
||||
let radius = self.state.active_size() * zoom * 1.6;
|
||||
painter.circle_stroke(
|
||||
egui::pos2(screen_x, screen_y),
|
||||
radius,
|
||||
egui::Stroke::new(1.0, cursor_color),
|
||||
);
|
||||
}
|
||||
BrushStyle::Clouds | BrushStyle::Tree => {
|
||||
let radius = self.state.active_size() * zoom;
|
||||
painter.circle_stroke(
|
||||
egui::pos2(screen_x, screen_y),
|
||||
radius,
|
||||
egui::Stroke::new(1.0, cursor_color),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
let radius = self.state.active_size() * zoom * 0.5;
|
||||
painter.circle_stroke(
|
||||
egui::pos2(screen_x, screen_y),
|
||||
radius,
|
||||
egui::Stroke::new(1.5, cursor_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let radius = self.state.active_size() * zoom * 0.5;
|
||||
painter.circle_stroke(
|
||||
egui::pos2(screen_x, screen_y),
|
||||
radius,
|
||||
egui::Stroke::new(1.5, color),
|
||||
egui::Stroke::new(1.5, cursor_color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ pub fn extract_selection_edges(
|
||||
pub fn render_composition(
|
||||
ctx: &egui::Context,
|
||||
doc: &mut AppDocument,
|
||||
_state: &mut ToolState,
|
||||
state: &mut ToolState,
|
||||
_canvas_rect: egui::Rect,
|
||||
_zoom: f32,
|
||||
) -> bool {
|
||||
@@ -135,6 +135,12 @@ pub fn render_composition(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip expensive composite rendering during active drawing to prevent UI freezes.
|
||||
// The composite will be rendered on the next frame after the stroke ends.
|
||||
if state.is_drawing && doc.composite_texture.is_some() && has_buffer {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure buffer exists with correct size
|
||||
if !has_buffer {
|
||||
doc.composite_buffer = vec![0u8; full_size];
|
||||
|
||||
Reference in New Issue
Block a user