Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
1c0a4ce
Add portable LUI runtime primitives while preserving upstream identity
tiensonqin Oct 25, 2026
4d51d7a
Release abandoned native UI handle bindings after collection
tiensonqin Oct 25, 2026
2949164
Expose bounded list windows and preserve active row owners
tiensonqin Oct 25, 2026
7875529
feat(ui): expose committed window input capture
tiensonqin Oct 25, 2026
236fd4d
feat(ui): expose committed retained text geometry
tiensonqin Oct 25, 2026
6e29499
Merge committed input capture
tiensonqin Oct 25, 2026
32bebaa
Merge committed text geometry
tiensonqin Oct 25, 2026
62757c2
feat(ui): expose committed window input capture
tiensonqin Oct 25, 2026
f3aa738
feat(ui): expose committed retained text geometry
tiensonqin Oct 25, 2026
f8afc1c
Merge committed input capture and text geometry into bounded lists
tiensonqin Oct 25, 2026
1ed4afb
feat(ui): expose live native text input snapshots
tiensonqin Oct 25, 2026
c79e881
fix(linux): preserve native glyph coverage under LCD settings
tiensonqin Oct 25, 2026
e952b02
Merge live native input snapshots into bounded virtual rows
tiensonqin Oct 25, 2026
257fcbf
feat(ui): inherit pointer hit policy with descendant opt-in
tiensonqin Oct 25, 2026
a531a81
Merge bounded list ownership into native pointer policy
tiensonqin Oct 25, 2026
f98b359
fix(ui): capture clicks on non-interactive hit elements
tiensonqin Oct 26, 2026
593befb
fix(ui): isolate inspector input from application capture
tiensonqin Oct 26, 2026
33da0b3
fix(ui): commit deferred handle identities after layout
tiensonqin Oct 26, 2026
183f6af
feat(ui): expose committed menu and scroll observations
tiensonqin Oct 26, 2026
f9e2ec8
Merge pull request #5 from logseq/devin/1792970100-native-click
tiensonqin Oct 26, 2026
11ecc0c
fix(ui): preserve captured clicks after prevented pointer down
tiensonqin Oct 27, 2026
daa6e99
feat(editor): report committed document changes
tiensonqin Oct 27, 2026
b02411b
feat(ui): add cancellable owned pointer gestures
tiensonqin Oct 27, 2026
e18607c
fix(input): separate composition end from acceptance
tiensonqin Oct 27, 2026
59f8332
fix(ui): cancel captured gestures when content becomes inert
tiensonqin Oct 27, 2026
63195e6
Merge captured gestures and committed editor input
tiensonqin Oct 28, 2026
f685510
feat(ui): expose window focus lifecycle revision
tiensonqin Oct 28, 2026
f362d05
docs(ui): clarify capture context query lifetime
tiensonqin Oct 28, 2026
afdfedb
fix(ui): preserve captured click recognition
devin-ai-integration[bot] Oct 28, 2026
0839988
feat(editor): report committed document changes
devin-ai-integration[bot] Oct 28, 2026
3dffeaf
feat(ui): combine captured gestures and committed input
devin-ai-integration[bot] Oct 28, 2026
d793d2f
fix(ui): fence input ownership across window focus transitions
devin-ai-integration[bot] Oct 28, 2026
e14dc0d
fix(ui): deliver capture-loss cancellation to input observers
tiensonqin Oct 28, 2026
e6b9038
fix(ui): register capture cancellation only when used
tiensonqin Oct 28, 2026
b33915b
Merge pointer capture cancellation fixes
devin-ai-integration[bot] Oct 28, 2026
6efea65
fix(ui): publish final caret after frame callbacks
tiensonqin Oct 28, 2026
97928f5
Merge pull request #11: publish final caret after frame callbacks
devin-ai-integration[bot] Oct 28, 2026
b2f717d
fix(ui): preserve overlay focus return owners
tiensonqin Oct 28, 2026
43c83de
fix(ui): restore light-dismissed popover focus
tiensonqin Oct 28, 2026
992640d
fix(ui): preserve overlay focus return owners
tiensonqin Oct 28, 2026
088012f
fix(ui): restore light-dismissed popover focus
tiensonqin Oct 28, 2026
d1ff119
fix(text): preserve text families before emoji fallback
tiensonqin Oct 31, 2026
8da101e
Merge remote-tracking branch 'origin/feature/linux-native-text-mask' …
tiensonqin Oct 31, 2026
efb1ecc
Merge pull request #13 from logseq/fix/linux-pango-text-family-order-…
tiensonqin Oct 31, 2026
982f5a6
feat(ui): expose read-only build text flow query
tiensonqin Nov 4, 2026
bd776a2
feat(ui): open existing context menus at coordinates
tiensonqin Nov 7, 2026
69409d1
fix(input): retain native key character metadata
tiensonqin Nov 7, 2026
a8d4564
fix(input): preserve primary capture across other buttons
tiensonqin Nov 7, 2026
006f3ca
fix: attach native key glyph metadata
tiensonqin Nov 7, 2026
9a493a7
Merge upstream 1db4e93 into production fork
tiensonqin Nov 7, 2026
56b52a3
Retire released gestures before callbacks
tiensonqin Nov 7, 2026
1cbe160
Regenerate migration API inventory for fork
tiensonqin Nov 7, 2026
e12a64d
Expose context menu positions in window DIPs
tiensonqin Nov 7, 2026
4e17292
Preserve native menu priority after input capture
tiensonqin Nov 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions LUI-FORK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# LUI compatibility fork

This fork integrates upstream MyGo through commit
`1db4e93bd48efa7e3f5a93985c8e89a4c6fa9e3d`, retaining the complete upstream
repository, tests and licenses. Its Go module path and self-imports remain
`github.com/egoist/mygo`, preserving the upstream identity. Downstream applications
can pin an immutable fork version with their Go module replacement mechanism.

No third-party source, module cache or generated build output needs to be
committed to downstream applications.

Local extensions are deliberately limited to reusable native runtime primitives required by portable LUI behavior:

- `Context.Measure` isolates intrinsic measurement; `Context.Fitting` chooses using final local layout constraints and constructs only the selected retained candidate. Fitting subtrees bypass cached-layout reuse.
- `Context.AfterFrame` runs once after the final committed layout, including late virtual rows and fitting children, before the next input/render pass.
- `ListState.SetOverscan` and `SetItemExtent` configure bounded virtualization using the actual local viewport. A scroll request fencepost is corrected and `ScrollState.Stop` cancels superseded animation.
- `Element.ZIndex` orders painting and pointer hits without reordering layout. `DisabledOpacity`, percentage minimums with pixel offsets, `ScrollAxes`, `WhiteSpace`, `Inert`, explicit italic and decoration overrides expose portable style behavior. `Tester.Accessibility` returns a copied committed accessibility tree for integration assertions.
- `Dialog.OpenContext` cancels the owned native picker on macOS, GTK and Windows and filters a result if cancellation wins the completion race. `Dialog.Open` preserves its existing pre-start diagnostic.
- `RoleTabPanel`, `OnMenuDismiss`, native cancellation notification, native menu `ImagePNG`/SVG rasterization, `LinkBase`, `CollisionPadding` and late-bound `AttachToHandle`/`MatchAnchorWidth` support accessible tabs and native menus/popovers, including targets constructed later in the same frame.

`ZIndex` uses upstream's opt-in paint/commit ordering, not a separate sorter.
Upstream `TextLayout`, accessibility, physical keys and `KeyCode` remain intact.
Committed handle geometry, live text snapshots, focus epochs and build-time
text-flow queries retain their separate lifetime and coordinate contracts.

## Source and behavior migration

- The fork's committed `TextLine` result is now `CommittedTextLine`:
`CommittedText.Lines() ([]CommittedTextLine, bool)`. Its ranges are
node-relative rune offsets and its `Bounds` are window DIPs. Upstream's
`TextLine` remains UTF-16, with layout-local `Rect` and baseline/RTL metrics.
Change explicit committed-result annotations from `[]ui.TextLine` to
`[]ui.CommittedTextLine`; inferred callers need no rename. Do not convert
between the types by aliasing offsets or coordinates.
- `Handle.ScrollOffset(c)` retains `(x, y float32, ok bool)`.
- Custom `ui.TextInputClient` implementations must implement both
`EndComposition()` (transient end, pending transaction retained) and
`CancelComposition()` (rollback), alongside `UnmarkText()` (accept).
Release on blur, replacement or disposal cancels unfinished preedit rather
than accepting it. The internal native client contract also requires typed
cancellation; retired adapters cannot mutate replacement clients. See
[the migration guide](docs/ui/migration.md#text-input-composition-phases).
- `InputEvent.Text` carries real native glyph metadata on normal/raw key down
and up alongside logical, physical and code identities. It is not a second
insertion route. Consumed input denotes actual capture/handler/shortcut
ownership, not ordinary builtin printable-key bookkeeping.
- Native cancellation remains canceled `InputPointerUp` for `HandleInput`;
`HandleCapturedPointer` translates its owned primary cancellation into one
`InputPointerCancel`. Ordinary completed release never becomes cancellation.
- Menus use upstream `SetMenuPresentation`. Choice is applied before one
dismissal callback; retired hosts/owners cannot complete a replacement menu.
`ImagePNG` copies legacy full-color bytes and lazily uses upstream icons only
when no explicit `Icon` was supplied. Unused apps do not opt into decoders.

The opt-in `testing/native` facade (`lui_native_smoke` build tag) exposes existing native host hooks for real Cocoa, GTK and Win32 qualification. Small host helpers read actual ATK/UIA focus and native popup image/name bindings. GTK image rows keep an explicit ATK name and update their retained label without removing the icon. These checks cover native interfaces and input-protocol boundaries; they do not claim audible screen-reader output or keyboard IME candidate UI.

Focused `ui/lui_*_test.go` tests verify these extensions through the real layout, interaction, accessibility and rendering pipeline. Each extension source carries its API documentation; existing runtime files contain only the necessary integration hooks.

Refresh this fork by merging an upstream release, reviewing these extensions,
and rerunning the layout, input, accessibility and native host checks. Preserve
upstream copyright notices and asset licenses. Never patch the Go module cache.
12 changes: 10 additions & 2 deletions content.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,21 @@ func (w *Window) attachContent(content Content) {
PlaceWebViews: w.placeWebViews,
PopupMenu: func(m *platform.Menu, x, y float64, chosen func(int)) {
pos := &platform.Point{X: int(math.Round(x)), Y: int(math.Round(y))}
menu := NewMenu(contentMenu(m, chosen))
completed := false
notify := func(id int) {
if !completed {
completed = true
chosen(id)
}
}
menu := NewMenu(contentMenu(m, notify))
// The menu waits for the user in a loop of its own: not in the
// event that asked for it, which the surface is handling.
postMain(func() {
if w.native != nil {
menu.popup(w, pos, m.RightToLeft)
}
notify(0)
})
},
}
Expand All @@ -110,7 +118,7 @@ func (w *Window) attachContent(content Content) {
func contentMenu(m *platform.Menu, chosen func(int)) []*MenuItem {
items := make([]*MenuItem, 0, len(m.Items))
for _, p := range m.Items {
it := &MenuItem{Label: p.Label, Accelerator: p.Accelerator, Disabled: !p.Enabled, Hidden: !p.Visible, Checked: p.Checked}
it := &MenuItem{Label: p.Label, Accelerator: p.Accelerator, Disabled: !p.Enabled, Hidden: !p.Visible, Checked: p.Checked, imagePNG: append([]byte(nil), p.ImagePNG...)}
if p.Icon != nil {
it.Icon = &MenuIcon{p.Icon}
}
Expand Down
59 changes: 59 additions & 0 deletions content_menu_images_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package mygo

import (
"bytes"
"image"
"image/color"
"image/png"
"testing"

"github.com/egoist/mygo/internal/platform"
)

func TestContentNativeMenuPreservesOwnedImagesThroughHostSnapshots(t *testing.T) {
pixels := image.NewRGBA(image.Rect(0, 0, 2, 2))
pixels.SetRGBA(1, 1, color.RGBA{R: 255, A: 255})
var encoded bytes.Buffer
if err := png.Encode(&encoded, pixels); err != nil {
t.Fatal(err)
}
icon := encoded.Bytes()
makeItem := func(id int, label string) *platform.MenuItem {
return &platform.MenuItem{ID: id, Label: label, Enabled: true, Visible: true, ImagePNG: append([]byte(nil), icon...)}
}
plain, checked, nested := makeItem(1, "Plain icon"), makeItem(2, "Checked icon"), makeItem(4, "Nested icon")
checked.Type, checked.Checked = platform.MenuItemCheckbox, true
submenu := makeItem(3, "Submenu icon")
submenu.Type, submenu.Submenu = platform.MenuItemSubmenu, &platform.Menu{Items: []*platform.MenuItem{nested}}
chosen := 0
items := contentMenu(&platform.Menu{Items: []*platform.MenuItem{plain, checked, submenu}}, func(id int) { chosen = id })
// The host bridge owns an opening snapshot after Content returns; callers
// may publish a replacement or release their original PNG buffers.
for _, item := range []*platform.MenuItem{plain, checked, submenu, nested} {
clear(item.ImagePNG)
}
menu := NewMenu(items)
snapshot := menu.snapshot()
all := []*platform.MenuItem{snapshot.Items[0], snapshot.Items[1], snapshot.Items[2], snapshot.Items[2].Submenu.Items[0]}
for _, item := range all {
decoded, err := png.Decode(bytes.NewReader(item.ImagePNG))
if err != nil {
t.Errorf("native host lost %q decoded image: %v", item.Label, err)
continue
}
if got := color.RGBAModel.Convert(decoded.At(1, 1)).(color.RGBA); got != (color.RGBA{R: 255, A: 255}) {
t.Errorf("native host changed %q image pixels: %v", item.Label, got)
}
}
if !snapshot.Items[1].Checked || snapshot.Items[1].Type != platform.MenuItemCheckbox {
t.Fatal("image forwarding changed checked menu behavior")
}
items[2].Submenu[0].Click(nil, nil)
if chosen != 4 {
t.Fatalf("nested image item selected %d, want 4", chosen)
}
clear(snapshot.Items[0].ImagePNG)
if again := menu.snapshot().Items[0].ImagePNG; !bytes.Equal(again, icon) {
t.Fatal("native snapshot shares mutable image bytes with retained menu")
}
}
31 changes: 3 additions & 28 deletions dialog.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mygo

import (
"context"
"strings"

"github.com/egoist/mygo/internal/platform"
Expand Down Expand Up @@ -137,35 +138,9 @@ func toFilters(fs []FileFilter) []platform.FileFilter {

// Open shows a dialog to pick files or directories and returns the chosen
// paths, or nil when canceled.
func (DialogModule) Open(opts OpenDialogOptions) ([]string, error) {
func (d DialogModule) Open(opts OpenDialogOptions) ([]string, error) {
needsApp("Dialog.Open")
type result struct {
paths []string
err error
}
ch := make(chan result, 1)
popts := &platform.OpenDialogOptions{
Title: opts.Title,
DefaultPath: opts.DefaultPath,
ButtonLabel: opts.ButtonLabel,
Message: opts.Message,
Filters: toFilters(opts.Filters),
OpenFiles: !opts.Directory,
OpenDirectories: opts.Directory,
Multiple: opts.Multiple,
ShowHiddenFiles: opts.ShowHiddenFiles,
CreateDirectories: opts.CreateDirectories,
TreatPackagesAsDirectories: opts.TreatPackagesAsDirectories,
}
if !postMain(func() {
backend().Dialogs().ShowOpenDialog(opts.Parent.nativeOrNil(), popts, func(paths []string, err error) {
deliver(ch, result{paths, err})
})
}) {
return nil, errLoopStopped
}
r := await(ch)
return r.paths, r.err
return d.OpenContext(context.Background(), opts)
}

// Save shows a dialog to choose where to save a file and returns the path,
Expand Down
64 changes: 60 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2448,6 +2448,26 @@ Recent (about 290 KB).

## Native UI (`ui`)

The compatibility fork integrates upstream through
`1db4e93bd48efa7e3f5a93985c8e89a4c6fa9e3d`; [LUI-FORK.md](../LUI-FORK.md)
describes its reusable extensions and source adaptations. Upstream `TextLayout`
and `TextLine` retain layout-local geometry and UTF-16 ranges. The separate
`CommittedText.Lines()` result is `[]CommittedTextLine`, with node-relative rune
ranges and window-DIP `Bounds`, invalidated with its committed handle/frame.
Explicit annotations using the fork's old `[]TextLine` name must be renamed;
inferred callers need no change. `Handle.ScrollOffset(c)` remains
`(x, y float32, ok bool)`.

`Element.HandleCapturedPointer` opts a primary pointer handler into an owned
gesture. An accepted down receives movement and release outside the element;
Escape, window deactivation, native capture loss, removal, disablement and close
end it with `InputPointerCancel`, never a successful release or click. Discard
transient state on cancellation and commit it only on `InputPointerUp`.
`HandleInput` retains its legacy synthetic release on window deactivation;
existing text and IME handlers do not opt into this contract. Install one input
handler per element. Surface backends report capture loss as `PointerCancel`;
Windows sends a real release before relinquishing its own native capture.

The public API keeps one stable `*Context` per window. Child builders share
it and temporarily change its parent. `Element` is a 16-byte checked value:
a direct owner record, arena slot and 32-bit generation. The owner detaches
Expand All @@ -2462,6 +2482,26 @@ persistent control identity separately for each window. Focus queries read
identity without depending on construction order; focus requests wait for a
hidden control. `Services` offers persistent clipboard, URL and redraw access.

Handle identities are finalized from the actual committed tree after layout,
including children built by `Fitting` and virtual lists. `AfterFrame` can read
their `Bounds` and `FocusWithin` without replaying input, actions or focus
requests. Omitted bindings lose their identity even when a same-key element
continues or is recreated; isolated measurement probes never publish bindings
to the real window.

`Context.NativeMenuOpen(owner)` reads the native menu presentation state for
one committed handle, including a menu queued by the committed frame before
the host presents it. It becomes false on selection or cancellation without
waiting for `OnMenuDismiss`. Native menu builders also run to deliver a chosen
item, so builder execution alone does not mean that a menu remains open. This
query does not change input behavior or observe retained in-window popovers.

`Handle.ScrollOffset(c)` reads the actual committed offset of any bound scroll
host, including an ordinary `Box.ScrollAxes` without `TrackScroll`. Its axes
match `ScrollState`. A non-scroller, hidden or expired binding returns
`ok == false`, not an invented zero offset. Unapplied `ScrollState` requests
remain distinct from committed position; no scrolling commands are changed.

Constructors build and style controls eagerly. `Context.Key` supplies an ID
before state initialization. A control takes its input as the public
constructor that built it returns, as Dear ImGui's widgets do, so what it
Expand Down Expand Up @@ -3332,8 +3372,12 @@ either.
`TextInputClient` to the same platform services. A stable, focus-checked
adapter supplies absolute UTF-16 text ranges, selection, marked text,
mutation callbacks, range geometry and hit testing. It is invalidated on
replacement/disposal, and focus changes unmark the old client and reset
native composition. Revealing the element, as the focus does, scrolls
replacement/disposal. Focus loss and release cancel unfinished preedit through
mandatory typed `CancelComposition`, restoring committed text and selection.
`EndComposition` only hides marking while retaining the pending transaction;
`UnmarkText` explicitly accepts it. Native end-before-commit helpers retain
UTF-16 ranges and client identity, resetting them before mutation, and retired
adapters reject callbacks. Revealing the element, as the focus does, scrolls
to the client's caret (`clientCaret`), not the element's box, which a
code editor as high as its lines makes as tall as the file. GTK and
IMM32 use bounded surrounding context while
Expand Down Expand Up @@ -3975,8 +4019,15 @@ either.
wait for the next frame, which the first changed, as a Down and an
Enter typed quickly into a menu, the Enter choosing the item the Down
highlighted. Each element
built at the top of `Overlay` notes the focus as it opens (`openers`),
which pruning gives back once it is gone with the focus that was in it.
built at the top of `Overlay` notes the focus as it opens (`openers`).
After collecting the committed focus order, before `arrangeFocus` and
pruning, return targets follow retired overlay owners only, stopping at
a live owner. A closing overlay gives focus back to a focusable opener
compatible with the remaining modal, without replacing live focus
elsewhere. Outside presses record their overlay owners until the final
commit, across rebuild passes. Only retired owners on one return chain
can restore cleared focus; a query without closure restores nothing.
The frame consumes these candidates, including on an early exit.
An element attached to another (`AttachTo`, which `PopoverBase` and the
popups of selects and comboboxes use) is that element's popover
(`Element.popover`, and `state.anchor` once committed): its layout
Expand Down Expand Up @@ -4676,6 +4727,11 @@ either.
stands for the family of GTK's `gtk-font-name` first
(`platform.Theme.UIFont`, which package ui gives `SetUIFamily` with each
change of the appearance).
Linux text needs Pango 1.48 or later. Its fontmap substitution hook
removes fontconfig's global `color=true` preference, which can otherwise
put an emoji fallback ahead of the requested text faces for bare digits.
Pango still itemizes emoji with the `emoji` family, preserving color
glyphs and variation, keycap, ZWJ and flag sequences.
- **Atlas lifetime.** A mask drawn for the first time goes to the
transient zone at the bottom of the atlas, which the next frame frees,
and moves to the lasting zone at the top once another frame draws it, so
Expand Down
Loading
Loading