Skip to content

Commit f0b369d

Browse files
authored
fix(ide): composite overlays above Scintilla (#46)
* fix(ide): composite overlays above Scintilla * fix(ide): share platform overlay surface
1 parent d04edbb commit f0b369d

24 files changed

Lines changed: 332 additions & 225 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'lynxtron-go': patch
3+
---
4+
5+
Three fixes found while testing the IDE surface.
6+
7+
**The palette opened behind the code.** Native Scintilla views paint above all
8+
Lynx UI whatever the z-index says, so an overlay does not cover the editor — the
9+
editor covers the overlay. The palette, gallery, dialogs, loading state, and
10+
toasts now register with one shared `cover-view` host, which composites their
11+
children into a platform overlay slice above native views without creating a
12+
second macOS overlay surface during rapid modal transitions. The Scintilla extension now keeps the
13+
originating `lynx_view_t`, mounts its NSView/HWND under that view's native
14+
parent, and keeps the editor below Clay's overlay host instead of guessing the
15+
key window and floating above the entire Lynx surface. Editors stay attached
16+
while overlays are open, preserving focus, selection, scroll position, and
17+
paint state.
18+
19+
**One resolver, and reuse what is already on disk.** The Fiddle and the IDE are
20+
two views of one workspace, but each carried its own copy of "local source tree,
21+
else fetch" — two functions meaning the same thing, free to drift, sharing every
22+
failure anyway. They now share one, which also gained the step both were
23+
missing: `fetch` wipes and re-extracts its destination on every call, so opening
24+
a showcase in the Fiddle and then in the IDE downloaded and installed the same
25+
workspace twice, seconds apart. A materialized workspace is now reused, verified
26+
by reading its manifest so a half-extracted directory from an interrupted fetch
27+
is not mistaken for a usable one.
28+
29+
**A failed workspace says so.** A window opened to prepare a showcase that never
30+
arrived showed the same "Open Folder" invitation as an idle one; the reason sat
31+
in the Output panel, which is closed by default. The editor area now names the
32+
failure and offers Try again.

AGENTS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ succeeds, use the supported behaviour and update this list.
7575
- Probe the *actual* object exposed to the UI. A bridge intended as `foundation.*` was in fact spread onto the exposed root, and optional chaining hid every dead call.
7676

7777
**Native views**
78-
- Native child views float above all Lynx UI and are not clipped by Lynx ancestors. Give hosts `min-width: 0`, `min-height: 0`, `overflow: hidden` and verify in the real window.
78+
- Native child views float above the regular Lynx surface and are not clipped by Lynx ancestors. Give hosts `min-width: 0`, `min-height: 0`, `overflow: hidden` and verify in the real window.
79+
- UI that must cover a native child view must use a `<cover-view>` root. Clay renders its children into a platform overlay slice; ordinary `<view>` plus `z-index` cannot cross the native-view boundary. Keep the covered native view attached so focus, selection, scroll position, and paint state remain stable.
80+
- Route application overlays through one shared `<cover-view>` host on the current macOS runtime. Rapidly adding a second external overlay surface can race Clay's asynchronous present path; a single host can stack multiple Lynx overlay subtrees without exercising that surface-growth path.
81+
- A desktop native view must use the `lynx_view_t` passed as its registration opaque to resolve `lynx_view_get_native_window()`. On macOS mount it inside the returned renderer view so Clay's sibling `ClayOverlayView` remains above the whole subtree. On Windows use a child HWND under the returned parent and do not raise it above Clay's child overlay windows. Never rediscover the host via `keyWindow`, foreground-window enumeration, or an owned popup — those escape Clay's overlay ordering and break multi-window isolation.
7982
- Content pushed before the first attach lands in the document but does not repaint. Re-sync after the first layout.
8083
- Devtool screenshots **cannot see native views** — capture the OS window instead (`screencapture -x -l <CGWindowID>`).
8184

lynxtron-go/scintilla-extension/module/scintilla_extension_module.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ Napi::Value ScintillaExtensionModuleMethodsBinder(
506506

507507
void ScintillaExtensionModule::OnLynxViewCreate(lynx_view_t* lynx_view) {
508508
lynx_view_register_native_view(lynx_view, "scintilla-view",
509-
&scintilla_view_create_view, nullptr);
509+
&scintilla_view_create_view, lynx_view);
510510
}
511511
void ScintillaExtensionModule::OnLynxViewDestroy() {}
512512
void ScintillaExtensionModule::OnRuntimeInit() {}

lynxtron-go/scintilla-extension/module/scintilla_view.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace extension {
1818

1919
class ScintillaView : public lynx::pub::LynxNativeView {
2020
public:
21-
ScintillaView();
21+
explicit ScintillaView(lynx_view_t* lynx_view);
2222
~ScintillaView();
2323

2424
bool IsSurfaceEnabled() override { return false; } // Use a platform child view directly, not a surface.
@@ -90,6 +90,10 @@ class ScintillaView : public lynx::pub::LynxNativeView {
9090
void ApplyTheme(bool dark, int size_pt);
9191

9292
private:
93+
// Non-owning. The LynxView owns every registered native-view instance and
94+
// outlives it. Keeping the originating view removes the key-window/global
95+
// HWND heuristics and gives each editor the correct platform parent.
96+
lynx_view_t* lynx_view_ = nullptr;
9397
void* cocoa_view_ = nullptr; // Pointer to ScintillaCocoa (NSView)
9498
void* win_host_ = nullptr; // Pointer to the Win32 child host HWND
9599
void* win_view_ = nullptr; // Pointer to the Win32 Scintilla HWND
@@ -103,8 +107,8 @@ class ScintillaView : public lynx::pub::LynxNativeView {
103107
bool has_pending_content_ = false;
104108
std::string editor_id_;
105109
std::atomic<bool> content_changed_{false};
106-
// Host-driven detach (dialogs/overlays/drags): while set, OnLayoutChanged's
107-
// lazy attach must NOT re-add the view — it would float above the overlay.
110+
// Explicit route/host detach: while set, OnLayoutChanged must not re-add
111+
// the view until AttachToWindow clears it.
108112
std::atomic<bool> detached_by_host_{false};
109113
// Last layout rect (pt) so AttachToWindow can restore the frame even when
110114
// layout changed while detached. Guarded by dwell_mutex_ (written on the

lynxtron-go/scintilla-extension/module/scintilla_view.mm

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,29 @@ - (void)notification:(SCNotification*)n {
140140

141141
namespace extension {
142142

143+
#ifdef __APPLE__
144+
namespace {
145+
146+
void MountInLynxRendererHost(NSView* parent, NSView* nativeView) {
147+
if (!parent || !nativeView) return;
148+
if (nativeView.superview != parent) {
149+
[nativeView removeFromSuperview];
150+
[parent addSubview:nativeView];
151+
}
152+
}
153+
154+
NSRect FrameInPlatformParent(NSView* parent, float left, float top,
155+
float width, float height) {
156+
if (parent.isFlipped) {
157+
return NSMakeRect(left, top, width, height);
158+
}
159+
return NSMakeRect(left, parent.bounds.size.height - top - height,
160+
width, height);
161+
}
162+
163+
} // namespace
164+
#endif
165+
143166
// Diagnostic logging is opt-in: OnLayoutChanged fires on every sash-drag
144167
// frame, so unconditional printf turns a drag into a stdout flood. Set
145168
// LYNXTRON_SCINTILLA_LOG=1 to trace the attach/layout lifecycle (the
@@ -150,7 +173,8 @@ static bool ScxVerbose() {
150173
}
151174
#define SCX_LOG(...) do { if (ScxVerbose()) { printf(__VA_ARGS__); fflush(stdout); } } while (0)
152175

153-
ScintillaView::ScintillaView() {
176+
ScintillaView::ScintillaView(lynx_view_t* lynx_view)
177+
: lynx_view_(lynx_view) {
154178
SCX_LOG("ScintillaView::ScintillaView constructor called\n");
155179
#ifdef __APPLE__
156180
// Ensure UI operations happen on main thread
@@ -281,9 +305,9 @@ static bool ScxVerbose() {
281305
}
282306
}
283307

284-
// Host suppression state (dialog/overlay open) must land before the
285-
// first layout pass — panes created UNDER an open dialog (mosaic rebuild
286-
// while Settings is up) would otherwise lazily attach above it.
308+
// Backward-compatible explicit host-detach state. Product overlays now
309+
// use cover-view and leave Scintilla mounted below ClayOverlayView; route
310+
// or workspace transitions may still use this channel.
287311
if (attrs.HasProperty("suppressed")) {
288312
std::string v = attrs.GetProperty("suppressed").StdString();
289313
bool sup = !(v == "false" || v == "0");
@@ -342,21 +366,17 @@ static bool ScxVerbose() {
342366
if (detached_by_host_.load(std::memory_order_relaxed)) return;
343367
ScintillaViewContainer* container = (__bridge ScintillaViewContainer*)cocoa_view_;
344368
// So we just need to ensure the container resizes its subviews (ScintillaView).
345-
auto attachToWindow = [container]() {
369+
lynx_view_t* lynx_view = lynx_view_;
370+
auto attachToWindow = [container, lynx_view]() {
346371
if (container.hostDetached) return; // detach won the race — stay out
347-
if (container.superview != nil) return;
348-
// Prefer keyWindow, fall back to mainWindow, then first available
349-
// window. KNOWN LIMITATION: with several windows in one process this
350-
// can attach to whichever window happens to be key — the fallback
351-
// chain is a heuristic, not multi-window support (gap 2b).
352-
NSWindow* window = [NSApp keyWindow];
353-
if (!window) window = [NSApp mainWindow];
354-
if (!window) window = [[NSApp windows] firstObject];
355-
if (window) {
356-
SCX_LOG("ScintillaView::OnLayoutChanged: Adding container to window contentView\n");
357-
[window.contentView addSubview:container];
372+
NSView* parent = lynx_view
373+
? (__bridge NSView*)lynx_view_get_native_window(lynx_view)
374+
: nil;
375+
if (parent) {
376+
SCX_LOG("ScintillaView::OnLayoutChanged: Mounting in Lynx renderer host\n");
377+
MountInLynxRendererHost(parent, container);
358378
} else {
359-
SCX_LOG("ScintillaView::OnLayoutChanged: Warning - No window found to add subview\n");
379+
SCX_LOG("ScintillaView::OnLayoutChanged: Warning - Lynx native parent unavailable\n");
360380
}
361381
};
362382

@@ -366,10 +386,7 @@ static bool ScxVerbose() {
366386
auto setFrameInWindow = [container, left, top, width, height]() {
367387
NSView* parent = container.superview;
368388
if (!parent) return;
369-
CGFloat parentH = parent.bounds.size.height;
370-
CGFloat flippedY = parentH - top - height;
371-
NSRect frame = NSMakeRect(left, flippedY, width, height);
372-
[container setFrame:frame];
389+
[container setFrame:FrameInPlatformParent(parent, left, top, width, height)];
373390
};
374391

375392
if ([NSThread isMainThread]) {
@@ -618,25 +635,19 @@ static bool ScxVerbose() {
618635
}
619636
auto doAttach = ^{
620637
container.hostDetached = NO;
621-
if (container.superview == nil) {
622-
NSWindow* window = [NSApp keyWindow];
623-
if (!window) window = [NSApp mainWindow];
624-
if (!window) window = [[NSApp windows] firstObject];
625-
if (window) {
626-
[window.contentView addSubview:container];
627-
} else {
628-
SCX_LOG("ScintillaView::AttachToWindow: Warning - No window found to add subview\n");
629-
return;
630-
}
638+
NSView* parent = lynx_view_
639+
? (__bridge NSView*)lynx_view_get_native_window(lynx_view_)
640+
: nil;
641+
if (!parent) {
642+
SCX_LOG("ScintillaView::AttachToWindow: Warning - Lynx native parent unavailable\n");
643+
return;
631644
}
645+
MountInLynxRendererHost(parent, container);
632646
// Restore the LAST layout rect — the layout may have changed while we
633647
// were detached (sash drags relayout constantly) and those
634648
// OnLayoutChanged passes intentionally skipped attach/setFrame.
635649
if (w > 0 && h > 0 && container.superview != nil) {
636-
NSView* parent = container.superview;
637-
CGFloat parentH = parent.bounds.size.height;
638-
CGFloat flippedY = parentH - top - h;
639-
[container setFrame:NSMakeRect(left, flippedY, w, h)];
650+
[container setFrame:FrameInPlatformParent(parent, left, top, w, h)];
640651
}
641652
};
642653
if ([NSThread isMainThread]) {
@@ -805,7 +816,8 @@ static bool ScxVerbose() {
805816
} // namespace extension
806817

807818
LYNX_EXTERN_C lynx_native_view_t* scintilla_view_create_view(void* opaque) {
808-
auto* view = new extension::ScintillaView();
819+
auto* view = new extension::ScintillaView(
820+
static_cast<lynx_view_t*>(opaque));
809821

810822
auto* native_wrapper = view->native_view();
811823

0 commit comments

Comments
 (0)