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:
Generated
+45
-1
@@ -69,6 +69,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aliasable"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||
|
||||
[[package]]
|
||||
name = "aligned"
|
||||
version = "0.4.3"
|
||||
@@ -1333,7 +1339,7 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
]
|
||||
@@ -2884,6 +2890,12 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -3226,6 +3238,7 @@ dependencies = [
|
||||
"iced_runtime",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"ouroboros",
|
||||
"rustc-hash 2.1.2",
|
||||
"thiserror 1.0.69",
|
||||
"unicode-segmentation",
|
||||
@@ -4614,6 +4627,30 @@ dependencies = [
|
||||
"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]]
|
||||
name = "owned_ttf_parser"
|
||||
version = "0.25.1"
|
||||
@@ -4989,6 +5026,7 @@ dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"version_check",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7951,6 +7989,12 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yazi"
|
||||
version = "0.1.6"
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ eframe = { version = "0.34", default-features = false, features = ["default
|
||||
egui_extras = { version = "0.34", features = ["svg", "image"] }
|
||||
egui_dock = { version = "0.19", features = ["serde"] }
|
||||
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
|
||||
base64 = "0.22"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use hcie_blend::blend_pixels;
|
||||
use hcie_fx::protocol_to_hcie_fx_effect;
|
||||
use hcie_protocol::Layer;
|
||||
use rayon::prelude::*;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use hcie_blend::blend_pixels;
|
||||
use hcie_fx::apply_layer_effects;
|
||||
use hcie_fx::protocol_to_hcie_fx_effect;
|
||||
use hcie_protocol::Layer;
|
||||
use hcie_tile::{TiledLayer, TILE_SIZE};
|
||||
use rayon::prelude::*;
|
||||
|
||||
@@ -630,12 +630,22 @@ impl<'a> TabViewer for HcieTabViewer<'a> {
|
||||
// Pin / float toggle for docked utility panels only.
|
||||
// LayerStyles, LayerDetails and Plugins are dialog-style panels: they
|
||||
// cannot float and do not show a pin toggle.
|
||||
let supports_float =
|
||||
!tab.is_document() && !matches!(tab, HciePane::Tools | HciePane::LayerStyles | HciePane::LayerDetails | HciePane::Plugins);
|
||||
let supports_float = !tab.is_document()
|
||||
&& !matches!(
|
||||
tab,
|
||||
HciePane::Tools
|
||||
| HciePane::LayerStyles
|
||||
| HciePane::LayerDetails
|
||||
| HciePane::Plugins
|
||||
);
|
||||
if supports_float {
|
||||
// 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.
|
||||
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 =
|
||||
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));
|
||||
@@ -750,7 +760,10 @@ impl<'a> TabViewer for HcieTabViewer<'a> {
|
||||
!tab.is_document()
|
||||
&& !matches!(
|
||||
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
|
||||
// because these panels render as popups / dialogs, not docked utility cards.
|
||||
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 {
|
||||
let mut float = false;
|
||||
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);
|
||||
// Dialog-style panels (LayerStyles/LayerDetails/Plugins) are closed by the
|
||||
// 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);
|
||||
if close && close_in_context {
|
||||
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.
|
||||
#[allow(unused_imports)]
|
||||
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::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};
|
||||
pub use crate::app::theme::{
|
||||
apply_theme, default_font_bytes, setup_custom_fonts, ThemeColors, ThemePreset,
|
||||
};
|
||||
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::plugins;
|
||||
use eframe::egui;
|
||||
@@ -78,8 +85,6 @@ pub const TOOL_SLOTS: &[&[Tool]] = &[
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
|
||||
// ── 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
|
||||
// immediately overridden by the layout solver using stored column widths.
|
||||
// They only determine the tree structure, not final pixel sizes.
|
||||
let [remaining, left_col2] = tree.split_left(
|
||||
egui_dock::NodeIndex::root(),
|
||||
0.17,
|
||||
vec![HciePane::Brushes],
|
||||
);
|
||||
let [remaining, left_col2] =
|
||||
tree.split_left(egui_dock::NodeIndex::root(), 0.17, vec![HciePane::Brushes]);
|
||||
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]);
|
||||
@@ -677,7 +679,10 @@ impl HcieApp {
|
||||
log::info!("Added {} ABR brush preset(s) from {:?}", added, path);
|
||||
rfd::MessageDialog::new()
|
||||
.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)
|
||||
.show();
|
||||
}
|
||||
@@ -789,7 +794,7 @@ impl HcieApp {
|
||||
/// * `doc_pane`: The `dock::HciePane` document pane enum to push.
|
||||
pub fn push_document_to_center(&mut self, doc_pane: dock::HciePane) {
|
||||
let mut target_node = None;
|
||||
|
||||
|
||||
// 1. Prefer a leaf that already has document tabs
|
||||
for (path, leaf) in self.dock_state.iter_leaves() {
|
||||
if leaf.tabs.iter().any(|t| t.is_document()) {
|
||||
@@ -797,7 +802,7 @@ impl HcieApp {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let Some(node_path) = target_node {
|
||||
self.dock_state.set_focused_node_and_surface(node_path);
|
||||
self.dock_state.push_to_focused_leaf(doc_pane.clone());
|
||||
@@ -806,24 +811,22 @@ impl HcieApp {
|
||||
// enforce_constraints will relocate it to the center if needed.
|
||||
self.dock_state.push_to_focused_leaf(doc_pane.clone());
|
||||
}
|
||||
|
||||
|
||||
// Set the newly pushed tab as active to make it immediately visible
|
||||
if let Some(tab_path) = self.dock_state.find_tab(&doc_pane) {
|
||||
let _ = self.dock_state.set_active_tab(tab_path);
|
||||
}
|
||||
|
||||
|
||||
// Ensure dock layout settles and canvas texture uploads before the
|
||||
// user sees the new document. Without this, the canvas appears blank
|
||||
// on the first frame after opening a file.
|
||||
self.dock_settle_repaints = self.dock_settle_repaints.max(5);
|
||||
}
|
||||
|
||||
|
||||
/// Push a newly opened/created document tab into the center document leaf.
|
||||
fn push_document_to_center_placeholder() {}
|
||||
|
||||
/// Panic-safe tab removal: validates indices before calling egui_dock::remove_tab.
|
||||
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.
|
||||
/// Returns `None` and logs a warning if indices are stale.
|
||||
@@ -1021,9 +1024,7 @@ impl HcieApp {
|
||||
}
|
||||
self.active_doc = new_idx;
|
||||
} else {
|
||||
log::trace!(
|
||||
"Removing document tab from egui_dock multi-document state"
|
||||
);
|
||||
log::trace!("Removing document tab from egui_dock multi-document state");
|
||||
let tabs_to_remove = self.dock_find_tabs(
|
||||
|t| matches!(t, dock::HciePane::Document(d) if *d == idx),
|
||||
);
|
||||
@@ -1833,10 +1834,7 @@ impl HcieApp {
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
crate::brush_import::import_abr_with_prefix(
|
||||
&data,
|
||||
&file_stem,
|
||||
)
|
||||
crate::brush_import::import_abr_with_prefix(&data, &file_stem)
|
||||
};
|
||||
if imported.is_empty() {
|
||||
rfd::MessageDialog::new()
|
||||
@@ -1850,12 +1848,21 @@ impl HcieApp {
|
||||
} else {
|
||||
let added = imported.len();
|
||||
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.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()
|
||||
.set_title("Brush Import")
|
||||
.set_description(&format!(
|
||||
@@ -5118,7 +5125,9 @@ impl eframe::App for HcieApp {
|
||||
.state
|
||||
.brush_presets
|
||||
.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()
|
||||
.collect();
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -218,26 +218,21 @@ pub fn apply_theme(ctx: &egui::Context, theme: ThemePreset) {
|
||||
};
|
||||
|
||||
// ── Font sizes matching Qt 12px default ──
|
||||
style.text_styles.insert(
|
||||
egui::TextStyle::Body,
|
||||
egui::FontId::proportional(12.0),
|
||||
);
|
||||
style.text_styles.insert(
|
||||
egui::TextStyle::Button,
|
||||
egui::FontId::proportional(12.0),
|
||||
);
|
||||
style.text_styles.insert(
|
||||
egui::TextStyle::Small,
|
||||
egui::FontId::proportional(11.0),
|
||||
);
|
||||
style.text_styles.insert(
|
||||
egui::TextStyle::Heading,
|
||||
egui::FontId::proportional(13.5),
|
||||
);
|
||||
style.text_styles.insert(
|
||||
egui::TextStyle::Monospace,
|
||||
egui::FontId::proportional(11.5),
|
||||
);
|
||||
style
|
||||
.text_styles
|
||||
.insert(egui::TextStyle::Body, egui::FontId::proportional(12.0));
|
||||
style
|
||||
.text_styles
|
||||
.insert(egui::TextStyle::Button, egui::FontId::proportional(12.0));
|
||||
style
|
||||
.text_styles
|
||||
.insert(egui::TextStyle::Small, egui::FontId::proportional(11.0));
|
||||
style
|
||||
.text_styles
|
||||
.insert(egui::TextStyle::Heading, egui::FontId::proportional(13.5));
|
||||
style
|
||||
.text_styles
|
||||
.insert(egui::TextStyle::Monospace, egui::FontId::proportional(11.5));
|
||||
|
||||
// ── Harmonized layout parameters (8px grid) ──
|
||||
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,
|
||||
angle: 120.0,
|
||||
altitude: 30.0,
|
||||
contour: "Linear".to_string(),
|
||||
highlight_opacity: 0.75,
|
||||
shadow_opacity: 0.75,
|
||||
direction: "Up".to_string(),
|
||||
@@ -352,6 +353,49 @@ fn technique_combobox(ui: &mut egui::Ui, current: &mut String) -> bool {
|
||||
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:**
|
||||
/// 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,
|
||||
)
|
||||
.show_header(ui, |ui| {
|
||||
if ui.checkbox(enabled, "").changed() {
|
||||
if ui.checkbox(enabled, String::new()).changed() {
|
||||
changed = true;
|
||||
}
|
||||
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,
|
||||
)
|
||||
.show_header(ui, |ui| {
|
||||
if ui.checkbox(enabled, "").changed() {
|
||||
if ui.checkbox(enabled, String::new()).changed() {
|
||||
changed = true;
|
||||
}
|
||||
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,
|
||||
)
|
||||
.show_header(ui, |ui| {
|
||||
if ui.checkbox(enabled, "").changed() {
|
||||
if ui.checkbox(enabled, String::new()).changed() {
|
||||
changed = true;
|
||||
}
|
||||
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,
|
||||
)
|
||||
.show_header(ui, |ui| {
|
||||
if ui.checkbox(enabled, "").changed() {
|
||||
if ui.checkbox(enabled, String::new()).changed() {
|
||||
changed = true;
|
||||
}
|
||||
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,
|
||||
shadow_blend_mode,
|
||||
shadow_color,
|
||||
contour,
|
||||
} => {
|
||||
let title = "Bevel & Emboss";
|
||||
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,
|
||||
)
|
||||
.show_header(ui, |ui| {
|
||||
if ui.checkbox(enabled, "").changed() {
|
||||
if ui.checkbox(enabled, String::new()).changed() {
|
||||
changed = true;
|
||||
}
|
||||
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.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(
|
||||
RichText::new("Depth")
|
||||
.size(10.0)
|
||||
@@ -898,7 +955,7 @@ fn show_style_effect_mut(style: &mut LayerStyle, ui: &mut egui::Ui, colors: &The
|
||||
false,
|
||||
)
|
||||
.show_header(ui, |ui| {
|
||||
if ui.checkbox(enabled, "").changed() {
|
||||
if ui.checkbox(enabled, String::new()).changed() {
|
||||
changed = true;
|
||||
}
|
||||
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,
|
||||
)
|
||||
.show_header(ui, |ui| {
|
||||
if ui.checkbox(enabled, "").changed() {
|
||||
if ui.checkbox(enabled, String::new()).changed() {
|
||||
changed = true;
|
||||
}
|
||||
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,
|
||||
)
|
||||
.show_header(ui, |ui| {
|
||||
if ui.checkbox(enabled, "").changed() {
|
||||
if ui.checkbox(enabled, String::new()).changed() {
|
||||
changed = true;
|
||||
}
|
||||
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,
|
||||
)
|
||||
.show_header(ui, |ui| {
|
||||
if ui.checkbox(enabled, "").changed() {
|
||||
if ui.checkbox(enabled, String::new()).changed() {
|
||||
changed = true;
|
||||
}
|
||||
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,
|
||||
)
|
||||
.show_header(ui, |ui| {
|
||||
if ui.checkbox(enabled, "").changed() {
|
||||
if ui.checkbox(enabled, String::new()).changed() {
|
||||
changed = true;
|
||||
}
|
||||
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`, ...).
|
||||
/// * `file_stem` - Base name for the suggested file.
|
||||
/// * `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 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 {
|
||||
(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 {
|
||||
id: format!("abr_brs2_{}_{}x{}", base_pos + i, 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() {
|
||||
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;
|
||||
if nchars == 0 || text_start + nchars * 2 > block.len() {
|
||||
idx += 1;
|
||||
@@ -555,7 +557,12 @@ pub fn import_kpp(data: &[u8], name_hint: &str) -> Option<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),
|
||||
category: "Imported".to_string(),
|
||||
tip: BrushTip {
|
||||
@@ -623,7 +630,10 @@ mod tests {
|
||||
}
|
||||
};
|
||||
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");
|
||||
let has_names = bitmap.iter().any(|p| !p.name.starts_with("Brush "));
|
||||
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 layer_id = self.doc.engine.active_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());
|
||||
|
||||
let desc = match self.state.active_tool {
|
||||
@@ -1474,40 +1477,108 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
}
|
||||
Tool::Brush | _ => {
|
||||
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::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::SoftRound => hcie_engine_api::BrushStyle::SoftRound,
|
||||
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::Spray => hcie_engine_api::BrushStyle::Spray,
|
||||
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::Charcoal => hcie_engine_api::BrushStyle::Charcoal,
|
||||
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,
|
||||
hcie_engine_api::tools::BrushStyle::Default => {
|
||||
hcie_engine_api::BrushStyle::Round
|
||||
}
|
||||
hcie_engine_api::tools::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::SoftRound => {
|
||||
hcie_engine_api::BrushStyle::SoftRound
|
||||
}
|
||||
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::Spray => {
|
||||
hcie_engine_api::BrushStyle::Spray
|
||||
}
|
||||
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::Charcoal => {
|
||||
hcie_engine_api::BrushStyle::Charcoal
|
||||
}
|
||||
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.opacity = self.state.tool_configs.brush.opacity;
|
||||
@@ -1600,25 +1671,20 @@ impl<'a> egui::Widget for CanvasWidget<'a> {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(tex) = ui
|
||||
.data(|d| d.get_temp::<egui::TextureHandle>(ui.id().with("cursor_stamp_tex")))
|
||||
if let Some(tex) =
|
||||
ui.data(|d| d.get_temp::<egui::TextureHandle>(ui.id().with("cursor_stamp_tex")))
|
||||
{
|
||||
// Display the stamp preview at the correct diameter.
|
||||
// The texture was rendered at tex_size × tex_size pixels;
|
||||
// display it at diameter_px × diameter_px screen pixels so
|
||||
// the preview matches the actual brush footprint.
|
||||
let display_dim = egui::vec2(diameter_px, diameter_px);
|
||||
let rect = egui::Rect::from_center_size(
|
||||
egui::pos2(screen_x, screen_y),
|
||||
display_dim,
|
||||
);
|
||||
let rect =
|
||||
egui::Rect::from_center_size(egui::pos2(screen_x, screen_y), display_dim);
|
||||
painter.add(egui::Shape::image(
|
||||
tex.id(),
|
||||
rect,
|
||||
egui::Rect::from_min_max(
|
||||
egui::pos2(0.0, 0.0),
|
||||
egui::pos2(1.0, 1.0),
|
||||
),
|
||||
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||
egui::Color32::WHITE,
|
||||
));
|
||||
|
||||
@@ -2551,5 +2617,3 @@ fn find_text_layer_at(engine: &hcie_engine_api::Engine, x: f32, y: f32) -> Optio
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -203,11 +203,14 @@ pub fn render_composition(
|
||||
let src_start = ((y * rw + x0) as usize) * 4;
|
||||
let dst_start = ((y - y0) as usize * region_w) * 4;
|
||||
let row_bytes = region_w * 4;
|
||||
region_pixels[dst_start..dst_start + row_bytes]
|
||||
.copy_from_slice(&doc.composite_buffer[src_start..src_start + row_bytes]);
|
||||
region_pixels[dst_start..dst_start + row_bytes].copy_from_slice(
|
||||
&doc.composite_buffer[src_start..src_start + row_bytes],
|
||||
);
|
||||
}
|
||||
let region_image =
|
||||
egui::ColorImage::from_rgba_unmultiplied([region_w, region_h], ®ion_pixels);
|
||||
let region_image = egui::ColorImage::from_rgba_unmultiplied(
|
||||
[region_w, region_h],
|
||||
®ion_pixels,
|
||||
);
|
||||
tex.set_partial([x0 as usize, y0 as usize], region_image, options);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1530,9 +1530,11 @@ impl Engine {
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
|
||||
if let LayerData::Vector { ref mut shapes } = layer.data {
|
||||
layer.dirty = true;
|
||||
return Some(shapes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ pub enum AiProvider {
|
||||
|
||||
impl AiProvider {
|
||||
/// Return the default base URL for this provider.
|
||||
pub fn default_url(&self) -> &str {
|
||||
pub fn _default_url(&self) -> &str {
|
||||
match self {
|
||||
AiProvider::Ollama => "http://localhost:11434",
|
||||
AiProvider::Claude => "https://api.anthropic.com",
|
||||
@@ -101,7 +101,7 @@ impl AiProvider {
|
||||
}
|
||||
|
||||
/// Return the display name.
|
||||
pub fn display_name(&self) -> &str {
|
||||
pub fn _display_name(&self) -> &str {
|
||||
match self {
|
||||
AiProvider::Ollama => "Ollama",
|
||||
AiProvider::Claude => "Claude",
|
||||
@@ -120,7 +120,7 @@ pub struct AiChatConfig {
|
||||
/// Model name to use for chat completions.
|
||||
pub model: String,
|
||||
/// Maximum conversation turns before auto-truncation.
|
||||
pub max_turns: u32,
|
||||
pub _max_turns: u32,
|
||||
/// Whether to display reasoning/thinking content from the LLM.
|
||||
pub reasoning_enabled: bool,
|
||||
/// Whether to send canvas context (dimensions, layers) with each request.
|
||||
@@ -133,7 +133,7 @@ impl Default for AiChatConfig {
|
||||
provider: AiProvider::Ollama,
|
||||
base_url: "http://localhost:11434".to_string(),
|
||||
model: "qwen2.5:latest".to_string(),
|
||||
max_turns: 20,
|
||||
_max_turns: 20,
|
||||
reasoning_enabled: false,
|
||||
canvas_context: true,
|
||||
}
|
||||
@@ -156,7 +156,7 @@ pub struct AiChatState {
|
||||
/// Current reasoning/thinking text being streamed.
|
||||
pub current_reasoning: String,
|
||||
/// Available models fetched from the endpoint.
|
||||
pub available_models: Vec<String>,
|
||||
pub _available_models: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for AiChatState {
|
||||
@@ -168,7 +168,7 @@ impl Default for AiChatState {
|
||||
is_streaming: false,
|
||||
current_response: String::new(),
|
||||
current_reasoning: String::new(),
|
||||
available_models: Vec::new(),
|
||||
_available_models: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,10 +152,10 @@ pub struct HcieIcedApp {
|
||||
/// Pending automated or keyboard-triggered full-viewport screenshot.
|
||||
pub screenshot_request: Option<crate::cli::ScreenshotRequest>,
|
||||
/// New Image dialog state.
|
||||
pub dialog_input: String,
|
||||
pub dialog_input2: String,
|
||||
pub show_performance: bool,
|
||||
pub ui_scale: f32,
|
||||
pub _dialog_input: String,
|
||||
pub _dialog_input2: String,
|
||||
pub _show_performance: bool,
|
||||
pub _ui_scale: f32,
|
||||
pub modifiers: iced::keyboard::Modifiers,
|
||||
pub dialog_new_name: String,
|
||||
pub dialog_new_width: u32,
|
||||
@@ -280,7 +280,7 @@ pub struct HcieIcedApp {
|
||||
/// Whether the dock profile dialog is visible.
|
||||
pub show_dock_profile_dialog: bool,
|
||||
/// Current UI language for internationalization.
|
||||
pub language: i18n::Language,
|
||||
pub _language: i18n::Language,
|
||||
/// Context menu screen position (x, y). None means closed.
|
||||
pub canvas_context_menu: Option<(f32, f32)>,
|
||||
/// Dirty flag set whenever fg/bg/recent colors change, so colors can be
|
||||
@@ -1356,10 +1356,10 @@ impl HcieIcedApp {
|
||||
sub_tools_open: false,
|
||||
sidebar_expanded: settings.panel_layout.sidebar_expanded,
|
||||
slot_last_used: vec![0; 20],
|
||||
dialog_input: String::new(),
|
||||
dialog_input2: String::new(),
|
||||
show_performance: false,
|
||||
ui_scale: 1.0,
|
||||
_dialog_input: String::new(),
|
||||
_dialog_input2: String::new(),
|
||||
_show_performance: false,
|
||||
_ui_scale: 1.0,
|
||||
modifiers: iced::keyboard::Modifiers::default(),
|
||||
canvas_context_menu: None,
|
||||
settings: settings.clone(),
|
||||
@@ -1434,7 +1434,7 @@ impl HcieIcedApp {
|
||||
dock_profile_rename_idx: None,
|
||||
dock_profile_rename_buf: String::new(),
|
||||
show_dock_profile_dialog: false,
|
||||
language: i18n::Language::default(),
|
||||
_language: i18n::Language::default(),
|
||||
colors_dirty: false,
|
||||
vector_shapes_snapshot: None,
|
||||
vector_drag_last: None,
|
||||
@@ -1582,7 +1582,7 @@ impl HcieIcedApp {
|
||||
///
|
||||
/// The canvas image is centered in the pane, with `pan_offset` as an
|
||||
/// 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 zoom = doc.zoom;
|
||||
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.
|
||||
if let Some(rect) = dirty_rect.filter(|_| !is_full_copy) {
|
||||
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 = x0 as usize;
|
||||
let y0 = y0 as usize;
|
||||
@@ -5383,6 +5383,20 @@ impl HcieIcedApp {
|
||||
self.documents[self.active_doc].vector_drag_preview = 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(
|
||||
&session,
|
||||
(x, y),
|
||||
@@ -5399,6 +5413,7 @@ impl HcieIcedApp {
|
||||
self.documents[self.active_doc].vector_drag_preview = Some(shape);
|
||||
self.documents[self.active_doc].modified = true;
|
||||
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_angle = Some(angle);
|
||||
self.settings.tool_settings.vector_angle = angle.to_degrees();
|
||||
@@ -5485,12 +5500,32 @@ impl HcieIcedApp {
|
||||
}
|
||||
// ── Vector shape apply / cancel / flip ──────
|
||||
Message::VectorShapeApply => {
|
||||
// Commit edits: clear snapshot and deselect.
|
||||
let doc = &mut self.documents[self.active_doc];
|
||||
doc.selected_vector_shape = None;
|
||||
doc.vector_cycle_index = 0;
|
||||
doc.vector_edit_handle = hcie_engine_api::VectorEditHandle::None;
|
||||
doc.vector_drag_preview = None;
|
||||
// Commit any pending drag preview to the engine before deselecting,
|
||||
// then clear the snapshot so Cancel cannot revert.
|
||||
let need_refresh = {
|
||||
let doc = &mut self.documents[self.active_doc];
|
||||
if let Some(shape) = doc.vector_drag_preview.take() {
|
||||
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_drag_last = None;
|
||||
self.vector_drag_session = None;
|
||||
@@ -5510,6 +5545,7 @@ impl HcieIcedApp {
|
||||
self.vector_drag_session = None;
|
||||
doc.engine.mark_composite_dirty();
|
||||
self.refresh_composite_if_needed();
|
||||
return Task::perform(async {}, |_| Message::CompositeRefresh);
|
||||
}
|
||||
}
|
||||
Message::VectorShapeFlipH => {
|
||||
@@ -8230,7 +8266,7 @@ mod cycle_one_ux_tests {
|
||||
|
||||
/// Find the topmost text layer whose rasterized pixels contain the point
|
||||
/// `(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 uy = y.round().clamp(0.0, u32::MAX as f32) as u32;
|
||||
let infos = engine.layer_infos();
|
||||
|
||||
@@ -537,7 +537,7 @@ pub fn import_abr_with_prefix(data: &[u8], prefix: &str) -> Vec<BrushPreset> {
|
||||
///
|
||||
/// # Returns
|
||||
/// 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 zip::ZipArchive;
|
||||
|
||||
|
||||
@@ -60,11 +60,11 @@ struct OverlayProgram {
|
||||
/// Active selection transform (for drawing handles).
|
||||
selection_transform: Option<SelectionTransform>,
|
||||
/// Current transform handle being hovered/dragged.
|
||||
active_handle: TransformHandle,
|
||||
_active_handle: TransformHandle,
|
||||
/// Crop tool state (for drawing crop overlay).
|
||||
crop_state: Option<CropState>,
|
||||
/// 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).
|
||||
pressure: f32,
|
||||
/// Active tool (drives vector shape preview geometry).
|
||||
@@ -82,11 +82,11 @@ struct OverlayProgram {
|
||||
/// Polygon selection points accumulated during click-by-click (canvas-space).
|
||||
polygon_points: Vec<(u32, u32)>,
|
||||
/// 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.
|
||||
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.
|
||||
quick_mask: bool,
|
||||
_quick_mask: bool,
|
||||
/// Active brush diameter in canvas pixels for the footprint cursor.
|
||||
brush_size: f32,
|
||||
/// 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
|
||||
let dx = x1 - x0;
|
||||
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 arrow_angle = std::f32::consts::PI / 6.0;
|
||||
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)
|
||||
if let Some((x1, y1, x2, y2)) = self.selected_vector_bounds {
|
||||
self.draw_vector_edit_handles(
|
||||
@@ -1624,12 +1669,17 @@ pub fn view<'a>(
|
||||
)
|
||||
} else if let Some(idx) = doc.selected_vector_shape {
|
||||
let layer_id = doc.engine.active_layer_id();
|
||||
if let Some(shapes) = doc.engine.active_vector_shapes() {
|
||||
if let Some(shape) = shapes.get(idx) {
|
||||
let shapes = doc.engine.active_vector_shapes();
|
||||
// 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 angle = doc.engine.vector_shape_angle(layer_id, idx);
|
||||
(Some((x1, y1, x2, y2)), angle, None)
|
||||
} else {
|
||||
// Stale index — silently show nothing until the next valid click
|
||||
(None, 0.0, None)
|
||||
}
|
||||
} else {
|
||||
@@ -1648,9 +1698,9 @@ pub fn view<'a>(
|
||||
selection_bounds: doc.selection_bounds,
|
||||
vector_draw: doc.vector_draw,
|
||||
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()),
|
||||
marching_ants_offset,
|
||||
_marching_ants_offset: marching_ants_offset,
|
||||
pressure,
|
||||
active_tool: tool_state.active_tool,
|
||||
star_points,
|
||||
@@ -1659,9 +1709,9 @@ pub fn view<'a>(
|
||||
gradient_drag: doc.gradient_drag,
|
||||
lasso_points: doc.lasso_points.clone(),
|
||||
polygon_points: doc.polygon_points.clone(),
|
||||
marching_ants_edges: doc.marching_ants_edges.clone(),
|
||||
selection_fill_spans: doc.selection_fill_spans.clone(),
|
||||
quick_mask: doc.quick_mask,
|
||||
_marching_ants_edges: doc.marching_ants_edges.clone(),
|
||||
_selection_fill_spans: doc.selection_fill_spans.clone(),
|
||||
_quick_mask: doc.quick_mask,
|
||||
brush_size: tool_state.brush_size,
|
||||
selected_vector_bounds: sel_vec_bounds,
|
||||
selected_vector_angle: sel_vec_angle,
|
||||
|
||||
@@ -31,6 +31,7 @@ pub enum ColorPickerTarget {
|
||||
|
||||
/// HSV color representation (Hue, Saturation, Value)
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Hsv {
|
||||
pub h: f32, // 0..360
|
||||
pub s: f32, // 0..1
|
||||
@@ -38,12 +39,14 @@ pub struct Hsv {
|
||||
}
|
||||
|
||||
impl Hsv {
|
||||
#[allow(dead_code)]
|
||||
pub fn to_rgb(&self) -> (u8, u8, u8) {
|
||||
hsv_to_rgb(self.h, self.s, self.v)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert RGB to HSV.
|
||||
#[allow(dead_code)]
|
||||
pub fn rgb_to_hsv(r: u8, g: u8, b: u8) -> Hsv {
|
||||
let r = r 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.
|
||||
#[allow(dead_code)]
|
||||
pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
|
||||
let h = h / 360.0;
|
||||
let (r, g, b) = if s == 0.0 {
|
||||
|
||||
@@ -15,6 +15,7 @@ pub enum DockRole {
|
||||
/// A movable utility panel.
|
||||
Tool,
|
||||
/// Shell UI that never enters the dock manager.
|
||||
#[allow(dead_code)]
|
||||
Fixed,
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ impl PaneType {
|
||||
}
|
||||
|
||||
/// Compact readable label used on narrow auto-hide rails.
|
||||
#[allow(dead_code)]
|
||||
pub fn rail_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::ColorPicker => "Color",
|
||||
|
||||
@@ -40,6 +40,7 @@ pub enum TranslationKey {
|
||||
/// Returns the translated string for the given language and key.
|
||||
///
|
||||
/// Uses match arms to return `&'static str` — no allocations at call site.
|
||||
#[allow(dead_code)]
|
||||
pub fn t(lang: Language, key: TranslationKey) -> &'static str {
|
||||
match lang {
|
||||
Language::Turkish => match key {
|
||||
|
||||
@@ -4,3 +4,4 @@ pub mod cli;
|
||||
pub mod raster;
|
||||
pub mod selection;
|
||||
pub mod vector_edit;
|
||||
pub mod widgets;
|
||||
|
||||
@@ -24,6 +24,7 @@ mod sidebar;
|
||||
mod theme;
|
||||
mod vector_edit;
|
||||
mod viewer;
|
||||
mod widgets;
|
||||
|
||||
use app::HcieIcedApp;
|
||||
|
||||
|
||||
@@ -778,7 +778,7 @@ pub fn view(
|
||||
.padding([2, 8]);
|
||||
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)
|
||||
.into()
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use crate::app::Message;
|
||||
use crate::panels::styles;
|
||||
use crate::theme::ThemeColors;
|
||||
use iced::widget::{column, container, horizontal_rule, scrollable, text};
|
||||
use iced::widget::{column, container, scrollable, text};
|
||||
use iced::{Element, Length};
|
||||
|
||||
/// Build the history panel element.
|
||||
|
||||
@@ -229,6 +229,7 @@ fn menu_item_row(
|
||||
/// All menu definitions matching Photopea's menu structure exactly.
|
||||
#[derive(Clone)]
|
||||
struct MenuDef {
|
||||
#[allow(dead_code)]
|
||||
label: String,
|
||||
items: Vec<MenuItem>,
|
||||
}
|
||||
|
||||
@@ -10,10 +10,9 @@ use crate::color_picker::ColorPickerTarget;
|
||||
use crate::panels::styles;
|
||||
use crate::panels::typography::{BODY, SECTION};
|
||||
use crate::theme::ThemeColors;
|
||||
use crate::widgets::plain_slider;
|
||||
use hcie_engine_api::{BlendMode, LayerType, Tool, VectorShape};
|
||||
use iced::widget::{
|
||||
button, checkbox, column, container, horizontal_rule, row, scrollable, slider, text,
|
||||
};
|
||||
use iced::widget::{button, checkbox, column, container, horizontal_rule, row, scrollable, text};
|
||||
use iced::{Element, Length};
|
||||
|
||||
/// Human-readable label for a shape at a given index.
|
||||
@@ -200,11 +199,15 @@ fn layer_info_section<'a>(
|
||||
layer_id: u64,
|
||||
_colors: ThemeColors,
|
||||
) -> Element<'a, Message> {
|
||||
let opacity_slider = slider(0.0..=1.0, opacity, move |v| {
|
||||
Message::LayerSetOpacity(layer_id, v)
|
||||
})
|
||||
.step(0.01)
|
||||
.width(Length::Fill);
|
||||
let opacity_slider = plain_slider(
|
||||
"Opacity",
|
||||
opacity,
|
||||
0.0..=1.0,
|
||||
0.01,
|
||||
"%",
|
||||
0,
|
||||
move |v| Message::LayerSetOpacity(layer_id, v),
|
||||
);
|
||||
|
||||
column![
|
||||
text("Layer Info").size(SECTION),
|
||||
@@ -213,18 +216,11 @@ fn layer_info_section<'a>(
|
||||
text(name).size(BODY),
|
||||
]
|
||||
.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,
|
||||
row![
|
||||
text("Visible").size(BODY).width(Length::Fixed(58.0)),
|
||||
checkbox("", visible)
|
||||
.on_toggle(move |v| Message::LayerToggleVisibility(layer_id))
|
||||
.on_toggle(move |_v| Message::LayerToggleVisibility(layer_id))
|
||||
.size(11),
|
||||
]
|
||||
.spacing(4)
|
||||
@@ -289,44 +285,42 @@ fn vector_select_properties<'a>(
|
||||
text("Shape Properties").size(SECTION),
|
||||
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("Stroke").size(BODY),
|
||||
text(format!("{:.1}", stroke)).size(BODY)
|
||||
]
|
||||
.spacing(4),
|
||||
slider(0.0..=50.0, stroke, move |v| Message::VectorStrokeChanged(v))
|
||||
.step(0.5)
|
||||
.width(Length::Fill),
|
||||
row![
|
||||
text("Opacity").size(BODY),
|
||||
text(format!("{:.0}%", opacity * 100.0)).size(BODY)
|
||||
]
|
||||
.spacing(4),
|
||||
slider(0.0..=1.0, opacity, move |v| Message::VectorOpacityChanged(
|
||||
v
|
||||
))
|
||||
.step(0.01)
|
||||
.width(Length::Fill),
|
||||
row![
|
||||
text("Hardness").size(BODY),
|
||||
text(format!("{:.0}%", hardness * 100.0)).size(BODY)
|
||||
]
|
||||
.spacing(4),
|
||||
slider(0.0..=1.0, hardness, move |v| {
|
||||
Message::GeometryHardnessChanged(v)
|
||||
})
|
||||
.step(0.01)
|
||||
.width(Length::Fill),
|
||||
row![
|
||||
text("Angle").size(BODY),
|
||||
text(format!("{:.0}°", vector_angle)).size(BODY)
|
||||
]
|
||||
.spacing(4),
|
||||
slider(-180.0..=180.0, vector_angle, move |v| {
|
||||
Message::VectorAngleChanged(v)
|
||||
})
|
||||
.step(1.0)
|
||||
.width(Length::Fill),
|
||||
plain_slider(
|
||||
"Stroke",
|
||||
stroke,
|
||||
0.0..=50.0,
|
||||
0.5,
|
||||
"px",
|
||||
1,
|
||||
move |v| Message::VectorStrokeChanged(v),
|
||||
),
|
||||
plain_slider(
|
||||
"Opacity",
|
||||
opacity,
|
||||
0.0..=1.0,
|
||||
0.01,
|
||||
"%",
|
||||
0,
|
||||
move |v| Message::VectorOpacityChanged(v),
|
||||
),
|
||||
plain_slider(
|
||||
"Hardness",
|
||||
hardness,
|
||||
0.0..=1.0,
|
||||
0.01,
|
||||
"%",
|
||||
0,
|
||||
move |v| Message::GeometryHardnessChanged(v),
|
||||
),
|
||||
plain_slider(
|
||||
"Angle",
|
||||
vector_angle,
|
||||
-180.0..=180.0,
|
||||
1.0,
|
||||
"°",
|
||||
0,
|
||||
move |v| Message::VectorAngleChanged(v),
|
||||
),
|
||||
row![
|
||||
text("Fill").size(BODY),
|
||||
checkbox("", filled)
|
||||
@@ -380,22 +374,24 @@ fn vector_tool_properties<'a>(
|
||||
) -> Element<'a, Message> {
|
||||
let mut col = column![
|
||||
text("Vector Shape").size(11),
|
||||
row![
|
||||
text("Stroke").size(10),
|
||||
text(format!("{:.1}", stroke)).size(10)
|
||||
]
|
||||
.spacing(4),
|
||||
slider(0.0..=50.0, stroke, |v| Message::VectorStrokeChanged(v))
|
||||
.step(0.5)
|
||||
.width(Length::Fill),
|
||||
row![
|
||||
text("Opacity").size(10),
|
||||
text(format!("{:.0}%", opacity * 100.0)).size(10)
|
||||
]
|
||||
.spacing(4),
|
||||
slider(0.0..=1.0, opacity, |v| Message::VectorOpacityChanged(v))
|
||||
.step(0.01)
|
||||
.width(Length::Fill),
|
||||
plain_slider(
|
||||
"Stroke",
|
||||
stroke,
|
||||
0.0..=50.0,
|
||||
0.5,
|
||||
"px",
|
||||
1,
|
||||
|v| Message::VectorStrokeChanged(v),
|
||||
),
|
||||
plain_slider(
|
||||
"Opacity",
|
||||
opacity,
|
||||
0.0..=1.0,
|
||||
0.01,
|
||||
"%",
|
||||
0,
|
||||
|v| Message::VectorOpacityChanged(v),
|
||||
),
|
||||
row![
|
||||
text("Fill").size(10),
|
||||
checkbox("", fill)
|
||||
@@ -426,45 +422,37 @@ fn vector_tool_properties<'a>(
|
||||
// Shape-specific controls
|
||||
match tool {
|
||||
Tool::VectorRect => {
|
||||
col = col.push(
|
||||
row![
|
||||
text("Radius").size(10),
|
||||
text(format!("{:.1}", radius)).size(10)
|
||||
]
|
||||
.spacing(4),
|
||||
);
|
||||
col = col.push(
|
||||
slider(0.0..=100.0, radius, |v| Message::VectorRadiusChanged(v))
|
||||
.step(1.0)
|
||||
.width(Length::Fill),
|
||||
);
|
||||
col = col.push(plain_slider(
|
||||
"Radius",
|
||||
radius,
|
||||
0.0..=100.0,
|
||||
1.0,
|
||||
"px",
|
||||
1,
|
||||
|v| Message::VectorRadiusChanged(v),
|
||||
));
|
||||
}
|
||||
Tool::VectorStar => {
|
||||
col = col.push(
|
||||
row![
|
||||
text("Points").size(10),
|
||||
text(format!("{}", points)).size(10)
|
||||
]
|
||||
.spacing(4),
|
||||
);
|
||||
col = col.push(
|
||||
slider(3.0..=20.0, points as f32, |v| {
|
||||
Message::VectorPointsChanged(v as u32)
|
||||
})
|
||||
.step(1.0)
|
||||
.width(Length::Fill),
|
||||
);
|
||||
col = col.push(plain_slider(
|
||||
"Points",
|
||||
points as f32,
|
||||
3.0..=20.0,
|
||||
1.0,
|
||||
"",
|
||||
0,
|
||||
|v| Message::VectorPointsChanged(v as u32),
|
||||
));
|
||||
}
|
||||
Tool::VectorPolygon => {
|
||||
col = col
|
||||
.push(row![text("Sides").size(10), text(format!("{}", sides)).size(10)].spacing(4));
|
||||
col = col.push(
|
||||
slider(3.0..=12.0, sides as f32, |v| {
|
||||
Message::VectorSidesChanged(v as u32)
|
||||
})
|
||||
.step(1.0)
|
||||
.width(Length::Fill),
|
||||
);
|
||||
col = col.push(plain_slider(
|
||||
"Sides",
|
||||
sides as f32,
|
||||
3.0..=12.0,
|
||||
1.0,
|
||||
"",
|
||||
0,
|
||||
|v| Message::VectorSidesChanged(v as u32),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -488,32 +476,35 @@ fn brush_properties<'a>(
|
||||
hardness: f32,
|
||||
_colors: ThemeColors,
|
||||
) -> 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![
|
||||
text("Brush Properties").size(11),
|
||||
row![text("Size").size(10), text(format!("{:.0}", size)).size(10)].spacing(4),
|
||||
size_slider,
|
||||
row![
|
||||
text("Opacity").size(10),
|
||||
text(format!("{:.0}%", opacity * 100.0)).size(10)
|
||||
]
|
||||
.spacing(4),
|
||||
opacity_slider,
|
||||
row![
|
||||
text("Hardness").size(10),
|
||||
text(format!("{:.0}%", hardness * 100.0)).size(10)
|
||||
]
|
||||
.spacing(4),
|
||||
hardness_slider,
|
||||
plain_slider(
|
||||
"Size",
|
||||
size,
|
||||
1.0..=200.0,
|
||||
1.0,
|
||||
"px",
|
||||
0,
|
||||
|v| Message::BrushSizeChanged(v),
|
||||
),
|
||||
plain_slider(
|
||||
"Opacity",
|
||||
opacity,
|
||||
0.0..=1.0,
|
||||
0.01,
|
||||
"%",
|
||||
0,
|
||||
|v| Message::BrushOpacityChanged(v),
|
||||
),
|
||||
plain_slider(
|
||||
"Hardness",
|
||||
hardness,
|
||||
0.0..=1.0,
|
||||
0.01,
|
||||
"%",
|
||||
0,
|
||||
|v| Message::BrushHardnessChanged(v),
|
||||
),
|
||||
]
|
||||
.spacing(4)
|
||||
.into()
|
||||
|
||||
@@ -106,6 +106,7 @@ pub fn panel_background(colors: ThemeColors) -> iced::widget::container::Style {
|
||||
}
|
||||
|
||||
/// Panel header — #333333 (Photopea header color).
|
||||
#[allow(dead_code)]
|
||||
pub fn panel_header(colors: ThemeColors) -> iced::widget::container::Style {
|
||||
iced::widget::container::Style {
|
||||
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>(
|
||||
doc_name: &'a str,
|
||||
modified: bool,
|
||||
zoom: f32,
|
||||
canvas_w: u32,
|
||||
canvas_h: u32,
|
||||
_zoom: f32,
|
||||
_canvas_w: u32,
|
||||
_canvas_h: u32,
|
||||
colors: ThemeColors,
|
||||
active_menu: Option<usize>,
|
||||
) -> Element<'a, Message> {
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::app::{Message, ToolState};
|
||||
use crate::panels::styles;
|
||||
use crate::theme::ThemeColors;
|
||||
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};
|
||||
|
||||
/// Build the Photopea-style tool options bar.
|
||||
@@ -25,8 +25,8 @@ use iced::{Element, Length};
|
||||
/// - Move: Auto-Select
|
||||
/// - Text: Font, Size, Color
|
||||
/// - Gradient: Mode, Opacity
|
||||
pub fn view<'a>(tool_state: &'a ToolState, colors: ThemeColors) -> Element<'a, Message> {
|
||||
let tool_name = text(tool_name_label(tool_state.active_tool))
|
||||
pub fn _view<'a>(tool_state: &'a ToolState, colors: ThemeColors) -> Element<'a, Message> {
|
||||
let tool_name = text(_tool_name_label(tool_state.active_tool))
|
||||
.size(11)
|
||||
.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
||||
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.
|
||||
fn tool_name_label(tool: Tool) -> &'static str {
|
||||
fn _tool_name_label(tool: Tool) -> &'static str {
|
||||
match tool {
|
||||
Tool::Move => "Move Tool",
|
||||
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).
|
||||
#[allow(dead_code)]
|
||||
fn checkbox_label(label: &str, checked: bool) -> Element<'static, Message> {
|
||||
let mark = if checked { "☑" } else { "☐" };
|
||||
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.
|
||||
#[allow(dead_code)]
|
||||
fn small_button(label: &str, colors: ThemeColors) -> Element<'static, Message> {
|
||||
let bg_hover = colors.bg_hover;
|
||||
let bg_panel = colors.bg_panel;
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::settings::AppSettings;
|
||||
use crate::theme::ThemeColors;
|
||||
use hcie_engine_api::Tool;
|
||||
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};
|
||||
|
||||
@@ -309,10 +309,10 @@ fn text_settings(
|
||||
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_content = text_draft.map(|d| d.content.clone()).unwrap_or_default();
|
||||
let alignment = text_draft
|
||||
let _alignment = text_draft
|
||||
.map(|d| format!("{:?}", d.alignment))
|
||||
.unwrap_or_else(|| "Left".to_string());
|
||||
let orientation = text_draft
|
||||
let _orientation = text_draft
|
||||
.map(|d| format!("{:?}", d.orientation))
|
||||
.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> {
|
||||
let c = colors;
|
||||
let l = label.to_string();
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
pub mod executor;
|
||||
pub mod parser;
|
||||
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
|
||||
/// * `canvas_w` - Width of the canvas in pixels
|
||||
/// * `canvas_h` - Height of the canvas in pixels
|
||||
#[allow(dead_code)]
|
||||
pub fn composite_floating_to_layer(
|
||||
layer_pixels: &mut [u8],
|
||||
floating: &SelectionTransform,
|
||||
|
||||
@@ -339,6 +339,7 @@ impl TransformHandle {
|
||||
];
|
||||
|
||||
/// Get cursor icon hint for this handle.
|
||||
#[allow(dead_code)]
|
||||
pub fn cursor_hint(&self) -> &'static str {
|
||||
match self {
|
||||
TransformHandle::None => "default",
|
||||
|
||||
@@ -22,6 +22,7 @@ use iced::{Element, Length};
|
||||
pub struct ToolSlot {
|
||||
pub tools: &'static [Tool],
|
||||
pub label: &'static str,
|
||||
#[allow(dead_code)]
|
||||
pub icon: &'static str,
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ impl ThemeState {
|
||||
/// **Returns:** The preset used to produce the current color tokens.
|
||||
/// **Logic & Workflow:** Reads the preset from the same lock as its resolved colors.
|
||||
/// **Side Effects / Dependencies:** Briefly locks shared theme state.
|
||||
#[allow(dead_code)]
|
||||
pub fn preset(&self) -> ThemePreset {
|
||||
self.state.lock().unwrap().0
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ impl ViewerState {
|
||||
}
|
||||
|
||||
/// Get subdirectories of a path, cached to avoid repeated disk reads.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_subdirs(&mut self, path: &Path) -> &[PathBuf] {
|
||||
if !self.subdirs_cache.contains_key(path) {
|
||||
let mut dirs = Vec::new();
|
||||
|
||||
@@ -154,6 +154,7 @@ fn thumbnail_card(
|
||||
}
|
||||
|
||||
/// Count how many thumbnails need loading.
|
||||
#[allow(dead_code)]
|
||||
pub fn count_unloaded(state: &ViewerState) -> usize {
|
||||
state
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ fn main() {
|
||||
&alpha, CANVAS, CANVAS,
|
||||
s.depth, s.size, s.soften, 120.0, s.altitude,
|
||||
&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();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use image::GenericImageView;
|
||||
|
||||
fn main() {
|
||||
let ref_img = image::open("_images/_psd_stil_test/test_2/Emboss.png")
|
||||
.unwrap()
|
||||
|
||||
@@ -127,7 +127,7 @@ fn main() {
|
||||
&rect_alpha, unsafe { CANVAS }, unsafe { CANVAS },
|
||||
s.depth, s.size, s.soften, s.angle, 30.0,
|
||||
&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();
|
||||
|
||||
+46
@@ -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
|
||||
Reference in New Issue
Block a user