mac: the composer placeholder must be click-through (#1061)
The placeholder NSTextField spans the field's full width and hit-tests, but a label's mouseDown neither focuses nor forwards — so clicking an EMPTY composer's text line was swallowed whenever focus sat elsewhere (after clicking message text, etc). Caught by ClickLab: hit chain ended at the label, first responder never moved. hitTest nil, the FrostedBand rule: decoration is click-through.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+11
−1
ios/LChatMac/MacComposerField.swift+11 −1
diff --git a/ios/LChatMac/MacComposerField.swift b/ios/LChatMac/MacComposerField.swift| index 9f985a6..6519e17 100644 |
| --- a/ios/LChatMac/MacComposerField.swift |
| +++ b/ios/LChatMac/MacComposerField.swift |
| @@ -312,7 +312,17 @@ final class MacComposerController { |
| // The NSTextView itself: hand-assembled TextKit stack, placeholder, guarded |
| // pastes. Plain text at this stage — the rich toggles return in a later PR. |
| final class ComposerTextView: NSTextView { |
| - let placeholderLabel = NSTextField(labelWithString: "") |
| + // The placeholder is decoration, and decoration must be click-through |
| + // (the FrostedBand rule): a plain NSTextField label hit-tests but its |
| + // mouseDown neither focuses anything nor forwards up the chain — and the |
| + // label spans the field's full width (see layout()), so an EMPTY composer |
| + // silently ate the very click meant to focus it whenever focus sat |
| + // elsewhere (caught by ClickLab: hit chain ended at the label, responder |
| + // never moved). |
| + private final class ClickThroughLabel: NSTextField { |
| + override func hitTest(_ point: NSPoint) -> NSView? { nil } |
| + } |
| + let placeholderLabel: NSTextField = ClickThroughLabel(labelWithString: "") |
| var onStagePaste: ((NSPasteboard) -> Bool)? |
| var onStageDrop: ((NSPasteboard) -> Bool)? |
| var onFocusChange: ((Bool) -> Void)? |