From 76ad667fd686fda6c337b60e85a6c6393c848986 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 10 Sep 2026 20:28:09 +0200 Subject: [PATCH] Offer a mask that is nothing but a hand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every route to a layer began with a selection — a gradient, a band, a subject, a category — and painting was reachable only by making one of those and joining a painted part to it. So the answer to "brush a correction onto this corner of the sky" was "add a radial gradient you do not want, then paint into that", which is not an answer. Paint sits beside Linear and Radial and makes a layer whose base is a brush. It covers nothing until a stroke lands in it, so pressing it arms the brush and shows the mask as well: a row that appeared and changed no pixel, with the pointer still in "select", is indistinguishable from a button that did nothing. --- ui/dr-ui/src/develop.rs | 31 +++++++++++++++++++++++++++++++ ui/dr-ui/src/masks_ui.rs | 31 +++++++++++++++++++++++++++++++ ui/dr-ui/ui/masks.slint | 16 ++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/ui/dr-ui/src/develop.rs b/ui/dr-ui/src/develop.rs index bd21c9d..4e89dda 100644 --- a/ui/dr-ui/src/develop.rs +++ b/ui/dr-ui/src/develop.rs @@ -3434,6 +3434,37 @@ impl DevelopSession { Some(id) } + /// TRACES: FR-DEV-19b + /// Add a mask that is nothing but hand-painted, and select it. + /// + /// **The one route to a brush that starts from nothing.** Everything else + /// in this file makes a layer out of a selection — a gradient, a band, a + /// subject, a category — and painting was reachable only by making one of + /// those first and then joining a painted part to it. So the answer to + /// "brush a correction onto this corner of the sky" was "add a radial + /// gradient you do not want, then paint into it", which is not an answer. + /// + /// The layer covers nothing until a stroke lands in it, which is exactly + /// what [`dr_pipeline::mask::MaskPart::covers`] is about: it is not active, + /// it costs no slice, and an invert on it would not take the adjustment + /// global. What the panel shows meanwhile is a row with the tools armed + /// over it — see `masks_ui`, which arms them. + pub fn add_brush_mask(&mut self) -> Option { + let id = self.graph.masks().next_id(); + if !self + .graph + .masks_mut() + .push(MaskLayer::new(id.clone(), MaskSource::brush())) + { + return None; + } + self.active_masks = vec![id.clone()]; + self.active_part = 0; + self.history + .record(&self.graph, Edit::Action(labels::step::MASK_ADDED)); + Some(id) + } + /// TRACES: FR-DEV-10 /// Add a mask that selects by a range of the photograph's own values. /// diff --git a/ui/dr-ui/src/masks_ui.rs b/ui/dr-ui/src/masks_ui.rs index c777b7b..692afe3 100644 --- a/ui/dr-ui/src/masks_ui.rs +++ b/ui/dr-ui/src/masks_ui.rs @@ -1010,6 +1010,37 @@ pub(crate) fn wire( redraw(&w); }); } + // TRACES: FR-DEV-19b + { + let weak = window.as_weak(); + let session = session.clone(); + let redraw = redraw.clone(); + let rows = rows.clone(); + window.global::().on_add_brush(move || { + let Some(w) = weak.upgrade() else { return }; + let made = session + .borrow_mut() + .as_mut() + .and_then(|s| s.add_brush_mask()) + .is_some(); + // Armed, and this is not a convenience: the layer covers no pixel + // until something is painted into it, so a press that made a row + // and left the pointer in "select" would be indistinguishable from + // a button that did nothing. The same reasoning as `on_part_added` + // below, and the tool's own handler turns the mask view on. + if made { + w.global::().set_tool(1); + if let Some(s) = session.borrow_mut().as_mut() { + if s.mask_view() == 0 { + s.set_mask_view(1); + } + } + } + sync(&w, &session); + sync_rows(&w, &rows, &session); + redraw(&w); + }); + } // TRACES: FR-DEV-10 { let weak = window.as_weak(); diff --git a/ui/dr-ui/ui/masks.slint b/ui/dr-ui/ui/masks.slint index f896611..6bef8f8 100644 --- a/ui/dr-ui/ui/masks.slint +++ b/ui/dr-ui/ui/masks.slint @@ -970,6 +970,22 @@ export component MaskPanel inherits Rectangle { enabled: Develop.enabled; clicked => { Masking.add-gradient(true); } } + + // TRACES: FR-DEV-19b + // **A mask that is nothing but a hand.** Beside the gradients + // because it belongs to the same answer to "where" — and because + // without it the only way to reach the brush was to make a + // gradient nobody wanted and paint into that. + // + // It covers nothing until a stroke lands, so pressing this arms + // the brush as well as making the layer: a row that appeared and + // changed no pixel, with the pointer still in "select", would look + // exactly like a button that did nothing. + Button { + text: "Paint"; + enabled: Develop.enabled; + clicked => { Masking.add-brush(); } + } } // Ranges, on their own row rather than beside the gradients. A