GOOD Refactor GUI components and add PlainSlider widget

- Added a new `PlainSlider` widget for keyboard-editable numeric sliders.
- Updated various panels to utilize the new `PlainSlider` for better user interaction.
- Removed unused code and dead code warnings across multiple files.
- Improved organization of modules by adding a `widgets` module.
- Cleaned up imports in several files to streamline the codebase.
- Added Qodana configuration file for code analysis and quality checks.
This commit is contained in:
2026-07-19 00:55:40 +03:00
parent bbabd2acdb
commit 9388e6f096
46 changed files with 1680 additions and 872 deletions
Generated
+45 -1
View File
@@ -69,6 +69,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "aliasable"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]] [[package]]
name = "aligned" name = "aligned"
version = "0.4.3" version = "0.4.3"
@@ -1333,7 +1339,7 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24"
dependencies = [ dependencies = [
"heck", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
] ]
@@ -2884,6 +2890,12 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@@ -3226,6 +3238,7 @@ dependencies = [
"iced_runtime", "iced_runtime",
"num-traits", "num-traits",
"once_cell", "once_cell",
"ouroboros",
"rustc-hash 2.1.2", "rustc-hash 2.1.2",
"thiserror 1.0.69", "thiserror 1.0.69",
"unicode-segmentation", "unicode-segmentation",
@@ -4614,6 +4627,30 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "ouroboros"
version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59"
dependencies = [
"aliasable",
"ouroboros_macro",
"static_assertions",
]
[[package]]
name = "ouroboros_macro"
version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn 2.0.118",
]
[[package]] [[package]]
name = "owned_ttf_parser" name = "owned_ttf_parser"
version = "0.25.1" version = "0.25.1"
@@ -4989,6 +5026,7 @@ dependencies = [
"quote", "quote",
"syn 2.0.118", "syn 2.0.118",
"version_check", "version_check",
"yansi",
] ]
[[package]] [[package]]
@@ -7951,6 +7989,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]] [[package]]
name = "yazi" name = "yazi"
version = "0.1.6" version = "0.1.6"
+1 -1
View File
@@ -62,7 +62,7 @@ eframe = { version = "0.34", default-features = false, features = ["default
egui_extras = { version = "0.34", features = ["svg", "image"] } egui_extras = { version = "0.34", features = ["svg", "image"] }
egui_dock = { version = "0.19", features = ["serde"] } egui_dock = { version = "0.19", features = ["serde"] }
rfd = "0.15" rfd = "0.15"
iced = { version = "0.13", features = ["tokio", "image", "svg", "canvas", "wgpu", "advanced"] } iced = { version = "0.13", features = ["tokio", "image", "svg", "canvas", "wgpu", "advanced", "lazy"] }
# Utilities # Utilities
base64 = "0.22" base64 = "0.22"
-1
View File
@@ -1,5 +1,4 @@
use hcie_blend::blend_pixels; use hcie_blend::blend_pixels;
use hcie_fx::protocol_to_hcie_fx_effect;
use hcie_protocol::Layer; use hcie_protocol::Layer;
use rayon::prelude::*; use rayon::prelude::*;
-1
View File
@@ -1,6 +1,5 @@
use hcie_blend::blend_pixels; use hcie_blend::blend_pixels;
use hcie_fx::apply_layer_effects; use hcie_fx::apply_layer_effects;
use hcie_fx::protocol_to_hcie_fx_effect;
use hcie_protocol::Layer; use hcie_protocol::Layer;
use hcie_tile::{TiledLayer, TILE_SIZE}; use hcie_tile::{TiledLayer, TILE_SIZE};
use rayon::prelude::*; use rayon::prelude::*;
@@ -630,12 +630,22 @@ impl<'a> TabViewer for HcieTabViewer<'a> {
// Pin / float toggle for docked utility panels only. // Pin / float toggle for docked utility panels only.
// LayerStyles, LayerDetails and Plugins are dialog-style panels: they // LayerStyles, LayerDetails and Plugins are dialog-style panels: they
// cannot float and do not show a pin toggle. // cannot float and do not show a pin toggle.
let supports_float = let supports_float = !tab.is_document()
!tab.is_document() && !matches!(tab, HciePane::Tools | HciePane::LayerStyles | HciePane::LayerDetails | HciePane::Plugins); && !matches!(
tab,
HciePane::Tools
| HciePane::LayerStyles
| HciePane::LayerDetails
| HciePane::Plugins
);
if supports_float { if supports_float {
// Pin icon is always visible on the right side of utility tabs so // Pin icon is always visible on the right side of utility tabs so
// users can float/re-dock a panel without first hovering the tab. // users can float/re-dock a panel without first hovering the tab.
let pin_offset = if is_closeable && response.hovered() { 24.0 } else { 8.0 }; let pin_offset = if is_closeable && response.hovered() {
24.0
} else {
8.0
};
let pin_center = let pin_center =
egui::pos2(response.rect.right() - pin_offset, response.rect.center().y); egui::pos2(response.rect.right() - pin_offset, response.rect.center().y);
let pin_rect = egui::Rect::from_center_size(pin_center, egui::vec2(14.0, 14.0)); let pin_rect = egui::Rect::from_center_size(pin_center, egui::vec2(14.0, 14.0));
@@ -750,7 +760,10 @@ impl<'a> TabViewer for HcieTabViewer<'a> {
!tab.is_document() !tab.is_document()
&& !matches!( && !matches!(
tab, tab,
HciePane::Tools | HciePane::LayerStyles | HciePane::LayerDetails | HciePane::Plugins HciePane::Tools
| HciePane::LayerStyles
| HciePane::LayerDetails
| HciePane::Plugins
) )
} }
@@ -837,7 +850,13 @@ impl<'a> TabViewer for HcieTabViewer<'a> {
// Float / close options intentionally disabled for LayerStyles, LayerDetails and Plugins // Float / close options intentionally disabled for LayerStyles, LayerDetails and Plugins
// because these panels render as popups / dialogs, not docked utility cards. // because these panels render as popups / dialogs, not docked utility cards.
let is_floating_allowed = !is_doc let is_floating_allowed = !is_doc
&& !matches!(tab, HciePane::Tools | HciePane::LayerStyles | HciePane::LayerDetails | HciePane::Plugins); && !matches!(
tab,
HciePane::Tools
| HciePane::LayerStyles
| HciePane::LayerDetails
| HciePane::Plugins
);
if is_floating_allowed { if is_floating_allowed {
let mut float = false; let mut float = false;
menu_item(ui, "Yeni Pencere", true, &mut float); menu_item(ui, "Yeni Pencere", true, &mut float);
@@ -863,7 +882,11 @@ impl<'a> TabViewer for HcieTabViewer<'a> {
let close_enabled = self.is_closeable(tab); let close_enabled = self.is_closeable(tab);
// Dialog-style panels (LayerStyles/LayerDetails/Plugins) are closed by the // Dialog-style panels (LayerStyles/LayerDetails/Plugins) are closed by the
// dialog itself, not by the dock tab. // dialog itself, not by the dock tab.
let close_in_context = close_enabled && !matches!(tab, HciePane::LayerStyles | HciePane::LayerDetails | HciePane::Plugins); let close_in_context = close_enabled
&& !matches!(
tab,
HciePane::LayerStyles | HciePane::LayerDetails | HciePane::Plugins
);
menu_item(ui, close_label, close_in_context, &mut close); menu_item(ui, close_label, close_in_context, &mut close);
if close && close_in_context { if close && close_in_context {
if let HciePane::Document(idx) = tab { if let HciePane::Document(idx) = tab {
@@ -11,9 +11,16 @@ pub use crate::app::document_state::{AppDocument, AppMode};
// Settings is named directly only by integration tests, but re-exported for API continuity. // Settings is named directly only by integration tests, but re-exported for API continuity.
#[allow(unused_imports)] #[allow(unused_imports)]
pub use crate::app::settings::{DockProfile, RecentFileEntry, Settings}; pub use crate::app::settings::{DockProfile, RecentFileEntry, Settings};
pub use crate::app::theme::{ThemeColors, ThemePreset, apply_theme, default_font_bytes, setup_custom_fonts}; pub use crate::app::theme::{
pub use crate::app::tool_state::{SelectionOp, SelectionTransform, TextEditState, ToolState, TransformHandle}; apply_theme, default_font_bytes, setup_custom_fonts, ThemeColors, ThemePreset,
pub use crate::app::utils::{build_save_dialog, composite_flood_fill, crop_colorimage, draw_premium_pin, format_color, format_save_error_hint, save_colorimage, suggest_alternative_path}; };
pub use crate::app::tool_state::{
SelectionOp, SelectionTransform, TextEditState, ToolState, TransformHandle,
};
pub use crate::app::utils::{
build_save_dialog, composite_flood_fill, crop_colorimage, draw_premium_pin, format_color,
format_save_error_hint, save_colorimage, suggest_alternative_path,
};
use crate::event_bus::{AppEvent, EventBus}; use crate::event_bus::{AppEvent, EventBus};
use crate::plugins; use crate::plugins;
use eframe::egui; use eframe::egui;
@@ -78,8 +85,6 @@ pub const TOOL_SLOTS: &[&[Tool]] = &[
], ],
]; ];
// ── HcieApp ────────────────────────────────────────────────────────────────── // ── HcieApp ──────────────────────────────────────────────────────────────────
pub struct HcieApp { pub struct HcieApp {
@@ -267,11 +272,8 @@ fn default_dock_state() -> egui_dock::DockState<dock::HciePane> {
// Default split fractions are chosen for a 1280px-wide window and are // Default split fractions are chosen for a 1280px-wide window and are
// immediately overridden by the layout solver using stored column widths. // immediately overridden by the layout solver using stored column widths.
// They only determine the tree structure, not final pixel sizes. // They only determine the tree structure, not final pixel sizes.
let [remaining, left_col2] = tree.split_left( let [remaining, left_col2] =
egui_dock::NodeIndex::root(), tree.split_left(egui_dock::NodeIndex::root(), 0.17, vec![HciePane::Brushes]);
0.17,
vec![HciePane::Brushes],
);
let [_left_brushes, _left_filters] = tree.split_below(left_col2, 0.5, vec![HciePane::Filters]); let [_left_brushes, _left_filters] = tree.split_below(left_col2, 0.5, vec![HciePane::Filters]);
let [center, right_col2] = tree.split_right(remaining, 0.83, vec![HciePane::Layers]); let [center, right_col2] = tree.split_right(remaining, 0.83, vec![HciePane::Layers]);
@@ -677,7 +679,10 @@ impl HcieApp {
log::info!("Added {} ABR brush preset(s) from {:?}", added, path); log::info!("Added {} ABR brush preset(s) from {:?}", added, path);
rfd::MessageDialog::new() rfd::MessageDialog::new()
.set_title("Brush Import") .set_title("Brush Import")
.set_description(&format!("Added {} brush preset(s) to the Imported category.", added)) .set_description(&format!(
"Added {} brush preset(s) to the Imported category.",
added
))
.set_level(rfd::MessageLevel::Info) .set_level(rfd::MessageLevel::Info)
.show(); .show();
} }
@@ -818,13 +823,11 @@ impl HcieApp {
self.dock_settle_repaints = self.dock_settle_repaints.max(5); self.dock_settle_repaints = self.dock_settle_repaints.max(5);
} }
/// Push a newly opened/created document tab into the center document leaf. /// Push a newly opened/created document tab into the center document leaf.
fn push_document_to_center_placeholder() {} fn push_document_to_center_placeholder() {}
/// Panic-safe tab removal: validates indices before calling egui_dock::remove_tab. /// Panic-safe tab removal: validates indices before calling egui_dock::remove_tab.
/// Panic-safe tab removal: validates indices before calling egui_dock::remove_tab. /// Panic-safe tab removal: validates indices before calling egui_dock::remove_tab.
/// Returns `None` and logs a warning if indices are stale. /// Returns `None` and logs a warning if indices are stale.
fn dock_safe_remove_tab( fn dock_safe_remove_tab(
@@ -1021,9 +1024,7 @@ impl HcieApp {
} }
self.active_doc = new_idx; self.active_doc = new_idx;
} else { } else {
log::trace!( log::trace!("Removing document tab from egui_dock multi-document state");
"Removing document tab from egui_dock multi-document state"
);
let tabs_to_remove = self.dock_find_tabs( let tabs_to_remove = self.dock_find_tabs(
|t| matches!(t, dock::HciePane::Document(d) if *d == idx), |t| matches!(t, dock::HciePane::Document(d) if *d == idx),
); );
@@ -1833,10 +1834,7 @@ impl HcieApp {
.into_iter() .into_iter()
.collect::<Vec<_>>() .collect::<Vec<_>>()
} else { } else {
crate::brush_import::import_abr_with_prefix( crate::brush_import::import_abr_with_prefix(&data, &file_stem)
&data,
&file_stem,
)
}; };
if imported.is_empty() { if imported.is_empty() {
rfd::MessageDialog::new() rfd::MessageDialog::new()
@@ -1850,12 +1848,21 @@ impl HcieApp {
} else { } else {
let added = imported.len(); let added = imported.len();
for preset in imported { for preset in imported {
if !self.state.brush_presets.iter().any(|p| p.id == preset.id) { if !self
.state
.brush_presets
.iter()
.any(|p| p.id == preset.id)
{
self.state.brush_presets.push(preset); self.state.brush_presets.push(preset);
} }
} }
self.event_bus.push(AppEvent::SaveSettings); self.event_bus.push(AppEvent::SaveSettings);
log::info!("Added {} imported brush preset(s) from {:?}", added, path); log::info!(
"Added {} imported brush preset(s) from {:?}",
added,
path
);
rfd::MessageDialog::new() rfd::MessageDialog::new()
.set_title("Brush Import") .set_title("Brush Import")
.set_description(&format!( .set_description(&format!(
@@ -5118,7 +5125,9 @@ impl eframe::App for HcieApp {
.state .state
.brush_presets .brush_presets
.iter() .iter()
.filter(|p| p.category == "Custom" || p.category == "Imported" || p.category == "Imported (ABR)") .filter(|p| {
p.category == "Custom" || p.category == "Imported" || p.category == "Imported (ABR)"
})
.cloned() .cloned()
.collect(); .collect();
if let Ok(json) = serde_json::to_string(&persisted_presets) { if let Ok(json) = serde_json::to_string(&persisted_presets) {
@@ -5139,4 +5148,3 @@ impl eframe::App for HcieApp {
std::time::Duration::from_secs(120) std::time::Duration::from_secs(120)
} }
} }
@@ -218,26 +218,21 @@ pub fn apply_theme(ctx: &egui::Context, theme: ThemePreset) {
}; };
// ── Font sizes matching Qt 12px default ── // ── Font sizes matching Qt 12px default ──
style.text_styles.insert( style
egui::TextStyle::Body, .text_styles
egui::FontId::proportional(12.0), .insert(egui::TextStyle::Body, egui::FontId::proportional(12.0));
); style
style.text_styles.insert( .text_styles
egui::TextStyle::Button, .insert(egui::TextStyle::Button, egui::FontId::proportional(12.0));
egui::FontId::proportional(12.0), style
); .text_styles
style.text_styles.insert( .insert(egui::TextStyle::Small, egui::FontId::proportional(11.0));
egui::TextStyle::Small, style
egui::FontId::proportional(11.0), .text_styles
); .insert(egui::TextStyle::Heading, egui::FontId::proportional(13.5));
style.text_styles.insert( style
egui::TextStyle::Heading, .text_styles
egui::FontId::proportional(13.5), .insert(egui::TextStyle::Monospace, egui::FontId::proportional(11.5));
);
style.text_styles.insert(
egui::TextStyle::Monospace,
egui::FontId::proportional(11.5),
);
// ── Harmonized layout parameters (8px grid) ── // ── Harmonized layout parameters (8px grid) ──
style.spacing.item_spacing = egui::vec2(4.0, 4.0); style.spacing.item_spacing = egui::vec2(4.0, 4.0);
File diff suppressed because it is too large Load Diff
@@ -102,6 +102,7 @@ pub fn show_layer_styles_panel(doc: &mut AppDocument, state: &mut ToolState, ui:
size: 5.0, size: 5.0,
angle: 120.0, angle: 120.0,
altitude: 30.0, altitude: 30.0,
contour: "Linear".to_string(),
highlight_opacity: 0.75, highlight_opacity: 0.75,
shadow_opacity: 0.75, shadow_opacity: 0.75,
direction: "Up".to_string(), direction: "Up".to_string(),
@@ -352,6 +353,49 @@ fn technique_combobox(ui: &mut egui::Ui, current: &mut String) -> bool {
changed changed
} }
/// **Purpose:**
/// Displays a dropdown combo box to select the bevel contour profile.
///
/// **Logic & Workflow:**
/// Lists common contour presets (Linear, Cone, Gaussian, etc.).
///
/// **Arguments:**
/// - `ui`: Mutable egui UI layout context.
/// - `current`: Mutable reference to the contour string.
///
/// **Returns:**
/// - `bool`: True if selection changed.
fn contour_combobox(ui: &mut egui::Ui, current: &mut String) -> bool {
let mut changed = false;
let contours = [
"Linear",
"Cone",
"Cone-Inverted",
"Gaussian",
"Half Round",
"Round",
"Ring",
"Ring-Double",
"Sawtooth",
"Square",
"Valley",
"Shallow-Slope",
"Wave",
"Cove",
"Washboard",
];
egui::ComboBox::from_label("Contour")
.selected_text(current.as_str())
.show_ui(ui, |ui| {
for c in contours {
if ui.selectable_value(current, c.to_string(), c).changed() {
changed = true;
}
}
});
changed
}
/// **Purpose:** /// **Purpose:**
/// Displays a dropdown combo box to select the stroke position. /// Displays a dropdown combo box to select the stroke position.
/// ///
@@ -476,7 +520,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
false, false,
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
if ui.checkbox(enabled, "").changed() { if ui.checkbox(enabled, String::new()).changed() {
changed = true; changed = true;
} }
ui.label(RichText::new(title).strong().color(header_color)); ui.label(RichText::new(title).strong().color(header_color));
@@ -527,7 +571,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
false, false,
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
if ui.checkbox(enabled, "").changed() { if ui.checkbox(enabled, String::new()).changed() {
changed = true; changed = true;
} }
ui.label(RichText::new(title).strong().color(header_color)); ui.label(RichText::new(title).strong().color(header_color));
@@ -576,7 +620,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
false, false,
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
if ui.checkbox(enabled, "").changed() { if ui.checkbox(enabled, String::new()).changed() {
changed = true; changed = true;
} }
ui.label(RichText::new(title).strong().color(header_color)); ui.label(RichText::new(title).strong().color(header_color));
@@ -619,7 +663,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
false, false,
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
if ui.checkbox(enabled, "").changed() { if ui.checkbox(enabled, String::new()).changed() {
changed = true; changed = true;
} }
ui.label(RichText::new(title).strong().color(header_color)); ui.label(RichText::new(title).strong().color(header_color));
@@ -662,6 +706,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
highlight_color, highlight_color,
shadow_blend_mode, shadow_blend_mode,
shadow_color, shadow_color,
contour,
} => { } => {
let title = "Bevel & Emboss"; let title = "Bevel & Emboss";
let header_color = Color32::from_rgb(100, 180, 220); let header_color = Color32::from_rgb(100, 180, 220);
@@ -671,7 +716,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
false, false,
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
if ui.checkbox(enabled, "").changed() { if ui.checkbox(enabled, String::new()).changed() {
changed = true; changed = true;
} }
ui.label(RichText::new(title).strong().color(header_color)); ui.label(RichText::new(title).strong().color(header_color));
@@ -718,6 +763,18 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
}); });
ui.end_row(); ui.end_row();
ui.label(
RichText::new("Contour")
.size(10.0)
.color(colors.text_secondary),
);
ui.horizontal(|ui| {
if contour_combobox(ui, contour) {
changed = true;
}
});
ui.end_row();
ui.label( ui.label(
RichText::new("Depth") RichText::new("Depth")
.size(10.0) .size(10.0)
@@ -898,7 +955,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
false, false,
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
if ui.checkbox(enabled, "").changed() { if ui.checkbox(enabled, String::new()).changed() {
changed = true; changed = true;
} }
ui.label(RichText::new(title).strong().color(header_color)); ui.label(RichText::new(title).strong().color(header_color));
@@ -942,7 +999,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
false, false,
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
if ui.checkbox(enabled, "").changed() { if ui.checkbox(enabled, String::new()).changed() {
changed = true; changed = true;
} }
ui.label(RichText::new(title).strong().color(header_color)); ui.label(RichText::new(title).strong().color(header_color));
@@ -979,7 +1036,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
false, false,
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
if ui.checkbox(enabled, "").changed() { if ui.checkbox(enabled, String::new()).changed() {
changed = true; changed = true;
} }
ui.label(RichText::new(title).strong().color(header_color)); ui.label(RichText::new(title).strong().color(header_color));
@@ -1021,7 +1078,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
false, false,
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
if ui.checkbox(enabled, "").changed() { if ui.checkbox(enabled, String::new()).changed() {
changed = true; changed = true;
} }
ui.label(RichText::new(title).strong().color(header_color)); ui.label(RichText::new(title).strong().color(header_color));
@@ -1059,7 +1116,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
false, false,
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
if ui.checkbox(enabled, "").changed() { if ui.checkbox(enabled, String::new()).changed() {
changed = true; changed = true;
} }
ui.label(RichText::new(title).strong().color(header_color)); ui.label(RichText::new(title).strong().color(header_color));
@@ -149,7 +149,11 @@ pub fn suggest_alternative_path(original: &std::path::Path) -> std::path::PathBu
/// * `default_format` - Lowercase extension to pre-select (`kra`, `hcie`, `png`, ...). /// * `default_format` - Lowercase extension to pre-select (`kra`, `hcie`, `png`, ...).
/// * `file_stem` - Base name for the suggested file. /// * `file_stem` - Base name for the suggested file.
/// * `is_export` - When `true`, appends `_export` to the file stem. /// * `is_export` - When `true`, appends `_export` to the file stem.
pub fn build_save_dialog(default_format: &str, file_stem: &str, is_export: bool) -> rfd::FileDialog { pub fn build_save_dialog(
default_format: &str,
file_stem: &str,
is_export: bool,
) -> rfd::FileDialog {
let suffix = if is_export { "_export" } else { "" }; let suffix = if is_export { "_export" } else { "" };
let file_name = format!("{}{}.{}", file_stem, suffix, default_format); let file_name = format!("{}{}.{}", file_stem, suffix, default_format);
@@ -252,7 +252,8 @@ fn harvest_brs2_sampled_brushes(data: &[u8], base_pos: usize) -> Vec<BrushPreset
} else { } else {
(w, h) (w, h)
}; };
if let Some(decoded) = try_decode_and_scale(slice, w as usize, h as usize, final_w, final_h) { if let Some(decoded) = try_decode_and_scale(slice, w as usize, h as usize, final_w, final_h)
{
presets.push(BrushPreset { presets.push(BrushPreset {
id: format!("abr_brs2_{}_{}x{}", base_pos + i, final_w, final_h), id: format!("abr_brs2_{}_{}x{}", base_pos + i, final_w, final_h),
name: format!("Brush {}x{}", final_w, final_h), name: format!("Brush {}x{}", final_w, final_h),
@@ -441,7 +442,8 @@ fn extract_desc_block_names(data: &[u8]) -> Vec<String> {
if p + 4 > block.len() { if p + 4 > block.len() {
break; break;
} }
let nchars = u32::from_be_bytes([block[p], block[p + 1], block[p + 2], block[p + 3]]) as usize; let nchars =
u32::from_be_bytes([block[p], block[p + 1], block[p + 2], block[p + 3]]) as usize;
let text_start = p + 4; let text_start = p + 4;
if nchars == 0 || text_start + nchars * 2 > block.len() { if nchars == 0 || text_start + nchars * 2 > block.len() {
idx += 1; idx += 1;
@@ -555,7 +557,12 @@ pub fn import_kpp(data: &[u8], name_hint: &str) -> Option<BrushPreset> {
} }
} }
Some(BrushPreset { Some(BrushPreset {
id: format!("kpp_imported_{}_{}x{}", name_hint.replace(' ', "_"), final_w, final_h), id: format!(
"kpp_imported_{}_{}x{}",
name_hint.replace(' ', "_"),
final_w,
final_h
),
name: format!("{} (KPP)", name_hint), name: format!("{} (KPP)", name_hint),
category: "Imported".to_string(), category: "Imported".to_string(),
tip: BrushTip { tip: BrushTip {
@@ -623,7 +630,10 @@ mod tests {
} }
}; };
let presets = import_abr(&data); let presets = import_abr(&data);
let bitmap: Vec<_> = presets.iter().filter(|p| p.style == BrushStyle::Bitmap).collect(); let bitmap: Vec<_> = presets
.iter()
.filter(|p| p.style == BrushStyle::Bitmap)
.collect();
assert!(!bitmap.is_empty(), "Should have bitmap presets"); assert!(!bitmap.is_empty(), "Should have bitmap presets");
let has_names = bitmap.iter().any(|p| !p.name.starts_with("Brush ")); let has_names = bitmap.iter().any(|p| !p.name.starts_with("Brush "));
assert!(has_names, "Bitmap presets should have descriptive names"); assert!(has_names, "Bitmap presets should have descriptive names");
@@ -1098,7 +1098,10 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
let t_stroke_end = std::time::Instant::now(); let t_stroke_end = std::time::Instant::now();
let layer_id = self.doc.engine.active_layer_id(); let layer_id = self.doc.engine.active_layer_id();
self.doc.engine.end_stroke(layer_id); self.doc.engine.end_stroke(layer_id);
log::warn!("[PERF] self.doc.engine.end_stroke took {}ms", t_stroke_end.elapsed().as_millis()); log::warn!(
"[PERF] self.doc.engine.end_stroke took {}ms",
t_stroke_end.elapsed().as_millis()
);
self.doc.last_stroke_end_time = Some(std::time::Instant::now()); self.doc.last_stroke_end_time = Some(std::time::Instant::now());
let desc = match self.state.active_tool { let desc = match self.state.active_tool {
@@ -1474,40 +1477,108 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
} }
Tool::Brush | _ => { Tool::Brush | _ => {
tip.style = match self.state.tool_configs.brush.style { tip.style = match self.state.tool_configs.brush.style {
hcie_engine_api::tools::BrushStyle::Default => hcie_engine_api::BrushStyle::Round, hcie_engine_api::tools::BrushStyle::Default => {
hcie_engine_api::tools::BrushStyle::Round => hcie_engine_api::BrushStyle::Round, hcie_engine_api::BrushStyle::Round
hcie_engine_api::tools::BrushStyle::Square => hcie_engine_api::BrushStyle::Square, }
hcie_engine_api::tools::BrushStyle::HardRound => hcie_engine_api::BrushStyle::HardRound, hcie_engine_api::tools::BrushStyle::Round => {
hcie_engine_api::tools::BrushStyle::SoftRound => hcie_engine_api::BrushStyle::SoftRound, hcie_engine_api::BrushStyle::Round
hcie_engine_api::tools::BrushStyle::Noise => hcie_engine_api::BrushStyle::Noise, }
hcie_engine_api::tools::BrushStyle::Texture => hcie_engine_api::BrushStyle::Texture, hcie_engine_api::tools::BrushStyle::Square => {
hcie_engine_api::tools::BrushStyle::Spray => hcie_engine_api::BrushStyle::Spray, hcie_engine_api::BrushStyle::Square
hcie_engine_api::tools::BrushStyle::Pen => hcie_engine_api::BrushStyle::Pen, }
hcie_engine_api::tools::BrushStyle::Oil => hcie_engine_api::BrushStyle::Oil, hcie_engine_api::tools::BrushStyle::HardRound => {
hcie_engine_api::tools::BrushStyle::Charcoal => hcie_engine_api::BrushStyle::Charcoal, hcie_engine_api::BrushStyle::HardRound
hcie_engine_api::tools::BrushStyle::Leaf => hcie_engine_api::BrushStyle::Leaf, }
hcie_engine_api::tools::BrushStyle::Rock => hcie_engine_api::BrushStyle::Rock, hcie_engine_api::tools::BrushStyle::SoftRound => {
hcie_engine_api::tools::BrushStyle::Meadow => hcie_engine_api::BrushStyle::Meadow, hcie_engine_api::BrushStyle::SoftRound
hcie_engine_api::tools::BrushStyle::Wood => hcie_engine_api::BrushStyle::Wood, }
hcie_engine_api::tools::BrushStyle::Watercolor => hcie_engine_api::BrushStyle::Watercolor, hcie_engine_api::tools::BrushStyle::Noise => {
hcie_engine_api::tools::BrushStyle::Calligraphy => hcie_engine_api::BrushStyle::Calligraphy, hcie_engine_api::BrushStyle::Noise
hcie_engine_api::tools::BrushStyle::Marker => hcie_engine_api::BrushStyle::Marker, }
hcie_engine_api::tools::BrushStyle::Sketch => hcie_engine_api::BrushStyle::Sketch, hcie_engine_api::tools::BrushStyle::Texture => {
hcie_engine_api::tools::BrushStyle::Hatch => hcie_engine_api::BrushStyle::Hatch, hcie_engine_api::BrushStyle::Texture
hcie_engine_api::tools::BrushStyle::Star => hcie_engine_api::BrushStyle::Star, }
hcie_engine_api::tools::BrushStyle::Glow => hcie_engine_api::BrushStyle::Glow, hcie_engine_api::tools::BrushStyle::Spray => {
hcie_engine_api::tools::BrushStyle::Airbrush => hcie_engine_api::BrushStyle::Airbrush, hcie_engine_api::BrushStyle::Spray
hcie_engine_api::tools::BrushStyle::Pencil => hcie_engine_api::BrushStyle::Pencil, }
hcie_engine_api::tools::BrushStyle::Crayon => hcie_engine_api::BrushStyle::Crayon, hcie_engine_api::tools::BrushStyle::Pen => {
hcie_engine_api::tools::BrushStyle::WetPaint => hcie_engine_api::BrushStyle::WetPaint, hcie_engine_api::BrushStyle::Pen
hcie_engine_api::tools::BrushStyle::InkPen => hcie_engine_api::BrushStyle::InkPen, }
hcie_engine_api::tools::BrushStyle::Clouds => hcie_engine_api::BrushStyle::Clouds, hcie_engine_api::tools::BrushStyle::Oil => {
hcie_engine_api::tools::BrushStyle::Dirt => hcie_engine_api::BrushStyle::Dirt, hcie_engine_api::BrushStyle::Oil
hcie_engine_api::tools::BrushStyle::Tree => hcie_engine_api::BrushStyle::Tree, }
hcie_engine_api::tools::BrushStyle::Bristle => hcie_engine_api::BrushStyle::Bristle, hcie_engine_api::tools::BrushStyle::Charcoal => {
hcie_engine_api::tools::BrushStyle::Mixer => hcie_engine_api::BrushStyle::Mixer, hcie_engine_api::BrushStyle::Charcoal
hcie_engine_api::tools::BrushStyle::Blender => hcie_engine_api::BrushStyle::Blender, }
hcie_engine_api::tools::BrushStyle::Bitmap => hcie_engine_api::BrushStyle::Bitmap, hcie_engine_api::tools::BrushStyle::Leaf => {
hcie_engine_api::BrushStyle::Leaf
}
hcie_engine_api::tools::BrushStyle::Rock => {
hcie_engine_api::BrushStyle::Rock
}
hcie_engine_api::tools::BrushStyle::Meadow => {
hcie_engine_api::BrushStyle::Meadow
}
hcie_engine_api::tools::BrushStyle::Wood => {
hcie_engine_api::BrushStyle::Wood
}
hcie_engine_api::tools::BrushStyle::Watercolor => {
hcie_engine_api::BrushStyle::Watercolor
}
hcie_engine_api::tools::BrushStyle::Calligraphy => {
hcie_engine_api::BrushStyle::Calligraphy
}
hcie_engine_api::tools::BrushStyle::Marker => {
hcie_engine_api::BrushStyle::Marker
}
hcie_engine_api::tools::BrushStyle::Sketch => {
hcie_engine_api::BrushStyle::Sketch
}
hcie_engine_api::tools::BrushStyle::Hatch => {
hcie_engine_api::BrushStyle::Hatch
}
hcie_engine_api::tools::BrushStyle::Star => {
hcie_engine_api::BrushStyle::Star
}
hcie_engine_api::tools::BrushStyle::Glow => {
hcie_engine_api::BrushStyle::Glow
}
hcie_engine_api::tools::BrushStyle::Airbrush => {
hcie_engine_api::BrushStyle::Airbrush
}
hcie_engine_api::tools::BrushStyle::Pencil => {
hcie_engine_api::BrushStyle::Pencil
}
hcie_engine_api::tools::BrushStyle::Crayon => {
hcie_engine_api::BrushStyle::Crayon
}
hcie_engine_api::tools::BrushStyle::WetPaint => {
hcie_engine_api::BrushStyle::WetPaint
}
hcie_engine_api::tools::BrushStyle::InkPen => {
hcie_engine_api::BrushStyle::InkPen
}
hcie_engine_api::tools::BrushStyle::Clouds => {
hcie_engine_api::BrushStyle::Clouds
}
hcie_engine_api::tools::BrushStyle::Dirt => {
hcie_engine_api::BrushStyle::Dirt
}
hcie_engine_api::tools::BrushStyle::Tree => {
hcie_engine_api::BrushStyle::Tree
}
hcie_engine_api::tools::BrushStyle::Bristle => {
hcie_engine_api::BrushStyle::Bristle
}
hcie_engine_api::tools::BrushStyle::Mixer => {
hcie_engine_api::BrushStyle::Mixer
}
hcie_engine_api::tools::BrushStyle::Blender => {
hcie_engine_api::BrushStyle::Blender
}
hcie_engine_api::tools::BrushStyle::Bitmap => {
hcie_engine_api::BrushStyle::Bitmap
}
}; };
tip.size = self.state.tool_configs.brush.size; tip.size = self.state.tool_configs.brush.size;
tip.opacity = self.state.tool_configs.brush.opacity; tip.opacity = self.state.tool_configs.brush.opacity;
@@ -1600,25 +1671,20 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
}); });
} }
if let Some(tex) = ui if let Some(tex) =
.data(|d| d.get_temp::<egui::TextureHandle>(ui.id().with("cursor_stamp_tex"))) ui.data(|d| d.get_temp::<egui::TextureHandle>(ui.id().with("cursor_stamp_tex")))
{ {
// Display the stamp preview at the correct diameter. // Display the stamp preview at the correct diameter.
// The texture was rendered at tex_size × tex_size pixels; // The texture was rendered at tex_size × tex_size pixels;
// display it at diameter_px × diameter_px screen pixels so // display it at diameter_px × diameter_px screen pixels so
// the preview matches the actual brush footprint. // the preview matches the actual brush footprint.
let display_dim = egui::vec2(diameter_px, diameter_px); let display_dim = egui::vec2(diameter_px, diameter_px);
let rect = egui::Rect::from_center_size( let rect =
egui::pos2(screen_x, screen_y), egui::Rect::from_center_size(egui::pos2(screen_x, screen_y), display_dim);
display_dim,
);
painter.add(egui::Shape::image( painter.add(egui::Shape::image(
tex.id(), tex.id(),
rect, rect,
egui::Rect::from_min_max( egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
egui::pos2(0.0, 0.0),
egui::pos2(1.0, 1.0),
),
egui::Color32::WHITE, egui::Color32::WHITE,
)); ));
@@ -2551,5 +2617,3 @@ fn find_text_layer_at(engine: &hcie_engine_api::Engine, x: f32, y: f32) -> Optio
} }
None None
} }
@@ -203,11 +203,14 @@ pub fn render_composition(
let src_start = ((y * rw + x0) as usize) * 4; let src_start = ((y * rw + x0) as usize) * 4;
let dst_start = ((y - y0) as usize * region_w) * 4; let dst_start = ((y - y0) as usize * region_w) * 4;
let row_bytes = region_w * 4; let row_bytes = region_w * 4;
region_pixels[dst_start..dst_start + row_bytes] region_pixels[dst_start..dst_start + row_bytes].copy_from_slice(
.copy_from_slice(&doc.composite_buffer[src_start..src_start + row_bytes]); &doc.composite_buffer[src_start..src_start + row_bytes],
);
} }
let region_image = let region_image = egui::ColorImage::from_rgba_unmultiplied(
egui::ColorImage::from_rgba_unmultiplied([region_w, region_h], &region_pixels); [region_w, region_h],
&region_pixels,
);
tex.set_partial([x0 as usize, y0 as usize], region_image, options); tex.set_partial([x0 as usize, y0 as usize], region_image, options);
} }
} else { } else {
+2
View File
@@ -1530,9 +1530,11 @@ impl Engine {
} }
/// Get mutable reference to shapes on a vector layer for direct manipulation. /// Get mutable reference to shapes on a vector layer for direct manipulation.
/// Marks the layer dirty so the composite is regenerated on the next render pass.
pub fn get_layer_shapes_direct(&mut self, layer_id: u64) -> Option<&mut Vec<VectorShape>> { pub fn get_layer_shapes_direct(&mut self, layer_id: u64) -> Option<&mut Vec<VectorShape>> {
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) { if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
if let LayerData::Vector { ref mut shapes } = layer.data { if let LayerData::Vector { ref mut shapes } = layer.data {
layer.dirty = true;
return Some(shapes); return Some(shapes);
} }
} }
@@ -92,7 +92,7 @@ pub enum AiProvider {
impl AiProvider { impl AiProvider {
/// Return the default base URL for this provider. /// Return the default base URL for this provider.
pub fn default_url(&self) -> &str { pub fn _default_url(&self) -> &str {
match self { match self {
AiProvider::Ollama => "http://localhost:11434", AiProvider::Ollama => "http://localhost:11434",
AiProvider::Claude => "https://api.anthropic.com", AiProvider::Claude => "https://api.anthropic.com",
@@ -101,7 +101,7 @@ impl AiProvider {
} }
/// Return the display name. /// Return the display name.
pub fn display_name(&self) -> &str { pub fn _display_name(&self) -> &str {
match self { match self {
AiProvider::Ollama => "Ollama", AiProvider::Ollama => "Ollama",
AiProvider::Claude => "Claude", AiProvider::Claude => "Claude",
@@ -120,7 +120,7 @@ pub struct AiChatConfig {
/// Model name to use for chat completions. /// Model name to use for chat completions.
pub model: String, pub model: String,
/// Maximum conversation turns before auto-truncation. /// Maximum conversation turns before auto-truncation.
pub max_turns: u32, pub _max_turns: u32,
/// Whether to display reasoning/thinking content from the LLM. /// Whether to display reasoning/thinking content from the LLM.
pub reasoning_enabled: bool, pub reasoning_enabled: bool,
/// Whether to send canvas context (dimensions, layers) with each request. /// Whether to send canvas context (dimensions, layers) with each request.
@@ -133,7 +133,7 @@ impl Default for AiChatConfig {
provider: AiProvider::Ollama, provider: AiProvider::Ollama,
base_url: "http://localhost:11434".to_string(), base_url: "http://localhost:11434".to_string(),
model: "qwen2.5:latest".to_string(), model: "qwen2.5:latest".to_string(),
max_turns: 20, _max_turns: 20,
reasoning_enabled: false, reasoning_enabled: false,
canvas_context: true, canvas_context: true,
} }
@@ -156,7 +156,7 @@ pub struct AiChatState {
/// Current reasoning/thinking text being streamed. /// Current reasoning/thinking text being streamed.
pub current_reasoning: String, pub current_reasoning: String,
/// Available models fetched from the endpoint. /// Available models fetched from the endpoint.
pub available_models: Vec<String>, pub _available_models: Vec<String>,
} }
impl Default for AiChatState { impl Default for AiChatState {
@@ -168,7 +168,7 @@ impl Default for AiChatState {
is_streaming: false, is_streaming: false,
current_response: String::new(), current_response: String::new(),
current_reasoning: String::new(), current_reasoning: String::new(),
available_models: Vec::new(), _available_models: Vec::new(),
} }
} }
} }
+55 -19
View File
@@ -152,10 +152,10 @@ pub struct HcieIcedApp {
/// Pending automated or keyboard-triggered full-viewport screenshot. /// Pending automated or keyboard-triggered full-viewport screenshot.
pub screenshot_request: Option<crate::cli::ScreenshotRequest>, pub screenshot_request: Option<crate::cli::ScreenshotRequest>,
/// New Image dialog state. /// New Image dialog state.
pub dialog_input: String, pub _dialog_input: String,
pub dialog_input2: String, pub _dialog_input2: String,
pub show_performance: bool, pub _show_performance: bool,
pub ui_scale: f32, pub _ui_scale: f32,
pub modifiers: iced::keyboard::Modifiers, pub modifiers: iced::keyboard::Modifiers,
pub dialog_new_name: String, pub dialog_new_name: String,
pub dialog_new_width: u32, pub dialog_new_width: u32,
@@ -280,7 +280,7 @@ pub struct HcieIcedApp {
/// Whether the dock profile dialog is visible. /// Whether the dock profile dialog is visible.
pub show_dock_profile_dialog: bool, pub show_dock_profile_dialog: bool,
/// Current UI language for internationalization. /// Current UI language for internationalization.
pub language: i18n::Language, pub _language: i18n::Language,
/// Context menu screen position (x, y). None means closed. /// Context menu screen position (x, y). None means closed.
pub canvas_context_menu: Option<(f32, f32)>, pub canvas_context_menu: Option<(f32, f32)>,
/// Dirty flag set whenever fg/bg/recent colors change, so colors can be /// Dirty flag set whenever fg/bg/recent colors change, so colors can be
@@ -1356,10 +1356,10 @@ impl HcieIcedApp {
sub_tools_open: false, sub_tools_open: false,
sidebar_expanded: settings.panel_layout.sidebar_expanded, sidebar_expanded: settings.panel_layout.sidebar_expanded,
slot_last_used: vec![0; 20], slot_last_used: vec![0; 20],
dialog_input: String::new(), _dialog_input: String::new(),
dialog_input2: String::new(), _dialog_input2: String::new(),
show_performance: false, _show_performance: false,
ui_scale: 1.0, _ui_scale: 1.0,
modifiers: iced::keyboard::Modifiers::default(), modifiers: iced::keyboard::Modifiers::default(),
canvas_context_menu: None, canvas_context_menu: None,
settings: settings.clone(), settings: settings.clone(),
@@ -1434,7 +1434,7 @@ impl HcieIcedApp {
dock_profile_rename_idx: None, dock_profile_rename_idx: None,
dock_profile_rename_buf: String::new(), dock_profile_rename_buf: String::new(),
show_dock_profile_dialog: false, show_dock_profile_dialog: false,
language: i18n::Language::default(), _language: i18n::Language::default(),
colors_dirty: false, colors_dirty: false,
vector_shapes_snapshot: None, vector_shapes_snapshot: None,
vector_drag_last: None, vector_drag_last: None,
@@ -1582,7 +1582,7 @@ impl HcieIcedApp {
/// ///
/// The canvas image is centered in the pane, with `pan_offset` as an /// The canvas image is centered in the pane, with `pan_offset` as an
/// additional offset from center. This function accounts for both. /// additional offset from center. This function accounts for both.
fn screen_to_canvas(&self, sx: f32, sy: f32) -> Option<(f32, f32)> { fn _screen_to_canvas(&self, sx: f32, sy: f32) -> Option<(f32, f32)> {
let doc = &self.documents[self.active_doc]; let doc = &self.documents[self.active_doc];
let zoom = doc.zoom; let zoom = doc.zoom;
let (pane_w, pane_h) = doc.pane_size; let (pane_w, pane_h) = doc.pane_size;
@@ -1823,7 +1823,7 @@ impl HcieIcedApp {
// structure is no longer valid, and a partial copy would leave zeros in the new buffer. // structure is no longer valid, and a partial copy would leave zeros in the new buffer.
if let Some(rect) = dirty_rect.filter(|_| !is_full_copy) { if let Some(rect) = dirty_rect.filter(|_| !is_full_copy) {
let w = doc.engine.canvas_width() as usize; let w = doc.engine.canvas_width() as usize;
let h = doc.engine.canvas_height() as usize; let _h = doc.engine.canvas_height() as usize;
let [x0, y0, x1, y1] = rect; let [x0, y0, x1, y1] = rect;
let x0 = x0 as usize; let x0 = x0 as usize;
let y0 = y0 as usize; let y0 = y0 as usize;
@@ -5383,6 +5383,20 @@ impl HcieIcedApp {
self.documents[self.active_doc].vector_drag_preview = None; self.documents[self.active_doc].vector_drag_preview = None;
return Task::none(); return Task::none();
} }
// Frame-skip throttle: skip processing if less than ~16ms since
// the last update to keep the overlay drawing at ~60 FPS and avoid
// flooding the message queue.
let now = std::time::Instant::now();
let min_interval = std::time::Duration::from_secs_f32(1.0 / 60.0);
if let Some(last) = self.vector_drag_last_render {
if now.duration_since(last) < min_interval {
// Still update pointer tracking even when skipping preview
self.vector_drag_last = Some((x, y));
return Task::none();
}
}
let shape = crate::vector_edit::transform_shape( let shape = crate::vector_edit::transform_shape(
&session, &session,
(x, y), (x, y),
@@ -5399,6 +5413,7 @@ impl HcieIcedApp {
self.documents[self.active_doc].vector_drag_preview = Some(shape); self.documents[self.active_doc].vector_drag_preview = Some(shape);
self.documents[self.active_doc].modified = true; self.documents[self.active_doc].modified = true;
self.vector_drag_last = Some((x, y)); self.vector_drag_last = Some((x, y));
self.vector_drag_last_render = Some(now);
self.vector_drag_last_bounds = Some((x1, y1, x2, y2)); self.vector_drag_last_bounds = Some((x1, y1, x2, y2));
self.vector_drag_last_angle = Some(angle); self.vector_drag_last_angle = Some(angle);
self.settings.tool_settings.vector_angle = angle.to_degrees(); self.settings.tool_settings.vector_angle = angle.to_degrees();
@@ -5485,12 +5500,32 @@ impl HcieIcedApp {
} }
// ── Vector shape apply / cancel / flip ────── // ── Vector shape apply / cancel / flip ──────
Message::VectorShapeApply => { Message::VectorShapeApply => {
// Commit edits: clear snapshot and deselect. // Commit any pending drag preview to the engine before deselecting,
let doc = &mut self.documents[self.active_doc]; // then clear the snapshot so Cancel cannot revert.
doc.selected_vector_shape = None; let need_refresh = {
doc.vector_cycle_index = 0; let doc = &mut self.documents[self.active_doc];
doc.vector_edit_handle = hcie_engine_api::VectorEditHandle::None; if let Some(shape) = doc.vector_drag_preview.take() {
doc.vector_drag_preview = None; let shape_idx = doc.selected_vector_shape;
let (x1, y1, x2, y2) = shape.normalized_bounds();
let angle = shape.angle();
let layer_id = doc.engine.active_layer_id();
if let Some(idx) = shape_idx {
doc.engine.set_vector_shape_bounds(layer_id, idx, x1, y1, x2, y2);
doc.engine.set_vector_shape_angle(layer_id, idx, angle);
}
doc.engine.mark_composite_dirty();
doc.engine.is_composite_dirty()
} else {
false
}
};
if need_refresh {
self.refresh_composite_if_needed();
}
self.documents[self.active_doc].selected_vector_shape = None;
self.documents[self.active_doc].vector_cycle_index = 0;
self.documents[self.active_doc].vector_edit_handle =
hcie_engine_api::VectorEditHandle::None;
self.vector_shapes_snapshot = None; self.vector_shapes_snapshot = None;
self.vector_drag_last = None; self.vector_drag_last = None;
self.vector_drag_session = None; self.vector_drag_session = None;
@@ -5510,6 +5545,7 @@ impl HcieIcedApp {
self.vector_drag_session = None; self.vector_drag_session = None;
doc.engine.mark_composite_dirty(); doc.engine.mark_composite_dirty();
self.refresh_composite_if_needed(); self.refresh_composite_if_needed();
return Task::perform(async {}, |_| Message::CompositeRefresh);
} }
} }
Message::VectorShapeFlipH => { Message::VectorShapeFlipH => {
@@ -8230,7 +8266,7 @@ mod cycle_one_ux_tests {
/// Find the topmost text layer whose rasterized pixels contain the point /// Find the topmost text layer whose rasterized pixels contain the point
/// `(x, y)` in canvas coordinates. /// `(x, y)` in canvas coordinates.
fn find_text_layer_at(engine: &hcie_engine_api::Engine, x: f32, y: f32) -> Option<u64> { fn _find_text_layer_at(engine: &hcie_engine_api::Engine, x: f32, y: f32) -> Option<u64> {
let ux = x.round().clamp(0.0, u32::MAX as f32) as u32; let ux = x.round().clamp(0.0, u32::MAX as f32) as u32;
let uy = y.round().clamp(0.0, u32::MAX as f32) as u32; let uy = y.round().clamp(0.0, u32::MAX as f32) as u32;
let infos = engine.layer_infos(); let infos = engine.layer_infos();
@@ -537,7 +537,7 @@ pub fn import_abr_with_prefix(data: &[u8], prefix: &str) -> Vec<BrushPreset> {
/// ///
/// # Returns /// # Returns
/// Optional `BrushPreset` if parsing succeeds. /// Optional `BrushPreset` if parsing succeeds.
pub fn import_kpp(data: &[u8], name_hint: &str) -> Option<BrushPreset> { pub fn _import_kpp(data: &[u8], name_hint: &str) -> Option<BrushPreset> {
use std::io::Cursor; use std::io::Cursor;
use zip::ZipArchive; use zip::ZipArchive;
@@ -60,11 +60,11 @@ struct OverlayProgram {
/// Active selection transform (for drawing handles). /// Active selection transform (for drawing handles).
selection_transform: Option<SelectionTransform>, selection_transform: Option<SelectionTransform>,
/// Current transform handle being hovered/dragged. /// Current transform handle being hovered/dragged.
active_handle: TransformHandle, _active_handle: TransformHandle,
/// Crop tool state (for drawing crop overlay). /// Crop tool state (for drawing crop overlay).
crop_state: Option<CropState>, crop_state: Option<CropState>,
/// Marching ants animation offset (0.0..1.0). /// Marching ants animation offset (0.0..1.0).
marching_ants_offset: f32, _marching_ants_offset: f32,
/// Current pen pressure for HUD display (0.0..1.0, 1.0 = no tablet). /// Current pen pressure for HUD display (0.0..1.0, 1.0 = no tablet).
pressure: f32, pressure: f32,
/// Active tool (drives vector shape preview geometry). /// Active tool (drives vector shape preview geometry).
@@ -82,11 +82,11 @@ struct OverlayProgram {
/// Polygon selection points accumulated during click-by-click (canvas-space). /// Polygon selection points accumulated during click-by-click (canvas-space).
polygon_points: Vec<(u32, u32)>, polygon_points: Vec<(u32, u32)>,
/// Extracted edges for marching ants rendering. /// Extracted edges for marching ants rendering.
marching_ants_edges: Option<std::sync::Arc<Vec<(u32, u32, u32, u32)>>>, _marching_ants_edges: Option<std::sync::Arc<Vec<(u32, u32, u32, u32)>>>,
/// Exact horizontal selected-pixel spans for irregular selection fill. /// Exact horizontal selected-pixel spans for irregular selection fill.
selection_fill_spans: Option<std::sync::Arc<Vec<(u32, u32, u32)>>>, _selection_fill_spans: Option<std::sync::Arc<Vec<(u32, u32, u32)>>>,
/// Whether selected pixels use quick-mask red instead of the normal blue tint. /// Whether selected pixels use quick-mask red instead of the normal blue tint.
quick_mask: bool, _quick_mask: bool,
/// Active brush diameter in canvas pixels for the footprint cursor. /// Active brush diameter in canvas pixels for the footprint cursor.
brush_size: f32, brush_size: f32,
/// Normalized bounds of the selected vector shape in canvas-space (x1, y1, x2, y2). /// Normalized bounds of the selected vector shape in canvas-space (x1, y1, x2, y2).
@@ -891,7 +891,7 @@ impl canvas::Program<Message> for OverlayProgram {
// arrow_thickness = stroke * 1.8, head_len = arrow_thickness * 3 // arrow_thickness = stroke * 1.8, head_len = arrow_thickness * 3
let dx = x1 - x0; let dx = x1 - x0;
let dy = y1 - y0; let dy = y1 - y0;
let len = (dx * dx + dy * dy).sqrt().max(1.0); let _len = (dx * dx + dy * dy).sqrt().max(1.0);
let head_angle = dy.atan2(dx); let head_angle = dy.atan2(dx);
let arrow_angle = std::f32::consts::PI / 6.0; let arrow_angle = std::f32::consts::PI / 6.0;
let arrow_thickness = self.zoom; // stroke width in canvas space let arrow_thickness = self.zoom; // stroke width in canvas space
@@ -1283,6 +1283,51 @@ impl canvas::Program<Message> for OverlayProgram {
} }
} }
// Draw vector shape preview during a drag (real-time rotated outline).
// The engine composite is NOT updated during drag, so the overlay must
// draw the preview shape to show the user where the shape will end up.
// A solid bright outline is used instead of dashed so the preview is
// clearly visible during fast pointer movements.
if let Some(preview) = self.vector_drag_preview.as_ref() {
let (px1, py1, px2, py2) = preview.normalized_bounds();
let pang = preview.angle();
let cx = (px1 + px2) * 0.5;
let cy = (py1 + py2) * 0.5;
let cos_a = pang.cos();
let sin_a = pang.sin();
let rotate = |x: f32, y: f32| -> (f32, f32) {
let dx = x - cx;
let dy = y - cy;
(cx + dx * cos_a - dy * sin_a, cy + dx * sin_a + dy * cos_a)
};
let corners = [(px1, py1), (px2, py1), (px2, py2), (px1, py2)];
let rotated_corners: Vec<(f32, f32)> =
corners.iter().map(|&(x, y)| rotate(x, y)).collect();
let preview_path = Path::new(|b| {
for (i, &(rx, ry)) in rotated_corners.iter().enumerate() {
let sx = origin_x + rx * self.zoom;
let sy = origin_y + ry * self.zoom;
if i == 0 {
b.move_to(Point::new(sx, sy));
} else {
b.line_to(Point::new(sx, sy));
}
}
b.close();
});
// Solid bright outline for fast visible feedback
frame.stroke(
&preview_path,
Stroke {
style: canvas::stroke::Style::Solid(
iced::Color::from_rgba(0.2, 0.9, 0.4, 0.95),
),
width: (2.5 / self.zoom.max(1.0)).max(1.0),
..Default::default()
},
);
}
// Draw vector edit handles for selected shape (blue outline, resize + rotation) // Draw vector edit handles for selected shape (blue outline, resize + rotation)
if let Some((x1, y1, x2, y2)) = self.selected_vector_bounds { if let Some((x1, y1, x2, y2)) = self.selected_vector_bounds {
self.draw_vector_edit_handles( self.draw_vector_edit_handles(
@@ -1624,12 +1669,17 @@ pub fn view<'a>(
) )
} else if let Some(idx) = doc.selected_vector_shape { } else if let Some(idx) = doc.selected_vector_shape {
let layer_id = doc.engine.active_layer_id(); let layer_id = doc.engine.active_layer_id();
if let Some(shapes) = doc.engine.active_vector_shapes() { let shapes = doc.engine.active_vector_shapes();
if let Some(shape) = shapes.get(idx) { // Validate that the selected index exists on the current active layer.
// If the layer changed or shapes were reordered, the index may be stale.
if let Some(ref shapes) = shapes {
if idx < shapes.len() {
let shape = &shapes[idx];
let (x1, y1, x2, y2) = shape.normalized_bounds(); let (x1, y1, x2, y2) = shape.normalized_bounds();
let angle = doc.engine.vector_shape_angle(layer_id, idx); let angle = doc.engine.vector_shape_angle(layer_id, idx);
(Some((x1, y1, x2, y2)), angle, None) (Some((x1, y1, x2, y2)), angle, None)
} else { } else {
// Stale index — silently show nothing until the next valid click
(None, 0.0, None) (None, 0.0, None)
} }
} else { } else {
@@ -1648,9 +1698,9 @@ pub fn view<'a>(
selection_bounds: doc.selection_bounds, selection_bounds: doc.selection_bounds,
vector_draw: doc.vector_draw, vector_draw: doc.vector_draw,
selection_transform: doc.selection_transform.clone(), selection_transform: doc.selection_transform.clone(),
active_handle: TransformHandle::None, // TODO: track hover state _active_handle: TransformHandle::None, // TODO: track hover state
crop_state: Some(doc.crop_state.clone()), crop_state: Some(doc.crop_state.clone()),
marching_ants_offset, _marching_ants_offset: marching_ants_offset,
pressure, pressure,
active_tool: tool_state.active_tool, active_tool: tool_state.active_tool,
star_points, star_points,
@@ -1659,9 +1709,9 @@ pub fn view<'a>(
gradient_drag: doc.gradient_drag, gradient_drag: doc.gradient_drag,
lasso_points: doc.lasso_points.clone(), lasso_points: doc.lasso_points.clone(),
polygon_points: doc.polygon_points.clone(), polygon_points: doc.polygon_points.clone(),
marching_ants_edges: doc.marching_ants_edges.clone(), _marching_ants_edges: doc.marching_ants_edges.clone(),
selection_fill_spans: doc.selection_fill_spans.clone(), _selection_fill_spans: doc.selection_fill_spans.clone(),
quick_mask: doc.quick_mask, _quick_mask: doc.quick_mask,
brush_size: tool_state.brush_size, brush_size: tool_state.brush_size,
selected_vector_bounds: sel_vec_bounds, selected_vector_bounds: sel_vec_bounds,
selected_vector_angle: sel_vec_angle, selected_vector_angle: sel_vec_angle,
@@ -31,6 +31,7 @@ pub enum ColorPickerTarget {
/// HSV color representation (Hue, Saturation, Value) /// HSV color representation (Hue, Saturation, Value)
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct Hsv { pub struct Hsv {
pub h: f32, // 0..360 pub h: f32, // 0..360
pub s: f32, // 0..1 pub s: f32, // 0..1
@@ -38,12 +39,14 @@ pub struct Hsv {
} }
impl Hsv { impl Hsv {
#[allow(dead_code)]
pub fn to_rgb(&self) -> (u8, u8, u8) { pub fn to_rgb(&self) -> (u8, u8, u8) {
hsv_to_rgb(self.h, self.s, self.v) hsv_to_rgb(self.h, self.s, self.v)
} }
} }
/// Convert RGB to HSV. /// Convert RGB to HSV.
#[allow(dead_code)]
pub fn rgb_to_hsv(r: u8, g: u8, b: u8) -> Hsv { pub fn rgb_to_hsv(r: u8, g: u8, b: u8) -> Hsv {
let r = r as f32 / 255.0; let r = r as f32 / 255.0;
let g = g as f32 / 255.0; let g = g as f32 / 255.0;
@@ -69,6 +72,7 @@ pub fn rgb_to_hsv(r: u8, g: u8, b: u8) -> Hsv {
} }
/// Convert HSV to RGB. /// Convert HSV to RGB.
#[allow(dead_code)]
pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) { pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
let h = h / 360.0; let h = h / 360.0;
let (r, g, b) = if s == 0.0 { let (r, g, b) = if s == 0.0 {
@@ -15,6 +15,7 @@ pub enum DockRole {
/// A movable utility panel. /// A movable utility panel.
Tool, Tool,
/// Shell UI that never enters the dock manager. /// Shell UI that never enters the dock manager.
#[allow(dead_code)]
Fixed, Fixed,
} }
@@ -108,6 +108,7 @@ impl PaneType {
} }
/// Compact readable label used on narrow auto-hide rails. /// Compact readable label used on narrow auto-hide rails.
#[allow(dead_code)]
pub fn rail_label(self) -> &'static str { pub fn rail_label(self) -> &'static str {
match self { match self {
Self::ColorPicker => "Color", Self::ColorPicker => "Color",
@@ -40,6 +40,7 @@ pub enum TranslationKey {
/// Returns the translated string for the given language and key. /// Returns the translated string for the given language and key.
/// ///
/// Uses match arms to return `&'static str` — no allocations at call site. /// Uses match arms to return `&'static str` — no allocations at call site.
#[allow(dead_code)]
pub fn t(lang: Language, key: TranslationKey) -> &'static str { pub fn t(lang: Language, key: TranslationKey) -> &'static str {
match lang { match lang {
Language::Turkish => match key { Language::Turkish => match key {
@@ -4,3 +4,4 @@ pub mod cli;
pub mod raster; pub mod raster;
pub mod selection; pub mod selection;
pub mod vector_edit; pub mod vector_edit;
pub mod widgets;
@@ -24,6 +24,7 @@ mod sidebar;
mod theme; mod theme;
mod vector_edit; mod vector_edit;
mod viewer; mod viewer;
mod widgets;
use app::HcieIcedApp; use app::HcieIcedApp;
@@ -778,7 +778,7 @@ pub fn view(
.padding([2, 8]); .padding([2, 8]);
row![lbl, toggle_btn].spacing(4) row![lbl, toggle_btn].spacing(4)
}, },
{ text("Gradient stops configured via color picker").size(10) }, text("Gradient stops configured via color picker").size(10),
] ]
.spacing(2) .spacing(2)
.into() .into()
@@ -7,7 +7,7 @@
use crate::app::Message; use crate::app::Message;
use crate::panels::styles; use crate::panels::styles;
use crate::theme::ThemeColors; use crate::theme::ThemeColors;
use iced::widget::{column, container, horizontal_rule, scrollable, text}; use iced::widget::{column, container, scrollable, text};
use iced::{Element, Length}; use iced::{Element, Length};
/// Build the history panel element. /// Build the history panel element.
@@ -229,6 +229,7 @@ fn menu_item_row(
/// All menu definitions matching Photopea's menu structure exactly. /// All menu definitions matching Photopea's menu structure exactly.
#[derive(Clone)] #[derive(Clone)]
struct MenuDef { struct MenuDef {
#[allow(dead_code)]
label: String, label: String,
items: Vec<MenuItem>, items: Vec<MenuItem>,
} }
@@ -10,10 +10,9 @@ use crate::color_picker::ColorPickerTarget;
use crate::panels::styles; use crate::panels::styles;
use crate::panels::typography::{BODY, SECTION}; use crate::panels::typography::{BODY, SECTION};
use crate::theme::ThemeColors; use crate::theme::ThemeColors;
use crate::widgets::plain_slider;
use hcie_engine_api::{BlendMode, LayerType, Tool, VectorShape}; use hcie_engine_api::{BlendMode, LayerType, Tool, VectorShape};
use iced::widget::{ use iced::widget::{button, checkbox, column, container, horizontal_rule, row, scrollable, text};
button, checkbox, column, container, horizontal_rule, row, scrollable, slider, text,
};
use iced::{Element, Length}; use iced::{Element, Length};
/// Human-readable label for a shape at a given index. /// Human-readable label for a shape at a given index.
@@ -200,11 +199,15 @@ fn layer_info_section<'a>(
layer_id: u64, layer_id: u64,
_colors: ThemeColors, _colors: ThemeColors,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let opacity_slider = slider(0.0..=1.0, opacity, move |v| { let opacity_slider = plain_slider(
Message::LayerSetOpacity(layer_id, v) "Opacity",
}) opacity,
.step(0.01) 0.0..=1.0,
.width(Length::Fill); 0.01,
"%",
0,
move |v| Message::LayerSetOpacity(layer_id, v),
);
column![ column![
text("Layer Info").size(SECTION), text("Layer Info").size(SECTION),
@@ -213,18 +216,11 @@ fn layer_info_section<'a>(
text(name).size(BODY), text(name).size(BODY),
] ]
.spacing(4), .spacing(4),
row![
text("Opacity").size(BODY).width(Length::Fixed(58.0)),
text(format!("{:.0}%", opacity * 100.0))
.size(BODY)
.width(Length::Fixed(36.0)),
]
.spacing(4),
opacity_slider, opacity_slider,
row![ row![
text("Visible").size(BODY).width(Length::Fixed(58.0)), text("Visible").size(BODY).width(Length::Fixed(58.0)),
checkbox("", visible) checkbox("", visible)
.on_toggle(move |v| Message::LayerToggleVisibility(layer_id)) .on_toggle(move |_v| Message::LayerToggleVisibility(layer_id))
.size(11), .size(11),
] ]
.spacing(4) .spacing(4)
@@ -289,44 +285,42 @@ fn vector_select_properties<'a>(
text("Shape Properties").size(SECTION), text("Shape Properties").size(SECTION),
row![text("Shape").size(BODY), text(label).size(BODY)].spacing(4), row![text("Shape").size(BODY), text(label).size(BODY)].spacing(4),
row![text("Type").size(BODY), text(shape_type).size(BODY)].spacing(4), row![text("Type").size(BODY), text(shape_type).size(BODY)].spacing(4),
row![ plain_slider(
text("Stroke").size(BODY), "Stroke",
text(format!("{:.1}", stroke)).size(BODY) stroke,
] 0.0..=50.0,
.spacing(4), 0.5,
slider(0.0..=50.0, stroke, move |v| Message::VectorStrokeChanged(v)) "px",
.step(0.5) 1,
.width(Length::Fill), move |v| Message::VectorStrokeChanged(v),
row![ ),
text("Opacity").size(BODY), plain_slider(
text(format!("{:.0}%", opacity * 100.0)).size(BODY) "Opacity",
] opacity,
.spacing(4), 0.0..=1.0,
slider(0.0..=1.0, opacity, move |v| Message::VectorOpacityChanged( 0.01,
v "%",
)) 0,
.step(0.01) move |v| Message::VectorOpacityChanged(v),
.width(Length::Fill), ),
row![ plain_slider(
text("Hardness").size(BODY), "Hardness",
text(format!("{:.0}%", hardness * 100.0)).size(BODY) hardness,
] 0.0..=1.0,
.spacing(4), 0.01,
slider(0.0..=1.0, hardness, move |v| { "%",
Message::GeometryHardnessChanged(v) 0,
}) move |v| Message::GeometryHardnessChanged(v),
.step(0.01) ),
.width(Length::Fill), plain_slider(
row![ "Angle",
text("Angle").size(BODY), vector_angle,
text(format!("{:.0}°", vector_angle)).size(BODY) -180.0..=180.0,
] 1.0,
.spacing(4), "°",
slider(-180.0..=180.0, vector_angle, move |v| { 0,
Message::VectorAngleChanged(v) move |v| Message::VectorAngleChanged(v),
}) ),
.step(1.0)
.width(Length::Fill),
row![ row![
text("Fill").size(BODY), text("Fill").size(BODY),
checkbox("", filled) checkbox("", filled)
@@ -380,22 +374,24 @@ fn vector_tool_properties<'a>(
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let mut col = column![ let mut col = column![
text("Vector Shape").size(11), text("Vector Shape").size(11),
row![ plain_slider(
text("Stroke").size(10), "Stroke",
text(format!("{:.1}", stroke)).size(10) stroke,
] 0.0..=50.0,
.spacing(4), 0.5,
slider(0.0..=50.0, stroke, |v| Message::VectorStrokeChanged(v)) "px",
.step(0.5) 1,
.width(Length::Fill), |v| Message::VectorStrokeChanged(v),
row![ ),
text("Opacity").size(10), plain_slider(
text(format!("{:.0}%", opacity * 100.0)).size(10) "Opacity",
] opacity,
.spacing(4), 0.0..=1.0,
slider(0.0..=1.0, opacity, |v| Message::VectorOpacityChanged(v)) 0.01,
.step(0.01) "%",
.width(Length::Fill), 0,
|v| Message::VectorOpacityChanged(v),
),
row![ row![
text("Fill").size(10), text("Fill").size(10),
checkbox("", fill) checkbox("", fill)
@@ -426,45 +422,37 @@ fn vector_tool_properties<'a>(
// Shape-specific controls // Shape-specific controls
match tool { match tool {
Tool::VectorRect => { Tool::VectorRect => {
col = col.push( col = col.push(plain_slider(
row![ "Radius",
text("Radius").size(10), radius,
text(format!("{:.1}", radius)).size(10) 0.0..=100.0,
] 1.0,
.spacing(4), "px",
); 1,
col = col.push( |v| Message::VectorRadiusChanged(v),
slider(0.0..=100.0, radius, |v| Message::VectorRadiusChanged(v)) ));
.step(1.0)
.width(Length::Fill),
);
} }
Tool::VectorStar => { Tool::VectorStar => {
col = col.push( col = col.push(plain_slider(
row![ "Points",
text("Points").size(10), points as f32,
text(format!("{}", points)).size(10) 3.0..=20.0,
] 1.0,
.spacing(4), "",
); 0,
col = col.push( |v| Message::VectorPointsChanged(v as u32),
slider(3.0..=20.0, points as f32, |v| { ));
Message::VectorPointsChanged(v as u32)
})
.step(1.0)
.width(Length::Fill),
);
} }
Tool::VectorPolygon => { Tool::VectorPolygon => {
col = col col = col.push(plain_slider(
.push(row![text("Sides").size(10), text(format!("{}", sides)).size(10)].spacing(4)); "Sides",
col = col.push( sides as f32,
slider(3.0..=12.0, sides as f32, |v| { 3.0..=12.0,
Message::VectorSidesChanged(v as u32) 1.0,
}) "",
.step(1.0) 0,
.width(Length::Fill), |v| Message::VectorSidesChanged(v as u32),
); ));
} }
_ => {} _ => {}
} }
@@ -488,32 +476,35 @@ fn brush_properties<'a>(
hardness: f32, hardness: f32,
_colors: ThemeColors, _colors: ThemeColors,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let size_slider = slider(1.0..=200.0, size, |v| Message::BrushSizeChanged(v))
.step(1.0)
.width(Length::Fill);
let opacity_slider = slider(0.0..=1.0, opacity, |v| Message::BrushOpacityChanged(v))
.step(0.01)
.width(Length::Fill);
let hardness_slider = slider(0.0..=1.0, hardness, |v| Message::BrushHardnessChanged(v))
.step(0.01)
.width(Length::Fill);
column![ column![
text("Brush Properties").size(11), text("Brush Properties").size(11),
row![text("Size").size(10), text(format!("{:.0}", size)).size(10)].spacing(4), plain_slider(
size_slider, "Size",
row![ size,
text("Opacity").size(10), 1.0..=200.0,
text(format!("{:.0}%", opacity * 100.0)).size(10) 1.0,
] "px",
.spacing(4), 0,
opacity_slider, |v| Message::BrushSizeChanged(v),
row![ ),
text("Hardness").size(10), plain_slider(
text(format!("{:.0}%", hardness * 100.0)).size(10) "Opacity",
] opacity,
.spacing(4), 0.0..=1.0,
hardness_slider, 0.01,
"%",
0,
|v| Message::BrushOpacityChanged(v),
),
plain_slider(
"Hardness",
hardness,
0.0..=1.0,
0.01,
"%",
0,
|v| Message::BrushHardnessChanged(v),
),
] ]
.spacing(4) .spacing(4)
.into() .into()
@@ -106,6 +106,7 @@ pub fn panel_background(colors: ThemeColors) -> iced::widget::container::Style {
} }
/// Panel header — #333333 (Photopea header color). /// Panel header — #333333 (Photopea header color).
#[allow(dead_code)]
pub fn panel_header(colors: ThemeColors) -> iced::widget::container::Style { pub fn panel_header(colors: ThemeColors) -> iced::widget::container::Style {
iced::widget::container::Style { iced::widget::container::Style {
background: Some(iced::Background::Color(colors.bg_active)), background: Some(iced::Background::Color(colors.bg_active)),
@@ -53,9 +53,9 @@ pub(crate) fn menu_anchor_x(menu_index: usize, viewport_width: f32, dropdown_wid
pub fn view<'a>( pub fn view<'a>(
doc_name: &'a str, doc_name: &'a str,
modified: bool, modified: bool,
zoom: f32, _zoom: f32,
canvas_w: u32, _canvas_w: u32,
canvas_h: u32, _canvas_h: u32,
colors: ThemeColors, colors: ThemeColors,
active_menu: Option<usize>, active_menu: Option<usize>,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
@@ -13,7 +13,7 @@ use crate::app::{Message, ToolState};
use crate::panels::styles; use crate::panels::styles;
use crate::theme::ThemeColors; use crate::theme::ThemeColors;
use hcie_engine_api::Tool; use hcie_engine_api::Tool;
use iced::widget::{button, column, container, horizontal_rule, row, slider, text, text_input}; use iced::widget::{button, container, row, slider, text, text_input};
use iced::{Element, Length}; use iced::{Element, Length};
/// Build the Photopea-style tool options bar. /// Build the Photopea-style tool options bar.
@@ -25,8 +25,8 @@ use iced::{Element, Length};
/// - Move: Auto-Select /// - Move: Auto-Select
/// - Text: Font, Size, Color /// - Text: Font, Size, Color
/// - Gradient: Mode, Opacity /// - Gradient: Mode, Opacity
pub fn view<'a>(tool_state: &'a ToolState, colors: ThemeColors) -> Element<'a, Message> { pub fn _view<'a>(tool_state: &'a ToolState, colors: ThemeColors) -> Element<'a, Message> {
let tool_name = text(tool_name_label(tool_state.active_tool)) let tool_name = text(_tool_name_label(tool_state.active_tool))
.size(11) .size(11)
.style(move |_theme: &iced::Theme| iced::widget::text::Style { .style(move |_theme: &iced::Theme| iced::widget::text::Style {
color: Some(colors.text_primary), color: Some(colors.text_primary),
@@ -202,7 +202,7 @@ pub fn view<'a>(tool_state: &'a ToolState, colors: ThemeColors) -> Element<'a, M
} }
/// Get the display name for a tool. /// Get the display name for a tool.
fn tool_name_label(tool: Tool) -> &'static str { fn _tool_name_label(tool: Tool) -> &'static str {
match tool { match tool {
Tool::Move => "Move Tool", Tool::Move => "Move Tool",
Tool::Select => "Rectangle Select", Tool::Select => "Rectangle Select",
@@ -226,6 +226,7 @@ fn tool_name_label(tool: Tool) -> &'static str {
} }
/// A small checkbox-style label (visual only, for display). /// A small checkbox-style label (visual only, for display).
#[allow(dead_code)]
fn checkbox_label(label: &str, checked: bool) -> Element<'static, Message> { fn checkbox_label(label: &str, checked: bool) -> Element<'static, Message> {
let mark = if checked { "" } else { "" }; let mark = if checked { "" } else { "" };
let label_owned = label.to_string(); let label_owned = label.to_string();
@@ -236,6 +237,7 @@ fn checkbox_label(label: &str, checked: bool) -> Element<'static, Message> {
} }
/// A small flat button for tool options. /// A small flat button for tool options.
#[allow(dead_code)]
fn small_button(label: &str, colors: ThemeColors) -> Element<'static, Message> { fn small_button(label: &str, colors: ThemeColors) -> Element<'static, Message> {
let bg_hover = colors.bg_hover; let bg_hover = colors.bg_hover;
let bg_panel = colors.bg_panel; let bg_panel = colors.bg_panel;
@@ -17,7 +17,7 @@ use crate::settings::AppSettings;
use crate::theme::ThemeColors; use crate::theme::ThemeColors;
use hcie_engine_api::Tool; use hcie_engine_api::Tool;
use iced::widget::{ use iced::widget::{
button, checkbox, column, container, horizontal_rule, row, scrollable, slider, text, text_input, button, column, container, horizontal_rule, row, scrollable, slider, text, text_input,
}; };
use iced::{Element, Length}; use iced::{Element, Length};
@@ -309,10 +309,10 @@ fn text_settings(
let draft_angle = text_draft.map(|d| d.angle).unwrap_or(ts2.text_angle); let draft_angle = text_draft.map(|d| d.angle).unwrap_or(ts2.text_angle);
let draft_color = text_draft.map(|d| d.color).unwrap_or(fg_color); let draft_color = text_draft.map(|d| d.color).unwrap_or(fg_color);
let draft_content = text_draft.map(|d| d.content.clone()).unwrap_or_default(); let draft_content = text_draft.map(|d| d.content.clone()).unwrap_or_default();
let alignment = text_draft let _alignment = text_draft
.map(|d| format!("{:?}", d.alignment)) .map(|d| format!("{:?}", d.alignment))
.unwrap_or_else(|| "Left".to_string()); .unwrap_or_else(|| "Left".to_string());
let orientation = text_draft let _orientation = text_draft
.map(|d| format!("{:?}", d.orientation)) .map(|d| format!("{:?}", d.orientation))
.unwrap_or_else(|| "Horizontal".to_string()); .unwrap_or_else(|| "Horizontal".to_string());
@@ -231,6 +231,7 @@ fn svg_btn(
) )
} }
#[allow(dead_code)]
fn small_btn(label: &str, colors: ThemeColors) -> Element<'static, Message> { fn small_btn(label: &str, colors: ThemeColors) -> Element<'static, Message> {
let c = colors; let c = colors;
let l = label.to_string(); let l = label.to_string();
@@ -1,9 +1,3 @@
pub mod executor; pub mod executor;
pub mod parser; pub mod parser;
pub mod types; pub mod types;
pub use executor::execute_actions;
pub use parser::parse_dsl_script;
pub use parser::parse_error_line;
pub use parser::suggest_fix;
pub use parser::DEFAULT_SCRIPT;
@@ -93,6 +93,7 @@ pub fn extract_masked_pixels(
/// * `floating` - The floating selection transform containing pixels to composite /// * `floating` - The floating selection transform containing pixels to composite
/// * `canvas_w` - Width of the canvas in pixels /// * `canvas_w` - Width of the canvas in pixels
/// * `canvas_h` - Height of the canvas in pixels /// * `canvas_h` - Height of the canvas in pixels
#[allow(dead_code)]
pub fn composite_floating_to_layer( pub fn composite_floating_to_layer(
layer_pixels: &mut [u8], layer_pixels: &mut [u8],
floating: &SelectionTransform, floating: &SelectionTransform,
@@ -339,6 +339,7 @@ impl TransformHandle {
]; ];
/// Get cursor icon hint for this handle. /// Get cursor icon hint for this handle.
#[allow(dead_code)]
pub fn cursor_hint(&self) -> &'static str { pub fn cursor_hint(&self) -> &'static str {
match self { match self {
TransformHandle::None => "default", TransformHandle::None => "default",
@@ -22,6 +22,7 @@ use iced::{Element, Length};
pub struct ToolSlot { pub struct ToolSlot {
pub tools: &'static [Tool], pub tools: &'static [Tool],
pub label: &'static str, pub label: &'static str,
#[allow(dead_code)]
pub icon: &'static str, pub icon: &'static str,
} }
@@ -39,6 +39,7 @@ impl ThemeState {
/// **Returns:** The preset used to produce the current color tokens. /// **Returns:** The preset used to produce the current color tokens.
/// **Logic & Workflow:** Reads the preset from the same lock as its resolved colors. /// **Logic & Workflow:** Reads the preset from the same lock as its resolved colors.
/// **Side Effects / Dependencies:** Briefly locks shared theme state. /// **Side Effects / Dependencies:** Briefly locks shared theme state.
#[allow(dead_code)]
pub fn preset(&self) -> ThemePreset { pub fn preset(&self) -> ThemePreset {
self.state.lock().unwrap().0 self.state.lock().unwrap().0
} }
@@ -183,6 +183,7 @@ impl ViewerState {
} }
/// Get subdirectories of a path, cached to avoid repeated disk reads. /// Get subdirectories of a path, cached to avoid repeated disk reads.
#[allow(dead_code)]
pub fn get_subdirs(&mut self, path: &Path) -> &[PathBuf] { pub fn get_subdirs(&mut self, path: &Path) -> &[PathBuf] {
if !self.subdirs_cache.contains_key(path) { if !self.subdirs_cache.contains_key(path) {
let mut dirs = Vec::new(); let mut dirs = Vec::new();
@@ -154,6 +154,7 @@ fn thumbnail_card(
} }
/// Count how many thumbnails need loading. /// Count how many thumbnails need loading.
#[allow(dead_code)]
pub fn count_unloaded(state: &ViewerState) -> usize { pub fn count_unloaded(state: &ViewerState) -> usize {
state state
.images .images
@@ -0,0 +1,10 @@
//! Reusable iced widgets for the HCIE GUI.
//!
//! Contains the keyboard-editable `PlainSlider`, mirroring the egui version
//! used elsewhere in the project.
#[allow(deprecated)]
pub mod plain_slider;
#[allow(deprecated)]
pub use plain_slider::plain_slider;
@@ -0,0 +1,334 @@
//! A keyboard-editable numeric slider widget for iced.
//!
//! Mirrors the egui `PlainSlider` used in the egui GUI so the iced
//! Properties panel supports direct value entry from the keyboard.
//! The widget is implemented as an iced `Component`, which keeps the
//! transient edit state inside the widget instead of in the application
//! state. This trait is deprecated in iced 0.13, but it is still the
//! simplest way to bundle local state with a reusable widget.
#![allow(deprecated)]
use iced::widget::{column, component, mouse_area, row, slider, text, text_input, Component};
use iced::{Element, Length, Renderer, Theme};
use std::sync::Arc;
/// Internal state for a `PlainSlider` instance.
#[derive(Debug, Default, Clone)]
pub(crate) struct State {
/// Whether the user is currently typing a value.
editing: bool,
/// Current contents of the text input while editing.
buffer: String,
}
/// Internal events produced by the widget UI.
#[derive(Debug, Clone)]
pub(crate) enum Event {
/// The slider thumb was dragged to a new value.
SliderChanged(f32),
/// The value label was clicked; switch to text-input mode.
StartEdit,
/// The text input contents changed.
InputChanged(String),
/// The user pressed Enter or otherwise submitted the typed value.
Submit,
}
/// A numeric slider with an editable value label.
///
/// The value label displays the current value with a suffix such as `%`
/// or `px`. Clicking the label replaces it with a text input so the user
/// can type an exact value.
#[derive(Clone)]
pub(crate) struct PlainSlider<Message> {
/// Human-readable label shown to the left of the value.
label: String,
/// Current value. The parent application owns the authoritative value
/// and passes it back on each frame.
value: f32,
/// Inclusive numeric range the value is allowed to occupy.
range: std::ops::RangeInclusive<f32>,
/// Step size used by the slider thumb.
step: f32,
/// Suffix shown after the value (e.g. "%", "px").
suffix: String,
/// Number of decimal places to display.
decimals: usize,
/// Width of the value label / text input area.
input_width: f32,
/// Callback invoked whenever the value changes, either by dragging
/// the slider or by submitting the text input.
#[allow(clippy::type_complexity)]
on_change: Arc<dyn Fn(f32) -> Message>,
}
impl<Message> std::fmt::Debug for PlainSlider<Message> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PlainSlider")
.field("label", &self.label)
.field("value", &self.value)
.field("range", &self.range)
.field("step", &self.step)
.field("suffix", &self.suffix)
.field("decimals", &self.decimals)
.field("input_width", &self.input_width)
.field("on_change", &"Arc<dyn Fn(f32) -> Message>")
.finish()
}
}
impl<Message> PlainSlider<Message> {
/// Creates a new editable slider.
///
/// # Arguments
/// * `label` - short label shown next to the value.
/// * `value` - current numeric value.
/// * `range` - inclusive allowed range.
/// * `step` - slider step.
/// * `suffix` - unit suffix (e.g. "%", "px").
/// * `decimals` - number of decimals to display.
/// * `on_change` - callback producing a message for the parent.
pub fn new(
label: impl Into<String>,
value: f32,
range: std::ops::RangeInclusive<f32>,
step: f32,
suffix: impl Into<String>,
decimals: usize,
on_change: impl Fn(f32) -> Message + 'static,
) -> Self {
Self {
label: label.into(),
value,
range,
step,
suffix: suffix.into(),
decimals,
input_width: 48.0,
on_change: Arc::new(on_change),
}
}
/// Sets the width of the editable value area.
#[allow(dead_code)]
pub fn input_width(mut self, width: f32) -> Self {
self.input_width = width;
self
}
/// Formats a numeric value for display, including the suffix.
///
/// Percentage values are stored normalized (0..1) and displayed
/// multiplied by 100. Other units are displayed as-is.
fn format_value(&self, value: f32) -> String {
if self.suffix == "%" {
format!("{:.*}{}", self.decimals, value * 100.0, self.suffix)
} else if self.suffix.is_empty() {
format!("{:.*}", self.decimals, value)
} else {
format!("{:.*} {}", self.decimals, value, self.suffix)
}
}
/// Parses a raw string back into a numeric value, taking `%` into account.
fn parse_value(&self, raw: &str) -> Option<f32> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
let numeric = trimmed
.trim_end_matches(&self.suffix)
.trim()
.replace(',', ".");
let parsed: f32 = numeric.parse().ok()?;
let value = if self.suffix == "%" { parsed / 100.0 } else { parsed };
Some(value.clamp(*self.range.start(), *self.range.end()))
}
}
impl<Message> Component<Message, Theme, Renderer> for PlainSlider<Message>
where
Message: Clone,
{
type State = State;
type Event = Event;
fn update(&mut self, state: &mut State, event: Event) -> Option<Message> {
match event {
Event::SliderChanged(new_value) => {
// Update the local value so the displayed text follows the thumb.
self.value = new_value.clamp(*self.range.start(), *self.range.end());
Some((self.on_change)(self.value))
}
Event::StartEdit => {
state.editing = true;
state.buffer = self.format_value(self.value);
None
}
Event::InputChanged(new_value) => {
state.buffer = new_value;
None
}
Event::Submit => {
if let Some(v) = self.parse_value(&state.buffer) {
self.value = v;
state.editing = false;
state.buffer.clear();
Some((self.on_change)(v))
} else {
state.editing = false;
state.buffer.clear();
None
}
}
}
}
fn view(&self, state: &State) -> Element<'_, Event> {
let value_text = self.format_value(self.value);
let slider_control = slider(self.range.clone(), self.value, Event::SliderChanged)
.step(self.step)
.width(Length::Fill);
if state.editing {
let input = text_input("", &state.buffer)
.width(Length::Fixed(self.input_width))
.size(12)
.on_input(Event::InputChanged)
.on_submit(Event::Submit);
let editor = row![input]
.align_y(iced::Alignment::Center)
.spacing(4);
column![
row![
text(&self.label).size(12).width(Length::Shrink),
editor,
]
.spacing(4)
.align_y(iced::Alignment::Center),
slider_control,
]
.spacing(2)
.into()
} else {
let value_label = mouse_area(
text(value_text)
.size(12)
.width(Length::Fixed(self.input_width))
.align_x(iced::alignment::Horizontal::Right),
)
.on_press(Event::StartEdit);
column![
row![
text(&self.label).size(12).width(Length::Shrink),
value_label,
]
.spacing(4)
.align_y(iced::Alignment::Center),
slider_control,
]
.spacing(2)
.into()
}
}
}
/// Convenience constructor that wraps `PlainSlider` in a lazy component element.
///
/// Use this from panel code in place of the standard `slider` widget.
pub fn plain_slider<Message>(
label: impl Into<String>,
value: f32,
range: std::ops::RangeInclusive<f32>,
step: f32,
suffix: impl Into<String>,
decimals: usize,
on_change: impl Fn(f32) -> Message + 'static,
) -> Element<'static, Message>
where
Message: 'static + Clone,
{
component(PlainSlider::new(label, value, range, step, suffix, decimals, on_change))
}
#[cfg(test)]
mod tests {
use super::PlainSlider;
use iced::widget::Component;
fn dummy(_: f32) -> () {}
#[test]
fn formats_percentage_without_decimals() {
let slider = PlainSlider::new("Opacity", 0.5, 0.0..=1.0, 0.01, "%", 0, dummy);
assert_eq!(slider.format_value(0.5), "50%");
assert_eq!(slider.format_value(1.0), "100%");
assert_eq!(slider.format_value(0.0), "0%");
}
#[test]
fn formats_pxx_with_decimals() {
let slider = PlainSlider::new("Stroke", 37.0, 0.0..=50.0, 0.5, "px", 1, dummy);
assert_eq!(slider.format_value(37.0), "37.0 px");
}
#[test]
fn parses_percentage_input() {
let slider = PlainSlider::new("Opacity", 0.5, 0.0..=1.0, 0.01, "%", 0, dummy);
assert!((slider.parse_value("75%").unwrap() - 0.75).abs() < 1e-6);
assert!((slider.parse_value("75").unwrap() - 0.75).abs() < 1e-6);
assert_eq!(slider.parse_value(""), None);
}
#[test]
fn parses_px_input() {
let slider = PlainSlider::new("Stroke", 10.0, 0.0..=50.0, 0.5, "px", 1, dummy);
assert!((slider.parse_value("25 px").unwrap() - 25.0).abs() < 1e-6);
assert!((slider.parse_value("25").unwrap() - 25.0).abs() < 1e-6);
}
#[test]
fn clamps_out_of_range_input() {
let slider = PlainSlider::new("Opacity", 0.5, 0.0..=1.0, 0.01, "%", 0, dummy);
assert_eq!(slider.parse_value("150"), Some(1.0));
assert_eq!(slider.parse_value("-10"), Some(0.0));
}
#[test]
fn update_emits_on_slider_drag() {
let mut slider = PlainSlider::new("Size", 10.0, 1.0..=200.0, 1.0, "px", 0, dummy);
let mut state = super::State::default();
let message = slider.update(&mut state, super::Event::SliderChanged(42.0));
assert_eq!(message, Some(()));
assert!((slider.value - 42.0).abs() < 1e-6);
}
#[test]
fn update_commits_valid_text_input() {
let mut slider = PlainSlider::new("Opacity", 0.5, 0.0..=1.0, 0.01, "%", 0, dummy);
let mut state = super::State::default();
slider.update(&mut state, super::Event::StartEdit);
slider.update(&mut state, super::Event::InputChanged("80".to_string()));
let message = slider.update(&mut state, super::Event::Submit);
assert!((slider.value - 0.8).abs() < 1e-6);
assert_eq!(message, Some(()));
assert!(!state.editing);
}
#[test]
fn update_ignores_invalid_text_input() {
let mut slider = PlainSlider::new("Opacity", 0.5, 0.0..=1.0, 0.01, "%", 0, dummy);
let mut state = super::State::default();
slider.update(&mut state, super::Event::StartEdit);
slider.update(&mut state, super::Event::InputChanged("abc".to_string()));
let message = slider.update(&mut state, super::Event::Submit);
assert!((slider.value - 0.5).abs() < 1e-6);
assert_eq!(message, None);
assert!(!state.editing);
}
}
+1 -1
View File
@@ -80,7 +80,7 @@ fn main() {
&alpha, CANVAS, CANVAS, &alpha, CANVAS, CANVAS,
s.depth, s.size, s.soften, 120.0, s.altitude, s.depth, s.size, s.soften, 120.0, s.altitude,
&dir_enum, &hcie_fx::types::Technique::Smooth, &style_enum, &dir_enum, &hcie_fx::types::Technique::Smooth, &style_enum,
best_lb, best_hl, best_sh, 1.0, best_lb, best_hl, best_sh, 1.0, 1.0,
); );
let px = npix(); let px = npix();
-2
View File
@@ -1,5 +1,3 @@
use image::GenericImageView;
fn main() { fn main() {
let ref_img = image::open("_images/_psd_stil_test/test_2/Emboss.png") let ref_img = image::open("_images/_psd_stil_test/test_2/Emboss.png")
.unwrap() .unwrap()
@@ -127,7 +127,7 @@ fn main() {
&rect_alpha, unsafe { CANVAS }, unsafe { CANVAS }, &rect_alpha, unsafe { CANVAS }, unsafe { CANVAS },
s.depth, s.size, s.soften, s.angle, 30.0, s.depth, s.size, s.soften, s.angle, 30.0,
&dir_enum, &hcie_fx::types::Technique::Smooth, &style_enum, &dir_enum, &hcie_fx::types::Technique::Smooth, &style_enum,
lighting_blur, hl_scale, sh_scale, profile_exp, lighting_blur, hl_scale, sh_scale, profile_exp, 1.0,
); );
let px = npix(); let px = npix();
+46
View File
@@ -0,0 +1,46 @@
#-------------------------------------------------------------------------------#
# Qodana analysis is configured by qodana.yaml file #
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
#-------------------------------------------------------------------------------#
#################################################################################
# WARNING: Do not store sensitive information in this file, #
# as its contents will be included in the Qodana report. #
#################################################################################
version: "1.0"
#Specify inspection profile for code analysis
profile:
name: qodana.starter
#Enable inspections
#include:
# - name: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
#bootstrap: sh ./prepare-qodana.sh
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
#plugins:
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
# Quality gate. Will fail the CI/CD pipeline if any condition is not met
# severityThresholds - configures maximum thresholds for different problem severities
# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
# Code Coverage is available in Ultimate and Ultimate Plus plans
#failureConditions:
# severityThresholds:
# any: 15
# critical: 5
# testCoverageThresholds:
# fresh: 70
# total: 50
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
linter: jetbrains/qodana-<linter>:2026.1