From 7bedc993b73d2c00866912c3de7e4b31f74a9ebf Mon Sep 17 00:00:00 2001 From: marclaliberte Date: Fri, 9 Oct 2026 08:29:18 -0400 Subject: [PATCH 1/3] refactor(ui): extract the section visibility toggle The eye button that hides a panel's adjustments moves from CollapsibleSection into a reusable VisibilityToggle, together with the dimmed-content class, so adjustment tools can use the same control. CollapsibleSection also types its props and hover timeout. Co-Authored-By: Claude Opus 5.5 --- src/components/ui/CollapsibleSection.tsx | 43 ++++++++---------------- src/components/ui/VisibilityToggle.tsx | 35 +++++++++++++++++++ 2 files changed, 49 insertions(+), 29 deletions(-) create mode 100644 src/components/ui/VisibilityToggle.tsx diff --git a/src/components/ui/CollapsibleSection.tsx b/src/components/ui/CollapsibleSection.tsx index 3296621e37..e16b950f6e 100644 --- a/src/components/ui/CollapsibleSection.tsx +++ b/src/components/ui/CollapsibleSection.tsx @@ -1,19 +1,19 @@ -import { useRef, useState } from 'react'; -import { ChevronDown, Eye, EyeOff } from 'lucide-react'; +import { MouseEvent, ReactNode, useRef, useState } from 'react'; +import { ChevronDown } from 'lucide-react'; import clsx from 'clsx'; -import { useTranslation } from 'react-i18next'; import Text from './Text'; +import VisibilityToggle, { HIDDEN_CONTENT_CLASS } from './VisibilityToggle'; import { TextVariants, TextWeights } from '../../types/typography'; import { useCollapsibleHeight } from '../../hooks/useCollapsibleHeight'; interface CollapsibleSectionProps { canToggleVisibility?: boolean; - children: any; + children: ReactNode; isContentVisible: boolean; isOpen: boolean; - onContextMenu?: any; - onToggle: any; - onToggleVisibility?: any; + onContextMenu?(event: MouseEvent): void; + onToggle(): void; + onToggleVisibility?(): void; title: string; } @@ -27,10 +27,9 @@ export default function CollapsibleSection({ onToggleVisibility = () => {}, title, }: CollapsibleSectionProps) { - const { t } = useTranslation(); const { contentRef, wrapperRef } = useCollapsibleHeight(isOpen); const [isHovering, setIsHovering] = useState(false); - const hoverTimeoutRef = useRef(null); + const hoverTimeoutRef = useRef | null>(null); const handleMouseEnter = () => { if (!canToggleVisibility) { @@ -49,11 +48,6 @@ export default function CollapsibleSection({ setIsHovering(false); }; - const handleVisibilityClick = (e: any) => { - e.stopPropagation(); - onToggleVisibility(); - }; - return (
{canToggleVisibility && (
- + isVisible={isContentVisible} + onToggle={onToggleVisibility} + />
)}
@@ -92,10 +80,7 @@ export default function CollapsibleSection({
{children} diff --git a/src/components/ui/VisibilityToggle.tsx b/src/components/ui/VisibilityToggle.tsx new file mode 100644 index 0000000000..660e09286d --- /dev/null +++ b/src/components/ui/VisibilityToggle.tsx @@ -0,0 +1,35 @@ +import { Eye, EyeOff } from 'lucide-react'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; + +export const HIDDEN_CONTENT_CLASS = 'opacity-30 pointer-events-none'; + +interface VisibilityToggleProps { + className?: string; + isVisible: boolean; + onToggle(): void; + revealOnHover?: boolean; +} + +export default function VisibilityToggle({ className, isVisible, onToggle, revealOnHover }: VisibilityToggleProps) { + const { t } = useTranslation(); + + return ( + + ); +} From f941d8eef86b3581b12113c532af706a091f9c95 Mon Sep 17 00:00:00 2001 From: marclaliberte Date: Fri, 9 Oct 2026 08:29:18 -0400 Subject: [PATCH 2/3] feat(adjustments): add a visibility toggle to every adjustment tool Every tool in the Color, Details and Effects panels gets its own eye toggle, as do Lens Blur, Relight and Fog inside Spatial. Hiding a tool bypasses its effect while keeping its settings and controls. The frontend resolves all the eyes into a flat activeTools map (tool id -> yes/no, missing means active) stored on the edit and on each mask, and the engine only checks is_tool_active for each tool, so it knows nothing about how panels and menus are arranged. Panel and Spatial eyes switch all of their tools at once. Edits saved before activeTools existed still render exactly as before: the frontend converts their sectionVisibility flags when they are opened, and the engine falls back to those flags while activeTools is missing, so unopened edits keep their hidden panels in thumbnails and batch exports. Batch auto adjust builds the map from those flags before turning on the tools it writes. Auto adjust and picking a LUT turn on the tools they change, and a panel's reset or paste turns its tools back on. The Spatial effects share one SpatialEffect wrapper, and the Effects slider handlers use SliderChangeEvent instead of any. Co-Authored-By: Claude Opus 5.5 --- src-tauri/src/cache_utils.rs | 26 +- src-tauri/src/file_management.rs | 21 +- src-tauri/src/fog.rs | 8 +- src-tauri/src/image_processing.rs | 206 +++- src-tauri/src/lens_blur.rs | 8 +- src-tauri/src/relight.rs | 8 +- .../adjustments/AdjustmentSubSection.tsx | 17 +- src/components/adjustments/Effects.tsx | 1018 +++++++++-------- src/components/panel/editor/EditorToolbar.tsx | 2 +- src/components/panel/right/ControlsPanel.tsx | 66 +- src/components/panel/right/MasksPanel.tsx | 54 +- src/context/ToolVisibilityContext.ts | 23 + src/hooks/useEditorActions.ts | 10 +- src/utils/adjustments.ts | 83 +- 14 files changed, 837 insertions(+), 713 deletions(-) create mode 100644 src/context/ToolVisibilityContext.ts diff --git a/src-tauri/src/cache_utils.rs b/src-tauri/src/cache_utils.rs index 87a4d50235..f76d04decc 100644 --- a/src-tauri/src/cache_utils.rs +++ b/src-tauri/src/cache_utils.rs @@ -171,13 +171,8 @@ pub fn calculate_patched_warped_hash(adjustments: &serde_json::Value) -> u64 { pub fn calculate_effects_hash(adjustments: &serde_json::Value) -> u64 { let mut hasher = DefaultHasher::new(); - let effects_visible = adjustments - .get("sectionVisibility") - .and_then(|v| v.get("effects")) - .and_then(|s| s.as_bool()) - .unwrap_or(true); - - let blur_enabled = effects_visible && adjustments["lensBlurEnabled"].as_bool().unwrap_or(false); + let blur_enabled = + crate::image_processing::is_effect_enabled(adjustments, "lensBlur", "lensBlurEnabled"); blur_enabled.hash(&mut hasher); if blur_enabled { @@ -198,7 +193,7 @@ pub fn calculate_effects_hash(adjustments: &serde_json::Value) -> u64 { } let relight_enabled = - effects_visible && adjustments["relightEnabled"].as_bool().unwrap_or(false); + crate::image_processing::is_effect_enabled(adjustments, "relight", "relightEnabled"); relight_enabled.hash(&mut hasher); if relight_enabled { @@ -217,7 +212,7 @@ pub fn calculate_effects_hash(adjustments: &serde_json::Value) -> u64 { ); } - let fog_enabled = effects_visible && adjustments["fogEnabled"].as_bool().unwrap_or(false); + let fog_enabled = crate::image_processing::is_effect_enabled(adjustments, "fog", "fogEnabled"); fog_enabled.hash(&mut hasher); if fog_enabled { @@ -293,13 +288,8 @@ pub fn calculate_transform_hash(adjustments: &serde_json::Value) -> u64 { let flip_v = adjustments["flipVertical"].as_bool().unwrap_or(false); flip_v.hash(&mut hasher); - let effects_visible = adjustments - .get("sectionVisibility") - .and_then(|v| v.get("effects")) - .and_then(|s| s.as_bool()) - .unwrap_or(true); - - let blur_enabled = effects_visible && adjustments["lensBlurEnabled"].as_bool().unwrap_or(false); + let blur_enabled = + crate::image_processing::is_effect_enabled(adjustments, "lensBlur", "lensBlurEnabled"); blur_enabled.hash(&mut hasher); if blur_enabled { if let Some(val) = adjustments.get("lensBlurAmount") { @@ -329,7 +319,7 @@ pub fn calculate_transform_hash(adjustments: &serde_json::Value) -> u64 { } let relight_enabled = - effects_visible && adjustments["relightEnabled"].as_bool().unwrap_or(false); + crate::image_processing::is_effect_enabled(adjustments, "relight", "relightEnabled"); relight_enabled.hash(&mut hasher); if relight_enabled { for key in [ @@ -349,7 +339,7 @@ pub fn calculate_transform_hash(adjustments: &serde_json::Value) -> u64 { } } - let fog_enabled = effects_visible && adjustments["fogEnabled"].as_bool().unwrap_or(false); + let fog_enabled = crate::image_processing::is_effect_enabled(adjustments, "fog", "fogEnabled"); fog_enabled.hash(&mut hasher); if fog_enabled { for key in [ diff --git a/src-tauri/src/file_management.rs b/src-tauri/src/file_management.rs index 3946b706e3..dd23fff899 100644 --- a/src-tauri/src/file_management.rs +++ b/src-tauri/src/file_management.rs @@ -2972,23 +2972,22 @@ pub async fn apply_auto_adjustments_to_paths( ); let auto_adjustments_json = auto_results_to_json(&auto_results); + let mut active_tools = + crate::image_processing::active_tools(&existing_metadata.adjustments); + if let (Some(existing_map), Some(auto_map)) = ( existing_metadata.adjustments.as_object_mut(), auto_adjustments_json.as_object(), ) { for (k, v) in auto_map { - if k == "sectionVisibility" { - if let Some(existing_vis_val) = existing_map.get_mut(k) { - if let (Some(existing_vis), Some(auto_vis)) = - (existing_vis_val.as_object_mut(), v.as_object()) - { - for (vis_k, vis_v) in auto_vis { - existing_vis.insert(vis_k.clone(), vis_v.clone()); - } - } - } else { - existing_map.insert(k.clone(), v.clone()); + if k == "activeTools" { + if let Some(auto_tools) = v.as_object() { + active_tools.extend(auto_tools.clone()); } + existing_map.insert( + k.clone(), + serde_json::Value::Object(std::mem::take(&mut active_tools)), + ); } else { existing_map.insert(k.clone(), v.clone()); } diff --git a/src-tauri/src/fog.rs b/src-tauri/src/fog.rs index e8ba44b412..f602834832 100644 --- a/src-tauri/src/fog.rs +++ b/src-tauri/src/fog.rs @@ -76,13 +76,7 @@ pub fn apply_fog<'a>( image: Cow<'a, DynamicImage>, adjustments: &serde_json::Value, ) -> Cow<'a, DynamicImage> { - let effects_visible = adjustments - .get("sectionVisibility") - .and_then(|v| v.get("effects")) - .and_then(|s| s.as_bool()) - .unwrap_or(true); - - if !adjustments["fogEnabled"].as_bool().unwrap_or(false) || !effects_visible { + if !crate::image_processing::is_effect_enabled(adjustments, "fog", "fogEnabled") { return image; } diff --git a/src-tauri/src/image_processing.rs b/src-tauri/src/image_processing.rs index c11c9e90c3..917b2695e8 100644 --- a/src-tauri/src/image_processing.rs +++ b/src-tauri/src/image_processing.rs @@ -2164,6 +2164,71 @@ fn is_section_visible(adjustments: &serde_json::Value, section: &str) -> bool { .unwrap_or(true) } +const LEGACY_SECTION_TOOLS: &[(&str, &[&str])] = &[ + ("basic", &["basic"]), + ("curves", &["curves"]), + ( + "color", + &[ + "whiteBalance", + "colorPresence", + "hue", + "colorGrading", + "colorMixer", + "colorCalibration", + ], + ), + ( + "details", + &[ + "sharpening", + "presence", + "noiseReduction", + "chromaticAberration", + ], + ), + ( + "effects", + &[ + "creative", "lensBlur", "relight", "fog", "lut", "vignette", "grain", + ], + ), +]; + +pub fn is_tool_active(adjustments: &serde_json::Value, tool: &str) -> bool { + match adjustments.get("activeTools") { + Some(active_tools) => active_tools + .get(tool) + .and_then(|active| active.as_bool()) + .unwrap_or(true), + None => { + is_section_visible(adjustments, tool) + && LEGACY_SECTION_TOOLS + .iter() + .find(|(_, tools)| tools.contains(&tool)) + .is_none_or(|(section, _)| is_section_visible(adjustments, section)) + } + } +} + +pub fn active_tools(adjustments: &serde_json::Value) -> serde_json::Map { + match adjustments + .get("activeTools") + .and_then(|tools| tools.as_object()) + { + Some(active_tools) => active_tools.clone(), + None => LEGACY_SECTION_TOOLS + .iter() + .flat_map(|(_, tools)| tools.iter()) + .map(|tool| (tool.to_string(), json!(is_tool_active(adjustments, tool)))) + .collect(), + } +} + +pub fn is_effect_enabled(adjustments: &serde_json::Value, tool: &str, enabled_key: &str) -> bool { + adjustments[enabled_key].as_bool().unwrap_or(false) && is_tool_active(adjustments, tool) +} + fn get_global_adjustments_from_json( js_adjustments: &serde_json::Value, is_raw: bool, @@ -2171,10 +2236,10 @@ fn get_global_adjustments_from_json( baseline_exposure: f32, tonemapper_override: Option, ) -> GlobalAdjustments { - let is_visible = |section: &str| is_section_visible(js_adjustments, section); + let is_visible = |tool: &str| is_tool_active(js_adjustments, tool); - let get_val = |section: &str, key: &str, scale: f32, default: Option| -> f32 { - if is_visible(section) { + let get_val = |tool: &str, key: &str, scale: f32, default: Option| -> f32 { + if is_visible(tool) { js_adjustments[key] .as_f64() .unwrap_or(default.unwrap_or(0.0)) as f32 @@ -2232,6 +2297,9 @@ fn get_global_adjustments_from_json( Vec::new() }; + let color_grading_visible = is_visible("colorGrading"); + let color_mixer_visible = is_visible("colorMixer"); + let cg_obj = js_adjustments .get("colorGrading") .cloned() @@ -2242,7 +2310,7 @@ fn get_global_adjustments_from_json( .cloned() .unwrap_or_default(); - let color_cal_settings = if is_visible("color") { + let color_cal_settings = if is_visible("colorCalibration") { ColorCalibrationSettings { shadows_tint: cal_obj["shadowsTint"].as_f64().unwrap_or(0.0) as f32 / SCALES.color_calibration_hue, @@ -2268,7 +2336,7 @@ fn get_global_adjustments_from_json( let (pipe_to_rendering, rendering_to_pipe) = calculate_agx_matrices(); let rgb_to_lms = white_balance::rgb_to_lms(); - let (has_lut, lut_intensity, lut_is_scene_referred) = if is_visible("effects") { + let (has_lut, lut_intensity, lut_is_scene_referred) = if is_visible("lut") { ( if js_adjustments["lutPath"].is_string() { 1 @@ -2298,69 +2366,69 @@ fn get_global_adjustments_from_json( whites: get_val("basic", "whites", SCALES.whites, None), blacks: get_val("basic", "blacks", SCALES.blacks, None), - saturation: get_val("color", "saturation", SCALES.saturation, None), - vibrance: get_val("color", "vibrance", SCALES.vibrance, None), - hue: get_val("color", "hue", 1.0, None), + saturation: get_val("colorPresence", "saturation", SCALES.saturation, None), + vibrance: get_val("colorPresence", "vibrance", SCALES.vibrance, None), + hue: get_val("hue", "hue", 1.0, None), wb_log_gain_l: white_balance_gains[0], wb_log_gain_m: white_balance_gains[1], wb_log_gain_s: white_balance_gains[2], _pad_color1: 0.0, _pad_color2: 0.0, - sharpness: get_val("details", "sharpness", SCALES.sharpness, None), + sharpness: get_val("sharpening", "sharpness", SCALES.sharpness, None), luma_noise_reduction: get_val( - "details", + "noiseReduction", "lumaNoiseReduction", SCALES.luma_noise_reduction, None, ), color_noise_reduction: get_val( - "details", + "noiseReduction", "colorNoiseReduction", SCALES.color_noise_reduction, None, ), - clarity: get_val("details", "clarity", SCALES.clarity, None), - dehaze: get_val("details", "dehaze", SCALES.dehaze, None), - structure: get_val("details", "structure", SCALES.structure, None), - centré: get_val("details", "centré", SCALES.centré, None), - vignette_amount: get_val("effects", "vignetteAmount", SCALES.vignette_amount, None), + clarity: get_val("presence", "clarity", SCALES.clarity, None), + dehaze: get_val("presence", "dehaze", SCALES.dehaze, None), + structure: get_val("presence", "structure", SCALES.structure, None), + centré: get_val("presence", "centré", SCALES.centré, None), + vignette_amount: get_val("vignette", "vignetteAmount", SCALES.vignette_amount, None), vignette_midpoint: get_val( - "effects", + "vignette", "vignetteMidpoint", SCALES.vignette_midpoint, Some(50.0), ), vignette_roundness: get_val( - "effects", + "vignette", "vignetteRoundness", SCALES.vignette_roundness, Some(0.0), ), vignette_feather: get_val( - "effects", + "vignette", "vignetteFeather", SCALES.vignette_feather, Some(50.0), ), - grain_amount: get_val("effects", "grainAmount", SCALES.grain_amount, None), - grain_size: get_val("effects", "grainSize", SCALES.grain_size, Some(25.0)), + grain_amount: get_val("grain", "grainAmount", SCALES.grain_amount, None), + grain_size: get_val("grain", "grainSize", SCALES.grain_size, Some(25.0)), grain_roughness: get_val( - "effects", + "grain", "grainRoughness", SCALES.grain_roughness, Some(50.0), ), chromatic_aberration_red_cyan: get_val( - "details", + "chromaticAberration", "chromaticAberrationRedCyan", SCALES.chromatic_aberration, None, ), chromatic_aberration_blue_yellow: get_val( - "details", + "chromaticAberration", "chromaticAberrationBlueYellow", SCALES.chromatic_aberration, None, @@ -2395,32 +2463,32 @@ fn get_global_adjustments_from_json( _pad_cg2: 0.0, _pad_cg3: 0.0, _pad_cg4: 0.0, - color_grading_shadows: if is_visible("color") { + color_grading_shadows: if color_grading_visible { parse_color_grade_settings(&cg_obj["shadows"]) } else { ColorGradeSettings::default() }, - color_grading_midtones: if is_visible("color") { + color_grading_midtones: if color_grading_visible { parse_color_grade_settings(&cg_obj["midtones"]) } else { ColorGradeSettings::default() }, - color_grading_highlights: if is_visible("color") { + color_grading_highlights: if color_grading_visible { parse_color_grade_settings(&cg_obj["highlights"]) } else { ColorGradeSettings::default() }, - color_grading_global: if is_visible("color") { + color_grading_global: if color_grading_visible { parse_color_grade_settings(&cg_obj["global"]) } else { ColorGradeSettings::default() }, - color_grading_blending: if is_visible("color") { + color_grading_blending: if color_grading_visible { cg_obj["blending"].as_f64().unwrap_or(50.0) as f32 / SCALES.color_grading_blending } else { 0.5 }, - color_grading_balance: if is_visible("color") { + color_grading_balance: if color_grading_visible { cg_obj["balance"].as_f64().unwrap_or(0.0) as f32 / SCALES.color_grading_balance } else { 0.0 @@ -2430,7 +2498,7 @@ fn get_global_adjustments_from_json( color_calibration: color_cal_settings, - hsl: if is_visible("color") { + hsl: if color_mixer_visible { parse_hsl_adjustments(&js_adjustments.get("hsl").cloned().unwrap_or_default()) } else { [HslColor::default(); 8] @@ -2448,11 +2516,11 @@ fn get_global_adjustments_from_json( _pad_end3: 0.0, _pad_end4: 0.0, - glow_amount: get_val("effects", "glowAmount", SCALES.glow, None), - halation_amount: get_val("effects", "halationAmount", SCALES.halation, None), - flare_amount: get_val("effects", "flareAmount", SCALES.flares, None), + glow_amount: get_val("creative", "glowAmount", SCALES.glow, None), + halation_amount: get_val("creative", "halationAmount", SCALES.halation, None), + flare_amount: get_val("creative", "flareAmount", SCALES.flares, None), sharpness_threshold: get_val( - "details", + "sharpening", "sharpnessThreshold", SCALES.sharpness_threshold, Some(15.0), @@ -2468,10 +2536,10 @@ fn get_mask_adjustments_from_json( return MaskAdjustments::default(); } - let is_visible = |section: &str| is_section_visible(adj, section); + let is_visible = |tool: &str| is_tool_active(adj, tool); - let get_val = |section: &str, key: &str, scale: f32| -> f32 { - if is_visible(section) { + let get_val = |tool: &str, key: &str, scale: f32| -> f32 { + if is_visible(tool) { adj[key].as_f64().unwrap_or(0.0) as f32 / scale } else { 0.0 @@ -2499,8 +2567,11 @@ fn get_mask_adjustments_from_json( } else { Vec::new() }; + let color_grading_visible = is_visible("colorGrading"); + let color_mixer_visible = is_visible("colorMixer"); + let cg_obj = adj.get("colorGrading").cloned().unwrap_or_default(); - let [wb_log_gain_l, wb_log_gain_m, wb_log_gain_s] = if is_visible("color") { + let [wb_log_gain_l, wb_log_gain_m, wb_log_gain_s] = if is_visible("whiteBalance") { white_balance::adaptation_log_gains( global_white_balance, global_white_balance.shifted( @@ -2521,57 +2592,65 @@ fn get_mask_adjustments_from_json( whites: get_val("basic", "whites", SCALES.whites), blacks: get_val("basic", "blacks", SCALES.blacks), - saturation: get_val("color", "saturation", SCALES.saturation), - vibrance: get_val("color", "vibrance", SCALES.vibrance), + saturation: get_val("colorPresence", "saturation", SCALES.saturation), + vibrance: get_val("colorPresence", "vibrance", SCALES.vibrance), - sharpness: get_val("details", "sharpness", SCALES.sharpness), - luma_noise_reduction: get_val("details", "lumaNoiseReduction", SCALES.luma_noise_reduction), + sharpness: get_val("sharpening", "sharpness", SCALES.sharpness), + luma_noise_reduction: get_val( + "noiseReduction", + "lumaNoiseReduction", + SCALES.luma_noise_reduction, + ), color_noise_reduction: get_val( - "details", + "noiseReduction", "colorNoiseReduction", SCALES.color_noise_reduction, ), - clarity: get_val("details", "clarity", SCALES.clarity), - dehaze: get_val("details", "dehaze", SCALES.dehaze), - structure: get_val("details", "structure", SCALES.structure), + clarity: get_val("presence", "clarity", SCALES.clarity), + dehaze: get_val("presence", "dehaze", SCALES.dehaze), + structure: get_val("presence", "structure", SCALES.structure), - glow_amount: get_val("effects", "glowAmount", SCALES.glow), - halation_amount: get_val("effects", "halationAmount", SCALES.halation), - flare_amount: get_val("effects", "flareAmount", SCALES.flares), - sharpness_threshold: get_val("details", "sharpnessThreshold", SCALES.sharpness_threshold), + glow_amount: get_val("creative", "glowAmount", SCALES.glow), + halation_amount: get_val("creative", "halationAmount", SCALES.halation), + flare_amount: get_val("creative", "flareAmount", SCALES.flares), + sharpness_threshold: get_val( + "sharpening", + "sharpnessThreshold", + SCALES.sharpness_threshold, + ), - hue: get_val("color", "hue", 1.0), + hue: get_val("hue", "hue", 1.0), wb_log_gain_l, wb_log_gain_m, wb_log_gain_s, _pad_wb: 0.0, - color_grading_shadows: if is_visible("color") { + color_grading_shadows: if color_grading_visible { parse_color_grade_settings(&cg_obj["shadows"]) } else { ColorGradeSettings::default() }, - color_grading_midtones: if is_visible("color") { + color_grading_midtones: if color_grading_visible { parse_color_grade_settings(&cg_obj["midtones"]) } else { ColorGradeSettings::default() }, - color_grading_highlights: if is_visible("color") { + color_grading_highlights: if color_grading_visible { parse_color_grade_settings(&cg_obj["highlights"]) } else { ColorGradeSettings::default() }, - color_grading_global: if is_visible("color") { + color_grading_global: if color_grading_visible { parse_color_grade_settings(&cg_obj["global"]) } else { ColorGradeSettings::default() }, - color_grading_blending: if is_visible("color") { + color_grading_blending: if color_grading_visible { cg_obj["blending"].as_f64().unwrap_or(50.0) as f32 / SCALES.color_grading_blending } else { 0.5 }, - color_grading_balance: if is_visible("color") { + color_grading_balance: if color_grading_visible { cg_obj["balance"].as_f64().unwrap_or(0.0) as f32 / SCALES.color_grading_balance } else { 0.0 @@ -2579,7 +2658,7 @@ fn get_mask_adjustments_from_json( _pad5: 0.0, _pad6: 0.0, - hsl: if is_visible("color") { + hsl: if color_mixer_visible { parse_hsl_adjustments(&adj.get("hsl").cloned().unwrap_or_default()) } else { [HslColor::default(); 8] @@ -2605,7 +2684,7 @@ pub fn get_all_adjustments_from_json( as_shot: AsShot, render: RenderOptions, ) -> AllAdjustments { - let target_white_balance = if is_section_visible(js_adjustments, "color") { + let target_white_balance = if is_tool_active(js_adjustments, "whiteBalance") { white_balance::from_adjustments(js_adjustments, as_shot.white_balance) } else { as_shot.white_balance @@ -3562,10 +3641,11 @@ pub fn auto_results_to_json(results: &AutoAdjustmentResults) -> serde_json::Valu "centré": results.centre, "dehaze": results.dehaze, - "sectionVisibility": { + "activeTools": { "basic": true, - "color": true, - "effects": true + "colorPresence": true, + "presence": true, + "vignette": true }, "whites": results.whites, "blacks": results.blacks diff --git a/src-tauri/src/lens_blur.rs b/src-tauri/src/lens_blur.rs index d825b59f55..eb10cf55eb 100644 --- a/src-tauri/src/lens_blur.rs +++ b/src-tauri/src/lens_blur.rs @@ -15,13 +15,7 @@ pub fn apply_lens_blur<'a>( image: Cow<'a, DynamicImage>, adjustments: &serde_json::Value, ) -> Cow<'a, DynamicImage> { - let effects_visible = adjustments - .get("sectionVisibility") - .and_then(|v| v.get("effects")) - .and_then(|s| s.as_bool()) - .unwrap_or(true); - - if !adjustments["lensBlurEnabled"].as_bool().unwrap_or(false) || !effects_visible { + if !crate::image_processing::is_effect_enabled(adjustments, "lensBlur", "lensBlurEnabled") { return image; } diff --git a/src-tauri/src/relight.rs b/src-tauri/src/relight.rs index 475be83776..7a584abd34 100644 --- a/src-tauri/src/relight.rs +++ b/src-tauri/src/relight.rs @@ -286,13 +286,7 @@ pub fn apply_relight<'a>( image: Cow<'a, DynamicImage>, adjustments: &serde_json::Value, ) -> Cow<'a, DynamicImage> { - let effects_visible = adjustments - .get("sectionVisibility") - .and_then(|v| v.get("effects")) - .and_then(|s| s.as_bool()) - .unwrap_or(true); - - if !adjustments["relightEnabled"].as_bool().unwrap_or(false) || !effects_visible { + if !crate::image_processing::is_effect_enabled(adjustments, "relight", "relightEnabled") { return image; } diff --git a/src/components/adjustments/AdjustmentSubSection.tsx b/src/components/adjustments/AdjustmentSubSection.tsx index 5f2cf80bda..97c1934ad2 100644 --- a/src/components/adjustments/AdjustmentSubSection.tsx +++ b/src/components/adjustments/AdjustmentSubSection.tsx @@ -3,9 +3,11 @@ import { ChevronDown } from 'lucide-react'; import clsx from 'clsx'; import { useShallow } from 'zustand/react/shallow'; import Text from '../ui/Text'; +import VisibilityToggle, { HIDDEN_CONTENT_CLASS } from '../ui/VisibilityToggle'; import { TextVariants } from '../../types/typography'; import { useSettingsStore } from '../../store/useSettingsStore'; import { useCollapsibleHeight } from '../../hooks/useCollapsibleHeight'; +import { useToolVisibility } from '../../context/ToolVisibilityContext'; import { ADJUSTMENT_SECTION_TOOLS, getAdjustmentSectionToolIds, withAdjustmentLayout } from '../../utils/adjustments'; interface AdjustmentSubSectionProps { @@ -17,6 +19,7 @@ interface AdjustmentSubSectionProps { } export default function AdjustmentSubSection({ actions, children, id, order, title }: AdjustmentSubSectionProps) { + const visibility = useToolVisibility(id); const { appSettings, handleSettingsChange } = useSettingsStore( useShallow((state) => ({ appSettings: state.appSettings, @@ -48,10 +51,15 @@ export default function AdjustmentSubSection({ actions, children, id, order, tit return (
-
- +
+ {title} + {visibility && ( +
+ +
+ )} {actions &&
e.stopPropagation()}>{actions}
}
-
+
{children}
diff --git a/src/components/adjustments/Effects.tsx b/src/components/adjustments/Effects.tsx index 734f35dc33..9cd570c6a6 100644 --- a/src/components/adjustments/Effects.tsx +++ b/src/components/adjustments/Effects.tsx @@ -1,12 +1,13 @@ -import { useState, useEffect, useRef, useMemo } from 'react'; +import { useState, useEffect, useRef, useMemo, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { invoke } from '@tauri-apps/api/core'; import { toast } from 'react-toastify'; import { Loader2, Circle, Hexagon, Octagon, Aperture, Plus, X } from 'lucide-react'; import { motion } from 'framer-motion'; import clsx from 'clsx'; -import Slider from '../ui/Slider'; +import Slider, { SliderChangeEvent } from '../ui/Slider'; import Switch from '../ui/Switch'; +import VisibilityToggle, { HIDDEN_CONTENT_CLASS } from '../ui/VisibilityToggle'; import { Adjustments, Effect, @@ -29,11 +30,12 @@ import { DepthRangePicker } from '../ui/DepthRangePicker'; import { useProcessStore } from '../../store/useProcessStore'; import { useSettingsStore } from '../../store/useSettingsStore'; import { useEditorStore } from '../../store/useEditorStore'; +import { useToolVisibility } from '../../context/ToolVisibilityContext'; interface EffectsPanelProps { adjustments: Adjustments; isForMask?: boolean; - setAdjustments(adjustments: Partial | ((prev: Adjustments) => Adjustments)): any; + setAdjustments(adjustments: Partial | ((prev: Adjustments) => Adjustments)): void; handleLutSelect(path: string, isSceneReferred: boolean): void; onLutHover?: (path: string | null) => void; appSettings: AppSettings | null; @@ -151,6 +153,46 @@ const BokehShapeSwitch = ({ selectedShape, onShapeChange }: BokehShapeSwitchProp ); }; +interface SpatialEffectProps { + children: ReactNode; + enabled: boolean; + id: string; + label: string; + onToggle(enabled: boolean): void; +} + +const SpatialEffect = ({ children, enabled, id, label, onToggle }: SpatialEffectProps) => { + const visibility = useToolVisibility(id, 'spatial'); + + return ( +
+
+ + {enabled && visibility && ( + + )} +
+ +
+
+
+ {children} +
+
+
+
+ ); +}; + export default function EffectsPanel({ adjustments, setAdjustments, @@ -178,7 +220,7 @@ export default function EffectsPanel({ ...prev, lensBlurDepthMap: b64, })); - } catch (e: any) { + } catch (e) { toast.error(`Failed to generate depth map: ${e}`); setAdjustments((prev: Partial) => ({ ...prev, lensBlurEnabled: false })); } finally { @@ -194,7 +236,7 @@ export default function EffectsPanel({ ...prev, relightNormalMap: b64, })); - } catch (e: any) { + } catch (e) { toast.error(`Failed to generate normal map: ${e}`); setAdjustments((prev: Partial) => ({ ...prev, relightEnabled: false })); } finally { @@ -210,7 +252,7 @@ export default function EffectsPanel({ ...prev, fogDepthMap: b64, })); - } catch (e: any) { + } catch (e) { toast.error(`Failed to generate depth map: ${e}`); setAdjustments((prev: Partial) => ({ ...prev, fogEnabled: false })); } finally { @@ -218,8 +260,8 @@ export default function EffectsPanel({ } }; - const handleAdjustmentChange = (key: string, value: any) => { - const numericValue = typeof value === 'boolean' ? value : parseInt(value, 10); + const handleAdjustmentChange = (key: string, value: boolean | number | string) => { + const numericValue = typeof value === 'boolean' ? value : parseInt(String(value), 10); setAdjustments((prev: Partial) => ({ ...prev, [key]: numericValue })); }; @@ -292,6 +334,9 @@ export default function EffectsPanel({ })); }; + const handleLightSliderChange = (key: keyof RelightLight) => (e: SliderChangeEvent) => + handleLightChange(key, parseInt(String(e.target.value), 10)); + const lightColorField = useParsedTextField( activeLight?.color ?? '#ffffff', (color) => handleLightChange('color', color), @@ -324,7 +369,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.glow')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(CreativeAdjustment.GlowAmount, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(CreativeAdjustment.GlowAmount, e.target.value)} step={1} value={adjustments.glowAmount} onDragStateChange={onDragStateChange} @@ -334,7 +379,9 @@ export default function EffectsPanel({ label={t('adjustments.effects.halation')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(CreativeAdjustment.HalationAmount, e.target.value)} + onChange={(e: SliderChangeEvent) => + handleAdjustmentChange(CreativeAdjustment.HalationAmount, e.target.value) + } step={1} value={adjustments.halationAmount} onDragStateChange={onDragStateChange} @@ -345,7 +392,9 @@ export default function EffectsPanel({ label={t('adjustments.effects.lightFlares')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(CreativeAdjustment.FlareAmount, e.target.value)} + onChange={(e: SliderChangeEvent) => + handleAdjustmentChange(CreativeAdjustment.FlareAmount, e.target.value) + } step={1} value={adjustments.flareAmount} onDragStateChange={onDragStateChange} @@ -363,524 +412,499 @@ export default function EffectsPanel({ title={t('adjustments.effects.spatial')} >
-
- - -
-
-
- {isGeneratingDepth ? ( -
-
- - - {aiModelDownloadStatus - ? t('editor.masks.settings.aiModelDownloading') - : t('editor.ai.generatingDepthMap')} - -
- {aiModelDownloadStatus && ( - - {aiModelDownloadStatus} - - )} -
- ) : ( - <> - handleAdjustmentChange(Effect.LensBlurAmount, e.target.value)} - step={1} - value={adjustments.lensBlurAmount ?? 50} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.lensBlurDiffusion, e.target.value)} - step={1} - value={adjustments.lensBlurDiffusion ?? 0} - onDragStateChange={onDragStateChange} - /> - - - setAdjustments((prev: Partial) => ({ - ...prev, - [Effect.LensBlurShape]: shapeId, - })) - } - /> - - { - setAdjustments((prev: Partial) => ({ - ...prev, - lensBlurMinDepth: 100 - values.maxDepth, - lensBlurMaxDepth: 100 - values.minDepth, - lensBlurMinFade: values.maxFade, - lensBlurMaxFade: values.minFade, - })); - }} - onDragStateChange={onDragStateChange} - /> - - )} + + {isGeneratingDepth ? ( +
+
+ + + {aiModelDownloadStatus + ? t('editor.masks.settings.aiModelDownloading') + : t('editor.ai.generatingDepthMap')} +
+ {aiModelDownloadStatus && ( + + {aiModelDownloadStatus} + + )}
-
-
-
- - -
-
-
- {isGeneratingNormals ? ( -
-
- - - {aiModelDownloadStatus - ? t('editor.masks.settings.aiModelDownloading') - : t('editor.ai.generatingNormalMap')} - -
- {aiModelDownloadStatus && ( - - {aiModelDownloadStatus} - - )} + ) : ( + <> + + handleAdjustmentChange(Effect.LensBlurAmount, e.target.value) + } + step={1} + value={adjustments.lensBlurAmount ?? 50} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + + handleAdjustmentChange(Effect.lensBlurDiffusion, e.target.value) + } + step={1} + value={adjustments.lensBlurDiffusion ?? 0} + onDragStateChange={onDragStateChange} + /> + + + setAdjustments((prev: Partial) => ({ + ...prev, + [Effect.LensBlurShape]: shapeId, + })) + } + /> + + { + setAdjustments((prev: Partial) => ({ + ...prev, + lensBlurMinDepth: 100 - values.maxDepth, + lensBlurMaxDepth: 100 - values.minDepth, + lensBlurMinFade: values.maxFade, + lensBlurMaxFade: values.minFade, + })); + }} + onDragStateChange={onDragStateChange} + /> + + )} + + +
+ {isGeneratingNormals ? ( +
+
+ + + {aiModelDownloadStatus + ? t('editor.masks.settings.aiModelDownloading') + : t('editor.ai.generatingNormalMap')} + +
+ {aiModelDownloadStatus && ( + + {aiModelDownloadStatus} + + )} +
+ ) : ( + <> + + handleAdjustmentChange(Effect.RelightAmbient, e.target.value) + } + step={1} + value={adjustments.relightAmbient ?? 0} + onDragStateChange={onDragStateChange} + /> + + + handleAdjustmentChange(Effect.RelightSoftness, e.target.value) + } + step={1} + value={adjustments.relightSoftness ?? 25} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + + handleAdjustmentChange(Effect.RelightShine, e.target.value) + } + step={1} + value={adjustments.relightShine ?? 0} + onDragStateChange={onDragStateChange} + /> + + handleAdjustmentChange(Effect.RelightShadows, enabled)} + /> + + {adjustments.relightShadows && ( + + handleAdjustmentChange(Effect.RelightShadowSoftness, e.target.value) + } + step={1} + value={adjustments.relightShadowSoftness ?? 15} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + )} + +
+
+ {relightLights.map((light, index) => ( +
setHoveredLightId(light.id)} + onMouseLeave={() => setHoveredLightId(null)} + > + + {hoveredLightId === light.id && ( + + )} +
+ ))} +
- ) : ( - <> - handleAdjustmentChange(Effect.RelightAmbient, e.target.value)} - step={1} - value={adjustments.relightAmbient ?? 0} - onDragStateChange={onDragStateChange} - /> - - handleAdjustmentChange(Effect.RelightSoftness, e.target.value)} - step={1} - value={adjustments.relightSoftness ?? 25} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.RelightShine, e.target.value)} - step={1} - value={adjustments.relightShine ?? 0} - onDragStateChange={onDragStateChange} - /> - - handleAdjustmentChange(Effect.RelightShadows, enabled)} - /> - - {adjustments.relightShadows && ( + + {activeLight ? ( + <> +
+ + {t('adjustments.effects.relightType')} + + handleLightChange('type', val)} + /> +
+ + handleLightChange('color', color)} + /> + - handleAdjustmentChange(Effect.RelightShadowSoftness, e.target.value) - } + defaultValue={60} + onChange={handleLightSliderChange('intensity')} step={1} - value={adjustments.relightShadowSoftness ?? 15} + value={activeLight.intensity} onDragStateChange={onDragStateChange} fillOrigin="min" /> - )} - -
-
- {relightLights.map((light, index) => ( -
setHoveredLightId(light.id)} - onMouseLeave={() => setHoveredLightId(null)} - > - - {hoveredLightId === light.id && ( - - )} -
- ))} - -
- {activeLight ? ( + {activeLight.type !== 'directional' && ( <> -
- - {t('adjustments.effects.relightType')} - - handleLightChange('type', val)} - /> -
- - handleLightChange('color', color)} + handleLightChange('intensity', parseInt(e.target.value, 10))} + defaultValue={30} + onChange={handleLightSliderChange('radius')} + step={1} + value={activeLight.radius} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + )} + + {activeLight.type !== 'point' && ( + <> + - {activeLight.type !== 'directional' && ( - <> - handleLightChange('depth', parseInt(e.target.value, 10))} - step={1} - value={activeLight.depth} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleLightChange('radius', parseInt(e.target.value, 10))} - step={1} - value={activeLight.radius} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - )} - - {activeLight.type !== 'point' && ( - <> - handleLightChange('angle', parseInt(e.target.value, 10))} - step={1} - suffix="°" - value={activeLight.angle} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - - handleLightChange('elevation', parseInt(e.target.value, 10)) - } - step={1} - suffix="°" - value={activeLight.elevation} - onDragStateChange={onDragStateChange} - /> - - )} - - {activeLight.type === 'spot' && ( - <> - handleLightChange('cone', parseInt(e.target.value, 10))} - step={1} - value={activeLight.cone} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - - handleLightChange('feather', parseInt(e.target.value, 10)) - } - step={1} - value={activeLight.feather} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - )} + + + )} + {activeLight.type === 'spot' && ( + <> - handleLightChange('temperature', parseInt(e.target.value, 10)) - } + min={0} + defaultValue={40} + onChange={handleLightSliderChange('cone')} step={1} - value={activeLight.temperature} - trackClassName="temperature-gradient-track" + value={activeLight.cone} onDragStateChange={onDragStateChange} + fillOrigin="min" /> handleLightChange('tint', parseInt(e.target.value, 10))} + min={0} + defaultValue={50} + onChange={handleLightSliderChange('feather')} step={1} - value={activeLight.tint} - trackClassName="tint-gradient-track" + value={activeLight.feather} onDragStateChange={onDragStateChange} + fillOrigin="min" /> - ) : ( - - {t('adjustments.effects.relightHint')} - )} -
- - )} -
-
+ + + + + + ) : ( + + {t('adjustments.effects.relightHint')} + + )} +
+ + )}
-
-
- - -
-
-
- {isGeneratingFogDepth ? ( -
-
- - - {aiModelDownloadStatus - ? t('editor.masks.settings.aiModelDownloading') - : t('editor.ai.generatingDepthMap')} - -
- {aiModelDownloadStatus && ( - - {aiModelDownloadStatus} - - )} -
- ) : ( - <> - handleAdjustmentChange(Effect.FogAmount, e.target.value)} - step={1} - value={adjustments.fogAmount ?? 50} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogStart, e.target.value)} - step={1} - value={adjustments.fogStart ?? 0} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogDensity, e.target.value)} - step={1} - value={adjustments.fogDensity ?? 50} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogHeight, e.target.value)} - step={1} - value={adjustments.fogHeight ?? 0} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogVariation, e.target.value)} - step={1} - value={adjustments.fogVariation ?? 25} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogGlow, e.target.value)} - step={1} - value={adjustments.fogGlow ?? 25} - onDragStateChange={onDragStateChange} - fillOrigin="min" - /> - - handleAdjustmentChange(Effect.FogTemperature, e.target.value)} - step={1} - value={adjustments.fogTemperature ?? 0} - trackClassName="temperature-gradient-track" - onDragStateChange={onDragStateChange} - /> - - handleAdjustmentChange(Effect.FogTint, e.target.value)} - step={1} - value={adjustments.fogTint ?? 0} - trackClassName="tint-gradient-track" - onDragStateChange={onDragStateChange} - /> - - )} + + + {isGeneratingFogDepth ? ( +
+
+ + + {aiModelDownloadStatus + ? t('editor.masks.settings.aiModelDownloading') + : t('editor.ai.generatingDepthMap')} +
+ {aiModelDownloadStatus && ( + + {aiModelDownloadStatus} + + )}
-
-
+ ) : ( + <> + handleAdjustmentChange(Effect.FogAmount, e.target.value)} + step={1} + value={adjustments.fogAmount ?? 50} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + handleAdjustmentChange(Effect.FogStart, e.target.value)} + step={1} + value={adjustments.fogStart ?? 0} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + handleAdjustmentChange(Effect.FogDensity, e.target.value)} + step={1} + value={adjustments.fogDensity ?? 50} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + handleAdjustmentChange(Effect.FogHeight, e.target.value)} + step={1} + value={adjustments.fogHeight ?? 0} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + handleAdjustmentChange(Effect.FogVariation, e.target.value)} + step={1} + value={adjustments.fogVariation ?? 25} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + handleAdjustmentChange(Effect.FogGlow, e.target.value)} + step={1} + value={adjustments.fogGlow ?? 25} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + + + handleAdjustmentChange(Effect.FogTemperature, e.target.value) + } + step={1} + value={adjustments.fogTemperature ?? 0} + trackClassName="temperature-gradient-track" + onDragStateChange={onDragStateChange} + /> + + handleAdjustmentChange(Effect.FogTint, e.target.value)} + step={1} + value={adjustments.fogTint ?? 0} + trackClassName="tint-gradient-track" + onDragStateChange={onDragStateChange} + /> + + )} +
)} @@ -910,7 +934,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.amount')} max={100} min={-100} - onChange={(e: any) => handleAdjustmentChange(Effect.VignetteAmount, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.VignetteAmount, e.target.value)} step={1} value={adjustments.vignetteAmount} onDragStateChange={onDragStateChange} @@ -920,7 +944,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.midpoint')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(Effect.VignetteMidpoint, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.VignetteMidpoint, e.target.value)} step={1} value={adjustments.vignetteMidpoint} onDragStateChange={onDragStateChange} @@ -930,7 +954,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.roundness')} max={100} min={-100} - onChange={(e: any) => handleAdjustmentChange(Effect.VignetteRoundness, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.VignetteRoundness, e.target.value)} step={1} value={adjustments.vignetteRoundness} onDragStateChange={onDragStateChange} @@ -940,7 +964,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.feather')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(Effect.VignetteFeather, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.VignetteFeather, e.target.value)} step={1} value={adjustments.vignetteFeather} onDragStateChange={onDragStateChange} @@ -955,7 +979,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.amount')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(Effect.GrainAmount, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.GrainAmount, e.target.value)} step={1} value={adjustments.grainAmount} onDragStateChange={onDragStateChange} @@ -965,7 +989,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.size')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(Effect.GrainSize, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.GrainSize, e.target.value)} step={1} value={adjustments.grainSize} onDragStateChange={onDragStateChange} @@ -976,7 +1000,7 @@ export default function EffectsPanel({ label={t('adjustments.effects.roughness')} max={100} min={0} - onChange={(e: any) => handleAdjustmentChange(Effect.GrainRoughness, e.target.value)} + onChange={(e: SliderChangeEvent) => handleAdjustmentChange(Effect.GrainRoughness, e.target.value)} step={1} value={adjustments.grainRoughness} onDragStateChange={onDragStateChange} diff --git a/src/components/panel/editor/EditorToolbar.tsx b/src/components/panel/editor/EditorToolbar.tsx index 1743d869d0..fa1609e01f 100644 --- a/src/components/panel/editor/EditorToolbar.tsx +++ b/src/components/panel/editor/EditorToolbar.tsx @@ -222,7 +222,7 @@ const EditorToolbar = memo( colorCalibration: 'Color Calibration', toneMapper: 'Tone Mapper', showClipping: 'Show Clipping', - sectionVisibility: 'Section Visibility', + activeTools: 'Tool Visibility', flareAmount: 'Flare Amount', glowAmount: 'Glow Amount', halationAmount: 'Halation Amount', diff --git a/src/components/panel/right/ControlsPanel.tsx b/src/components/panel/right/ControlsPanel.tsx index 1678afd482..dd06ab8e7d 100644 --- a/src/components/panel/right/ControlsPanel.tsx +++ b/src/components/panel/right/ControlsPanel.tsx @@ -14,12 +14,16 @@ import Resizer from '../../ui/Resizer'; import AdjustmentSectionsSubMenu from './AdjustmentSectionsSubMenu'; import { Adjustments, - SectionVisibility, INITIAL_ADJUSTMENTS, ADJUSTMENT_SECTIONS, + getActiveTools, getVisibleAdjustmentSections, + isToolActive, + setToolActive, + toggleToolActive, } from '../../../utils/adjustments'; import { useContextMenu } from '../../../context/ContextMenuContext'; +import { ToolVisibilityContext } from '../../../context/ToolVisibilityContext'; import { OPTION_SEPARATOR, Orientation } from '../../ui/AppProperties'; import Text from '../../ui/Text'; import { TextVariants, TextColors, TextWeights } from '../../../types/typography'; @@ -103,16 +107,10 @@ export default function Controls() { ); const handleToggleVisibility = (sectionName: string) => { - setAdjustments((prev: Adjustments) => { - const currentVisibility: SectionVisibility = prev.sectionVisibility || INITIAL_ADJUSTMENTS.sectionVisibility; - return { - ...prev, - sectionVisibility: { - ...currentVisibility, - [sectionName]: !currentVisibility[sectionName], - }, - }; - }); + setAdjustments((prev: Adjustments) => ({ + ...prev, + activeTools: toggleToolActive(getActiveTools(prev), sectionName), + })); }; const handleResetAdjustments = () => { @@ -124,7 +122,7 @@ export default function Controls() { acc[key] = INITIAL_ADJUSTMENTS[key as keyof Adjustments]; return acc; }, {}), - sectionVisibility: { ...INITIAL_ADJUSTMENTS.sectionVisibility }, + activeTools: { ...INITIAL_ADJUSTMENTS.activeTools }, })); }; @@ -169,10 +167,7 @@ export default function Controls() { setAdjustments((prev: Adjustments) => ({ ...prev, ...copiedSectionAdjustments.values, - sectionVisibility: { - ...(prev.sectionVisibility || INITIAL_ADJUSTMENTS.sectionVisibility), - [sectionName]: true, - }, + activeTools: setToolActive(getActiveTools(prev), sectionName, true), })); }; @@ -184,10 +179,7 @@ export default function Controls() { setAdjustments((prev: Adjustments) => ({ ...prev, ...resetValues, - sectionVisibility: { - ...(prev.sectionVisibility || INITIAL_ADJUSTMENTS.sectionVisibility), - [sectionName]: true, - }, + activeTools: setToolActive(getActiveTools(prev), sectionName, true), })); }; @@ -298,31 +290,35 @@ export default function Controls() { }[sectionName]; const title = t(`editor.adjustments.sections.${sectionName}`); - const sectionVisibility = adjustments.sectionVisibility || INITIAL_ADJUSTMENTS.sectionVisibility; + const activeTools = getActiveTools(adjustments); return (
handleSectionContextMenu(e, sectionName)} onToggle={() => handleToggleSection(sectionName)} onToggleVisibility={() => handleToggleVisibility(sectionName)} title={title} > - + + +
); diff --git a/src/components/panel/right/MasksPanel.tsx b/src/components/panel/right/MasksPanel.tsx index 5a0d03fff5..a2bd177d8d 100644 --- a/src/components/panel/right/MasksPanel.tsx +++ b/src/components/panel/right/MasksPanel.tsx @@ -79,8 +79,14 @@ import { INITIAL_MASK_CONTAINER, MaskContainer, ADJUSTMENT_SECTIONS, + getActiveTools, getVisibleAdjustmentSections, + isToolActive, + mergeActiveTools, + setToolActive, + toggleToolActive, } from '../../../utils/adjustments'; +import { ToolVisibilityContext } from '../../../context/ToolVisibilityContext'; import { useContextMenu } from '../../../context/ContextMenuContext'; import { OPTION_SEPARATOR, Orientation, Panel } from '../../ui/AppProperties'; import { createSubMask } from '../../../utils/maskUtils'; @@ -1408,7 +1414,10 @@ function ContainerRow({ label: item.name || item.preset.name, onClick: () => { const newAdj = { ...container.adjustments, ...(item.adjustments || item.preset.adjustments) }; - newAdj.sectionVisibility = { ...container.adjustments.sectionVisibility, ...newAdj.sectionVisibility }; + newAdj.activeTools = mergeActiveTools( + container.adjustments, + item.adjustments || item.preset.adjustments, + ); updateContainer(container.id, { adjustments: newAdj }); }, }; @@ -1925,10 +1934,7 @@ function SettingsPanel({ const newMaskAdjustments = { ...currentAdjustments, ...presetAdjustments, - sectionVisibility: { - ...(currentAdjustments.sectionVisibility || INITIAL_MASK_ADJUSTMENTS.sectionVisibility), - ...(presetAdjustments.sectionVisibility || {}), - }, + activeTools: mergeActiveTools(currentAdjustments, presetAdjustments), }; updateContainer(container.id, { adjustments: newMaskAdjustments }); }; @@ -2021,9 +2027,8 @@ function SettingsPanel({ const handleToggleVisibility = (sectionName: string) => { if (!isActive) return; const cur = container.adjustments; - const vis = cur.sectionVisibility || INITIAL_MASK_ADJUSTMENTS.sectionVisibility; updateContainer(container.id, { - adjustments: { ...cur, sectionVisibility: { ...vis, [sectionName]: !vis[sectionName] } }, + adjustments: { ...cur, activeTools: toggleToolActive(getActiveTools(cur), sectionName) }, }); }; @@ -2051,10 +2056,7 @@ function SettingsPanel({ setMaskContainerAdjustments((prev: any) => ({ ...prev, ...copiedSectionAdjustments.values, - sectionVisibility: { - ...(prev.sectionVisibility || INITIAL_MASK_ADJUSTMENTS.sectionVisibility), - [sectionName]: true, - }, + activeTools: setToolActive(getActiveTools(prev), sectionName, true), })); }; @@ -2068,10 +2070,7 @@ function SettingsPanel({ setMaskContainerAdjustments((prev: any) => ({ ...prev, ...resetValues, - sectionVisibility: { - ...(prev.sectionVisibility || INITIAL_MASK_ADJUSTMENTS.sectionVisibility), - [sectionName]: true, - }, + activeTools: setToolActive(getActiveTools(prev), sectionName, true), })); }; @@ -2104,8 +2103,7 @@ function SettingsPanel({ ]); }; - const sectionVisibility = - displayContainer.adjustments.sectionVisibility || INITIAL_MASK_ADJUSTMENTS.sectionVisibility; + const activeTools = getActiveTools(displayContainer.adjustments); const visibleSections = getVisibleAdjustmentSections(appSettings?.adjustmentLayout); return ( @@ -2272,19 +2270,23 @@ function SettingsPanel({ key={sectionName} title={title} isOpen={collapsibleState[sectionName]} - isContentVisible={sectionVisibility[sectionName]} + isContentVisible={isToolActive(activeTools, sectionName)} onToggle={() => handleToggleSection(sectionName)} onToggleVisibility={() => handleToggleVisibility(sectionName)} onContextMenu={(e: any) => handleSectionContextMenu(e, sectionName)} > - + + + ); })} diff --git a/src/context/ToolVisibilityContext.ts b/src/context/ToolVisibilityContext.ts new file mode 100644 index 0000000000..a4ede3c8e6 --- /dev/null +++ b/src/context/ToolVisibilityContext.ts @@ -0,0 +1,23 @@ +import { createContext, useContext } from 'react'; +import { ActiveTools, isToolActive } from '../utils/adjustments'; + +interface ToolVisibilityContextValue { + activeTools: ActiveTools; + onToggleTool(id: string): void; + section: string; +} + +export const ToolVisibilityContext = createContext(null); + +export const useToolVisibility = (id: string, parentId?: string) => { + const context = useContext(ToolVisibilityContext); + if (!context) { + return null; + } + const isVisible = isToolActive(context.activeTools, id); + return { + isDimmed: !isVisible && isToolActive(context.activeTools, parentId ?? context.section), + isVisible, + toggle: () => context.onToggleTool(id), + }; +}; diff --git a/src/hooks/useEditorActions.ts b/src/hooks/useEditorActions.ts index 3f33322ff4..27a0bfa91a 100644 --- a/src/hooks/useEditorActions.ts +++ b/src/hooks/useEditorActions.ts @@ -12,7 +12,10 @@ import { COPYABLE_ADJUSTMENT_KEYS, PasteMode, LensAdjustment, + getActiveTools, + mergeActiveTools, normalizeLoadedAdjustments, + setToolActive, } from '../utils/adjustments'; import { calculateCenteredCrop } from '../utils/cropUtils'; import { Invokes } from '../components/ui/AppProperties'; @@ -80,7 +83,7 @@ export function useEditorActions() { setAdjustments((prev: Adjustments) => ({ ...prev, ...autoAdjustments, - sectionVisibility: { ...prev.sectionVisibility, ...autoAdjustments.sectionVisibility }, + activeTools: mergeActiveTools(prev, autoAdjustments), })); } catch (err) { toast.error(`Failed to apply auto adjustments: ${err}`); @@ -147,10 +150,7 @@ export function useEditorActions() { lutSize: result.size, lutIntensity: 100, lutIsSceneReferred: isBuiltIn, - sectionVisibility: { - ...(prev.sectionVisibility || INITIAL_ADJUSTMENTS.sectionVisibility), - effects: true, - }, + activeTools: setToolActive(getActiveTools(prev), 'lut', true), })); } catch (err) { toast.error(`Failed to load LUT: ${err}`); diff --git a/src/utils/adjustments.ts b/src/utils/adjustments.ts index ab9b33bc5d..a74a874aca 100644 --- a/src/utils/adjustments.ts +++ b/src/utils/adjustments.ts @@ -240,6 +240,7 @@ export interface ParametricCurve { } export interface Adjustments { + activeTools: ActiveTools; [index: string]: any; aiPatches: Array; aspectRatio: number | null; @@ -331,7 +332,6 @@ export interface Adjustments { orientationSteps: number; rotation: number; saturation: number; - sectionVisibility: SectionVisibility; shadows: number; sharpness: number; sharpnessThreshold: number; @@ -415,6 +415,7 @@ interface Hsl { } export interface MaskAdjustments { + activeTools: ActiveTools; [index: string]: any; blacks: number; brightness: number; @@ -437,7 +438,6 @@ export interface MaskAdjustments { id?: string; lumaNoiseReduction: number; saturation: number; - sectionVisibility: SectionVisibility; shadows: number; sharpness: number; sharpnessThreshold: number; @@ -467,14 +467,7 @@ export interface Sections { effects: Array; } -export interface SectionVisibility { - [index: string]: boolean; - basic: boolean; - curves: boolean; - color: boolean; - details: boolean; - effects: boolean; -} +export type ActiveTools = Record; export const COLOR_LABELS: Array = [ { name: 'red', color: '#ef4444' }, @@ -544,6 +537,7 @@ export const getDefaultCurves = (): Curves => ({ export const DEFAULT_PARAMETRIC_CURVE = getDefaultParametricCurve(); export const INITIAL_MASK_ADJUSTMENTS: MaskAdjustments = { + activeTools: {}, blacks: 0, brightness: 0, clarity: 0, @@ -573,13 +567,6 @@ export const INITIAL_MASK_ADJUSTMENTS: MaskAdjustments = { hue: 0, lumaNoiseReduction: 0, saturation: 0, - sectionVisibility: { - basic: true, - curves: true, - color: true, - details: true, - effects: true, - }, shadows: 0, sharpness: 0, sharpnessThreshold: 15, @@ -600,6 +587,7 @@ export const INITIAL_MASK_CONTAINER: MaskContainer = { }; export const INITIAL_ADJUSTMENTS: Adjustments = { + activeTools: {}, aiPatches: [], aspectRatio: null, baselineExposure: true, @@ -689,13 +677,6 @@ export const INITIAL_ADJUSTMENTS: Adjustments = { orientationSteps: 0, rotation: 0, saturation: 0, - sectionVisibility: { - basic: true, - curves: true, - color: true, - details: true, - effects: true, - }, shadows: 0, sharpness: 0, sharpnessThreshold: 15, @@ -787,10 +768,8 @@ export const normalizeLoadedAdjustments = (loadedAdjustments: Adjustments): any ? deepCloneParametric(containerAdjustments.parametricCurve) : getDefaultParametricCurve(), curveMode: containerAdjustments.curveMode || INITIAL_MASK_ADJUSTMENTS.curveMode, - sectionVisibility: { - ...INITIAL_MASK_ADJUSTMENTS.sectionVisibility, - ...(containerAdjustments.sectionVisibility || {}), - }, + activeTools: getActiveTools(containerAdjustments), + sectionVisibility: undefined, sharpnessThreshold: containerAdjustments.sharpnessThreshold ?? INITIAL_MASK_ADJUSTMENTS.sharpnessThreshold, }, subMasks: normalizedSubMasks, @@ -885,10 +864,8 @@ export const normalizeLoadedAdjustments = (loadedAdjustments: Adjustments): any curveMode: loadedAdjustments.curveMode || INITIAL_ADJUSTMENTS.curveMode, masks: normalizedMasks, aiPatches: normalizedAiPatches, - sectionVisibility: { - ...INITIAL_ADJUSTMENTS.sectionVisibility, - ...(loadedAdjustments.sectionVisibility || {}), - }, + activeTools: getActiveTools(loadedAdjustments), + sectionVisibility: undefined, sharpnessThreshold: loadedAdjustments.sharpnessThreshold ?? INITIAL_ADJUSTMENTS.sharpnessThreshold, }; }; @@ -1129,6 +1106,7 @@ export const withAdjustmentLayout = (settings: AppSettings, changes: Partial; } export const ADJUSTMENT_SECTION_TOOLS: Record> = { @@ -1148,7 +1126,7 @@ export const ADJUSTMENT_SECTION_TOOLS: Record (ADJUSTMENT_SECTION_TOOLS[section] ?? []).map((tool) => tool.id); +const getToolIds = (id: string): Array => { + const expand = (tool: AdjustmentSectionTool) => tool.subTools ?? [tool.id]; + const sectionTools = ADJUSTMENT_SECTION_TOOLS[id]; + if (sectionTools) { + return sectionTools.flatMap(expand); + } + const tool = Object.values(ADJUSTMENT_SECTION_TOOLS) + .flat() + .find((sectionTool) => sectionTool.id === id); + return tool ? expand(tool) : [id]; +}; + +export const isToolActive = (activeTools: ActiveTools, id: string): boolean => + getToolIds(id).some((tool) => activeTools[tool] !== false); + +export const setToolActive = (activeTools: ActiveTools, id: string, active: boolean): ActiveTools => ({ + ...activeTools, + ...Object.fromEntries(getToolIds(id).map((tool) => [tool, active])), +}); + +export const toggleToolActive = (activeTools: ActiveTools, id: string): ActiveTools => + setToolActive(activeTools, id, !isToolActive(activeTools, id)); + +interface ToolStateSource { + activeTools?: ActiveTools; + sectionVisibility?: Record; +} + +export const getActiveTools = (adjustments: ToolStateSource): ActiveTools => + adjustments.activeTools ?? + Object.entries(adjustments.sectionVisibility ?? {}) + .filter(([, visible]) => !visible) + .reduce((activeTools, [id]) => setToolActive(activeTools, id, false), {}); + +export const mergeActiveTools = (base: ToolStateSource, overrides: ToolStateSource): ActiveTools => ({ + ...getActiveTools(base), + ...getActiveTools(overrides), +}); + export const getAdjustmentToolOrder = (section: string, toolOrder?: Record): string[] => reconcileOrder(getAdjustmentSectionToolIds(section), toolOrder?.[section]); From 3c1cf3993a13f6f8f08e909a57b7dd2a93b0b949 Mon Sep 17 00:00:00 2001 From: marclaliberte Date: Fri, 9 Oct 2026 08:33:47 -0400 Subject: [PATCH 3/3] refactor(adjustments): share the adjustment tool lookup ALL_ADJUSTMENT_TOOLS and ADJUSTMENT_TOOLS_BY_ID move from the panel customization menu into adjustments.ts so the active tool helpers reuse them, the visibility handlers take a tool id rather than a section name, and the legacy engine fallback only reads the section flags older edits actually contain. Co-Authored-By: Claude Opus 5.5 --- src-tauri/src/image_processing.rs | 11 ++++------- .../panel/right/AdjustmentSectionsSubMenu.tsx | 7 ++----- src/components/panel/right/ControlsPanel.tsx | 4 ++-- src/components/panel/right/MasksPanel.tsx | 4 ++-- src/utils/adjustments.ts | 10 +++++++--- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src-tauri/src/image_processing.rs b/src-tauri/src/image_processing.rs index 917b2695e8..018e54dbca 100644 --- a/src-tauri/src/image_processing.rs +++ b/src-tauri/src/image_processing.rs @@ -2201,13 +2201,10 @@ pub fn is_tool_active(adjustments: &serde_json::Value, tool: &str) -> bool { .get(tool) .and_then(|active| active.as_bool()) .unwrap_or(true), - None => { - is_section_visible(adjustments, tool) - && LEGACY_SECTION_TOOLS - .iter() - .find(|(_, tools)| tools.contains(&tool)) - .is_none_or(|(section, _)| is_section_visible(adjustments, section)) - } + None => LEGACY_SECTION_TOOLS + .iter() + .find(|(_, tools)| tools.contains(&tool)) + .is_none_or(|(section, _)| is_section_visible(adjustments, section)), } } diff --git a/src/components/panel/right/AdjustmentSectionsSubMenu.tsx b/src/components/panel/right/AdjustmentSectionsSubMenu.tsx index dea73fdcc5..a703e5a7dd 100644 --- a/src/components/panel/right/AdjustmentSectionsSubMenu.tsx +++ b/src/components/panel/right/AdjustmentSectionsSubMenu.tsx @@ -9,7 +9,7 @@ import { AdjustmentLayout } from '../../ui/AppProperties'; import { useSettingsStore } from '../../../store/useSettingsStore'; import { ADJUSTMENT_SECTIONS, - ADJUSTMENT_SECTION_TOOLS, + ADJUSTMENT_TOOLS_BY_ID, AdjustmentSectionTool, DEFAULT_HIDDEN_ADJUSTMENT_TOOLS, getAdjustmentSectionOrder, @@ -52,9 +52,6 @@ interface SectionRowProps { toolOrder: string[]; } -const ALL_TOOLS = Object.values(ADJUSTMENT_SECTION_TOOLS).flat(); -const TOOLS_BY_ID = Object.fromEntries(ALL_TOOLS.map((tool) => [tool.id, tool])); - const toggleId = (ids: string[], id: string) => (ids.includes(id) ? ids.filter((item) => item !== id) : [...ids, id]); function useReorderableList(savedOrder: string[], onCommit: (order: string[]) => void) { @@ -187,7 +184,7 @@ function SectionRow({ key={tool} onDragEnd={tools.handleDragEnd} onToggle={() => onToggleTool(tool)} - tool={TOOLS_BY_ID[tool]} + tool={ADJUSTMENT_TOOLS_BY_ID[tool]} /> ))} diff --git a/src/components/panel/right/ControlsPanel.tsx b/src/components/panel/right/ControlsPanel.tsx index dd06ab8e7d..385bf1a654 100644 --- a/src/components/panel/right/ControlsPanel.tsx +++ b/src/components/panel/right/ControlsPanel.tsx @@ -106,10 +106,10 @@ export default function Controls() { [setUI], ); - const handleToggleVisibility = (sectionName: string) => { + const handleToggleVisibility = (id: string) => { setAdjustments((prev: Adjustments) => ({ ...prev, - activeTools: toggleToolActive(getActiveTools(prev), sectionName), + activeTools: toggleToolActive(getActiveTools(prev), id), })); }; diff --git a/src/components/panel/right/MasksPanel.tsx b/src/components/panel/right/MasksPanel.tsx index a2bd177d8d..83d942de71 100644 --- a/src/components/panel/right/MasksPanel.tsx +++ b/src/components/panel/right/MasksPanel.tsx @@ -2024,11 +2024,11 @@ function SettingsPanel({ }); }; - const handleToggleVisibility = (sectionName: string) => { + const handleToggleVisibility = (id: string) => { if (!isActive) return; const cur = container.adjustments; updateContainer(container.id, { - adjustments: { ...cur, activeTools: toggleToolActive(getActiveTools(cur), sectionName) }, + adjustments: { ...cur, activeTools: toggleToolActive(getActiveTools(cur), id) }, }); }; diff --git a/src/utils/adjustments.ts b/src/utils/adjustments.ts index a74a874aca..cc6017d336 100644 --- a/src/utils/adjustments.ts +++ b/src/utils/adjustments.ts @@ -1133,6 +1133,12 @@ export const ADJUSTMENT_SECTION_TOOLS: Record = Object.fromEntries( + ALL_ADJUSTMENT_TOOLS.map((tool) => [tool.id, tool]), +); + export const getAdjustmentSectionToolIds = (section: string): string[] => (ADJUSTMENT_SECTION_TOOLS[section] ?? []).map((tool) => tool.id); @@ -1142,9 +1148,7 @@ const getToolIds = (id: string): Array => { if (sectionTools) { return sectionTools.flatMap(expand); } - const tool = Object.values(ADJUSTMENT_SECTION_TOOLS) - .flat() - .find((sectionTool) => sectionTool.id === id); + const tool = ADJUSTMENT_TOOLS_BY_ID[id]; return tool ? expand(tool) : [id]; };