Compare commits

...

18 Commits

Author SHA1 Message Date
qiannian 1a5fa17a39 app: update TopMost comment
TopMost is also supported on Windows.

Signed-off-by: qiannian <qianniancn@gmail.com>
2026-06-27 17:50:54 +08:00
qiannian 5191409708 app: [Windows] support TopMost option
Use HWND_TOPMOST and HWND_NOTOPMOST when applying Config.TopMost on Windows.

Leave SWP_NOZORDER set when TopMost hasn't changed, so other Configure calls don't reorder the window.

Signed-off-by: qiannian <qianniancn@gmail.com>
2026-06-27 10:38:28 +02:00
Jeff Williams 3eab806940 app: gracefully handle Windows clipboard read errors
Signed-off-by: Jeff Williams <info@anvil-editor.net>
2026-06-26 14:25:53 +02:00
qiannian 30dc7ff294 app: [Windows] don't make raised windows topmost
Use HWND_TOP instead of HWND_TOPMOST in ActionRaise so raising a
window no longer pins it on top.

Signed-off-by: qiannian <qianniancn@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-06-25 18:53:27 +02:00
Elias Naur 9e18cb93fb ap/internal/windows: change type of HWND_TOPMOST to syscall.Handle
syscall.Handle is equivalent to the C type HWND.

Inspired by a patch by qiannian.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-06-25 11:18:57 +02:00
qiannian e4932e163e widget,io,app: draw IME composition underline
Track IME composition range updates as key.CompositionEvent and route them to
the focused editor. Draw a thin underline under the current composition range
using the editor text material.

Keep the composition range in imeState so it is reset with the rest of the
editor input method state, and normalize it in input.Router before forwarding
CompositionEvent.

Signed-off-by: qiannian <qianniancn@gmail.com>
2026-06-17 12:05:40 -04:00
qiannian a8fe27488f app: [Windows] avoid default IME composition window
Mark WM_IME_STARTCOMPOSITION as handled after positioning the IME
composition and candidate windows.

Passing the message on to DefWindowProc can let Windows create its default
composition window, which shows the first composing character below Gio's
caret.

Fixes: https://todo.sr.ht/~eliasnaur/gio/687
Signed-off-by: qiannian <qianniancn@gmail.com>
2026-06-17 09:07:13 +02:00
qiannian 06307313cd io/input: support direct pointer leave events
Allow platform backends to send pointer.Leave directly.
The router delivers it to entered handlers so hover state is cleared normally.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-06-17 08:05:23 +02:00
qiannian 15335a2b37 app: [Windows] restore double-click restore for custom title bars
Keep custom move areas mapped to HTCAPTION even when the window is
maximized so custom title bars preserve the standard Windows behavior
where double-click restores the window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-06-16 10:06:47 +02:00
Qian Nian acf5635575 widget/material: add hover state to window decorations
Decorations buttons already use widget.Clickable and draw press ink, but they don't draw a hover/focus background. This makes custom window decorations appear unresponsive when the pointer is over minimize, maximize, or close buttons.

Register each button's system action over its clickable area and draw the same hovered background used by other material buttons. This also lets platforms query the button action from the material decorations hit area.

Signed-off-by: qiannian <qianniancn@gmail.com>
2026-06-14 10:50:56 +02:00
Kevin Yuan caccb608a5 app: [Windows] don't propagate WM_WINDOWPOSCHANGED to DefWindowProc
DefWindowProc handles WM_WINDOWPOSCHANGED by sending WM_SIZE and WM_MOVE
messages, which would lead us to handle resizes twice.

Per MSDN, the WM_SIZE handler is made redundant by handling
WM_WINDOWPOSCHANGED:
https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-windowposchanged

Signed-off-by: Kevin Yuan <farproc@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-05-26 12:50:02 +02:00
Kevin Yuan d52632b475 internal/egl: fix loadEGL caching error on Windows
loadEGL used sync.Once incorrectly: the error returned by
loadDLLs was assigned to a local variable inside loadEGL,
so on the second call Do would not run and the
function would return nil even though the DLLs were never
loaded. This caused a nil pointer dereference when callers
proceeded to use _eglGetDisplay and other uninitialized
function pointers.

Fix by replacing the sync.Once with sync.OnceValue, which
correctly caches the return value of loadDLLs across all
calls.

Signed-off-by: Kevin Yuan <farproc@gmail.com>
2026-05-26 08:07:26 +02:00
Egon Elbre dec57aea1c go.mod: upgrade to github.com/go-text/typesetting@v0.3.4
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2026-05-18 14:30:25 -04:00
Egon Elbre e2e2c1a046 text: avoid creating two Face instances
This way their cache can be shared.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2026-05-18 14:30:17 -04:00
Egon Elbre e8c1e1ba11 font/opentype: fix font.Face creation
typesetting introduced a cache field that needs to be
properly initialized. Use constructor to avoid the issue.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2026-05-18 14:30:12 -04:00
Eugene b1cadbdd76 io/input: do not track scroll events as pointers
Scroll events arrived at pointerQueue.Push and went through pointerOf
+ deliverEnterLeaveEvents + deliverEvent like Move/Press/Release. The
side effect: every scroll created or updated a state.pointers entry,
populated p.entered with whatever handlers sat under the wheel
position, and overwrote state.cursor based on hit-test at the scroll
position.

When the platform layer reports scroll with a different PointerID
than mouse-move events for the same physical mouse — which the
Windows backend does (scrollEvent omits PointerID, defaulting to 0,
while pointerUpdate forwards Windows' assigned ID) — the scroll
spawns a phantom state.pointers entry. Subsequent moves go to the
mouse's "real" entry, so the phantom never receives Leave events,
its entered set never empties, and the cleanup at the end of Push
keeps it alive. pointerQueue.Frame then runs hit-test for it every
frame at the user's last scroll position, threading state.cursor
through it after the live pointer's resolution and clobbering it
with whatever's under the scroll position.

The wheel is positional but isn't a pointer. Treating it as one is
the bug. Hit-test inline at the scroll position to find delivery
targets, dispatch via deliverEvent (which already handles filter
matching, scroll axis clamping, and area-local position), and
return without creating or updating a state.pointers entry.

Add a router-level test that fails without the fix: a Move sets the
cursor over a CursorPointer region, a subsequent Scroll over a
CursorText region, and the test asserts the cursor is still
CursorPointer. Pre-fix the scroll's deliverEnterLeaveEvents
overwrites state.cursor with CursorText.

Signed-off-by: Eugene <eugenebosyakov@gmail.com>
2026-05-18 09:05:32 +02:00
Chris Waldon 451b7d3a74 text: drop obsolete comment about NewWindow
Fixes: https://todo.sr.ht/~eliasnaur/gio/681
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2026-05-06 13:29:01 -04:00
Eugene e49c5b02c7 gesture: refresh PointerID on Press and Enter
Click and Hover both stored the first PointerID they observed in
their internal pid field and only updated it when not currently
hovered/entered. Once the gesture became hovered, any later event
under a different PointerID was effectively ignored: Click.Press
fell through 'c.pid != e.PointerID' and was silently dropped, and
Hover could never reset entered when the matching Leave arrived
under a new ID.

The Windows backend enables EnableMouseInPointer
(app/os_windows.go), under which Windows reassigns the same
physical mouse's PointerID across focus changes, window
leave/re-enter, and similar events. Once a widget had been hovered,
every subsequent press on it failed to register, including
widget.Editor's internal clicker that positions the caret on press.
Multi-line editors silently refused to move the caret on click
after the window had received any focus event.

Always take the latest PointerID on Hover.Enter and Click.Press.
The Press/Release handshake still works because Press now records
the press's own PointerID and Release continues to gate on
'c.pid != e.PointerID' so an unrelated pointer's release can't end
the press tracking.

Signed-off-by: Eugene <eugenebosyakov@gmail.com>
2026-04-30 06:19:20 +02:00
19 changed files with 309 additions and 120 deletions
+4 -2
View File
@@ -207,7 +207,9 @@ const (
CFS_POINT = 0x0002 CFS_POINT = 0x0002
CFS_CANDIDATEPOS = 0x0040 CFS_CANDIDATEPOS = 0x0040
HWND_TOPMOST = ^(uint32(1) - 1) // -1 HWND_TOP = syscall.Handle(0)
HWND_TOPMOST = ^(syscall.Handle(1) - 1) // -1
HWND_NOTOPMOST = ^(syscall.Handle(2) - 1) // -2
HTCAPTION = 2 HTCAPTION = 2
HTCLIENT = 1 HTCLIENT = 1
@@ -782,7 +784,7 @@ func SetWindowPlacement(hwnd syscall.Handle, wp *WindowPlacement) {
_SetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wp))) _SetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wp)))
} }
func SetWindowPos(hwnd syscall.Handle, hwndInsertAfter uint32, x, y, dx, dy int32, style uintptr) { func SetWindowPos(hwnd, hwndInsertAfter syscall.Handle, x, y, dx, dy int32, style uintptr) {
_SetWindowPos.Call(uintptr(hwnd), uintptr(hwndInsertAfter), _SetWindowPos.Call(uintptr(hwnd), uintptr(hwndInsertAfter),
uintptr(x), uintptr(y), uintptr(x), uintptr(y),
uintptr(dx), uintptr(dy), uintptr(dx), uintptr(dy),
+54 -41
View File
@@ -369,8 +369,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
w.update() w.update()
case windows.WM_WINDOWPOSCHANGED: case windows.WM_WINDOWPOSCHANGED:
w.update() w.update()
case windows.WM_SIZE: return 0
w.update()
case windows.WM_GETMINMAXINFO: case windows.WM_GETMINMAXINFO:
mm := (*windows.MinMaxInfo)(unsafe.Pointer(lParam)) mm := (*windows.MinMaxInfo)(unsafe.Pointer(lParam))
@@ -413,6 +412,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
icaret := image.Pt(int(caret.X+.5), int(caret.Y+.5)) icaret := image.Pt(int(caret.X+.5), int(caret.Y+.5))
windows.ImmSetCompositionWindow(imc, icaret.X, icaret.Y) windows.ImmSetCompositionWindow(imc, icaret.X, icaret.Y)
windows.ImmSetCandidateWindow(imc, icaret.X, icaret.Y) windows.ImmSetCandidateWindow(imc, icaret.X, icaret.Y)
return windows.TRUE
case windows.WM_IME_COMPOSITION: case windows.WM_IME_COMPOSITION:
imc := windows.ImmGetContext(w.hwnd) imc := windows.ImmGetContext(w.hwnd)
if imc == 0 { if imc == 0 {
@@ -495,34 +495,32 @@ func getModifiers() key.Modifiers {
// hitTest returns the non-client area hit by the point, needed to // hitTest returns the non-client area hit by the point, needed to
// process WM_NCHITTEST. // process WM_NCHITTEST.
func (w *window) hitTest(x, y int) uintptr { func (w *window) hitTest(x, y int) uintptr {
if w.config.Mode != Windowed { if w.config.Mode == Windowed {
// Only windowed mode should allow resizing. // Check for resize handle before system actions; otherwise it can be impossible to
return windows.HTCLIENT // resize a custom-decorations window when the system move area is flush with the
} // edge of the window.
// Check for resize handle before system actions; otherwise it can be impossible to top := y <= w.borderSize.Y
// resize a custom-decorations window when the system move area is flush with the bottom := y >= w.config.Size.Y-w.borderSize.Y
// edge of the window. left := x <= w.borderSize.X
top := y <= w.borderSize.Y right := x >= w.config.Size.X-w.borderSize.X
bottom := y >= w.config.Size.Y-w.borderSize.Y switch {
left := x <= w.borderSize.X case top && left:
right := x >= w.config.Size.X-w.borderSize.X return windows.HTTOPLEFT
switch { case top && right:
case top && left: return windows.HTTOPRIGHT
return windows.HTTOPLEFT case bottom && left:
case top && right: return windows.HTBOTTOMLEFT
return windows.HTTOPRIGHT case bottom && right:
case bottom && left: return windows.HTBOTTOMRIGHT
return windows.HTBOTTOMLEFT case top:
case bottom && right: return windows.HTTOP
return windows.HTBOTTOMRIGHT case bottom:
case top: return windows.HTBOTTOM
return windows.HTTOP case left:
case bottom: return windows.HTLEFT
return windows.HTBOTTOM case right:
case left: return windows.HTRIGHT
return windows.HTLEFT }
case right:
return windows.HTRIGHT
} }
p := f32.Pt(float32(x), float32(y)) p := f32.Pt(float32(x), float32(y))
if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove { if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove {
@@ -701,7 +699,13 @@ func (w *window) ReadClipboard() {
w.readClipboard() w.readClipboard()
} }
func (w *window) readClipboard() error { func (w *window) readClipboard() (cerr error) {
defer func() {
if cerr != nil {
w.processDataEvent("")
}
}()
if err := windows.OpenClipboard(w.hwnd); err != nil { if err := windows.OpenClipboard(w.hwnd); err != nil {
return err return err
} }
@@ -716,13 +720,17 @@ func (w *window) readClipboard() error {
} }
defer windows.GlobalUnlock(mem) defer windows.GlobalUnlock(mem)
content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr))) content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
w.processDataEvent(content)
return nil
}
func (w *window) processDataEvent(content string) {
w.ProcessEvent(transfer.DataEvent{ w.ProcessEvent(transfer.DataEvent{
Type: "application/text", Type: "application/text",
Open: func() io.ReadCloser { Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content)) return io.NopCloser(strings.NewReader(content))
}, },
}) })
return nil
} }
func (w *window) Configure(options []Option) { func (w *window) Configure(options []Option) {
@@ -739,7 +747,16 @@ func (w *window) Configure(options []Option) {
style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE) style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE)
var showMode int32 var showMode int32
var x, y, width, height int32 var x, y, width, height int32
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED) swpStyle := uintptr(windows.SWP_FRAMECHANGED)
if cnf.TopMost == w.config.TopMost {
// Don't change the z-order if TopMost didn't change.
swpStyle |= windows.SWP_NOZORDER
}
hwndAfter := windows.HWND_NOTOPMOST
if cnf.TopMost {
hwndAfter = windows.HWND_TOPMOST
}
w.config.TopMost = cnf.TopMost
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW) winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
style &^= winStyle style &^= winStyle
switch cnf.Mode { switch cnf.Mode {
@@ -791,7 +808,7 @@ func (w *window) Configure(options []Option) {
// Note: these invocation all trigger the windows callback method which may process a pending system.ActionCenter // Note: these invocation all trigger the windows callback method which may process a pending system.ActionCenter
// action, so SetWindowPos should come first so as to not "overwrite" system.ActionCenter. // action, so SetWindowPos should come first so as to not "overwrite" system.ActionCenter.
windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle) windows.SetWindowPos(w.hwnd, hwndAfter, x, y, width, height, swpStyle)
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style) windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style)
windows.ShowWindow(w.hwnd, showMode) windows.ShowWindow(w.hwnd, showMode)
} }
@@ -912,19 +929,15 @@ func (w *window) Perform(acts system.Action) {
y := (mi.Bottom - mi.Top - dy) / 2 y := (mi.Bottom - mi.Top - dy) / 2
windows.SetWindowPos(w.hwnd, 0, x, y, dx, dy, windows.SWP_NOZORDER|windows.SWP_FRAMECHANGED) windows.SetWindowPos(w.hwnd, 0, x, y, dx, dy, windows.SWP_NOZORDER|windows.SWP_FRAMECHANGED)
case system.ActionRaise: case system.ActionRaise:
w.raise() windows.SetForegroundWindow(w.hwnd)
windows.SetWindowPos(w.hwnd, windows.HWND_TOP, 0, 0, 0, 0,
windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW)
case system.ActionClose: case system.ActionClose:
windows.PostMessage(w.hwnd, windows.WM_CLOSE, 0, 0) windows.PostMessage(w.hwnd, windows.WM_CLOSE, 0, 0)
} }
}) })
} }
func (w *window) raise() {
windows.SetForegroundWindow(w.hwnd)
windows.SetWindowPos(w.hwnd, windows.HWND_TOPMOST, 0, 0, 0, 0,
windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW)
}
func convertKeyCode(code uintptr) (key.Name, bool) { func convertKeyCode(code uintptr) (key.Name, bool) {
if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' { if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' {
return key.Name(rune(code)), true return key.Name(rune(code)), true
+2 -1
View File
@@ -439,6 +439,7 @@ func (c *callbacks) EditorState() editorState {
func (c *callbacks) SetComposingRegion(r key.Range) { func (c *callbacks) SetComposingRegion(r key.Range) {
c.w.imeState.compose = r c.w.imeState.compose = r
c.w.driver.ProcessEvent(key.CompositionEvent(r))
} }
func (c *callbacks) EditorInsert(text string) { func (c *callbacks) EditorInsert(text string) {
@@ -971,7 +972,7 @@ func Decorated(enabled bool) Option {
// TopMost windows will be rendered above all other non-top-most windows. // TopMost windows will be rendered above all other non-top-most windows.
// //
// TopMost windows are only supported on MacOS currently. // TopMost windows are supported on macOS, Windows.
func TopMost(enabled bool) Option { func TopMost(enabled bool) Option {
return func(_ unit.Metric, cnf *Config) { return func(_ unit.Metric, cnf *Config) {
cnf.TopMost = enabled cnf.TopMost = enabled
+1 -1
View File
@@ -106,7 +106,7 @@ func parseLoader(ld *opentype.Loader) (*fontapi.Font, giofont.Font, error) {
// Face many be invoked any number of times and is safe so long as each return value is // Face many be invoked any number of times and is safe so long as each return value is
// only used by one goroutine. // only used by one goroutine.
func (f Face) Face() *fontapi.Face { func (f Face) Face() *fontapi.Face {
return &fontapi.Face{Font: f.face} return fontapi.NewFace(f.face)
} }
// FontFace returns a text.Font with populated font metadata for the // FontFace returns a text.Font with populated font metadata for the
+3 -12
View File
@@ -61,12 +61,8 @@ func (h *Hover) Update(q input.Source) bool {
h.entered = false h.entered = false
} }
case pointer.Enter: case pointer.Enter:
if !h.entered { h.pid = e.PointerID
h.pid = e.PointerID h.entered = true
}
if h.pid == e.PointerID {
h.entered = true
}
} }
} }
return h.entered return h.entered
@@ -222,12 +218,7 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary { if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary {
break break
} }
if !c.hovered { c.pid = e.PointerID
c.pid = e.PointerID
}
if c.pid != e.PointerID {
break
}
c.pressed = true c.pressed = true
if e.Time-c.clickedAt < doubleClickDuration { if e.Time-c.clickedAt < doubleClickDuration {
c.clicks++ c.clicks++
+72
View File
@@ -100,6 +100,78 @@ func TestMouseClicks(t *testing.T) {
} }
} }
func TestClickPointerIDReassignment(t *testing.T) {
// A Click must accept a Press from a PointerID that differs from the
// one its hovered state was previously associated with. Some backends
// reassign a single physical pointer's ID over its lifetime — e.g. the
// Windows pointer API across focus changes — and locking the gesture
// to the first observed ID would silently drop every subsequent press.
//
// The sequence below puts the gesture into the buggy state through
// public events alone: a press under PointerID 1 starts an active
// press cycle, a Move under PointerID 2 arrives mid-press (which the
// router routes as an Enter for PID 2 but the gesture's Enter handler
// is a no-op for pid while pressed), then PID 1 releases. After this,
// the router has the gesture entered for PID 2 (so the next event
// under PID 2 won't trigger another Enter) but the gesture itself
// still has pid=1.
var click Click
var ops op.Ops
rect := image.Rect(0, 0, 100, 100)
stack := clip.Rect(rect).Push(&ops)
click.Add(&ops)
stack.Pop()
var r input.Router
click.Update(r.Source())
r.Frame(&ops)
drain := func() {
for {
if _, ok := click.Update(r.Source()); !ok {
return
}
}
}
// Press under PointerID 1.
r.Queue(
pointer.Event{Kind: pointer.Move, Source: pointer.Mouse, Position: f32.Pt(50, 50), PointerID: 1},
pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Buttons: pointer.ButtonPrimary, Position: f32.Pt(50, 50), PointerID: 1},
)
drain()
// Move under PointerID 2 while PointerID 1 is still pressed. The
// router records the gesture as entered for PointerID 2 but the
// gesture's Enter handler is a no-op for pid because c.pressed.
r.Queue(pointer.Event{Kind: pointer.Move, Source: pointer.Mouse, Position: f32.Pt(50, 50), PointerID: 2})
drain()
// Release PointerID 1. PointerID 1's press tracking ends; the
// gesture's recorded pid stays at 1.
r.Queue(pointer.Event{Kind: pointer.Release, Source: pointer.Mouse, Position: f32.Pt(50, 50), PointerID: 1})
drain()
// Press under PointerID 2. The router won't refire Enter for PID 2
// (the gesture is already in PID 2's entered set), so the gesture's
// only chance to refresh its pid is the Press handler itself.
r.Queue(pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Buttons: pointer.ButtonPrimary, Position: f32.Pt(50, 50), PointerID: 2})
var sawPress bool
for {
ev, ok := click.Update(r.Source())
if !ok {
break
}
if ev.Kind == KindPress {
sawPress = true
}
}
if !sawPress {
t.Fatal("expected KindPress for press under reassigned PointerID; gesture dropped the press because of stale recorded pid")
}
}
func mouseClickEvents(times ...time.Duration) []event.Event { func mouseClickEvents(times ...time.Duration) []event.Event {
press := pointer.Event{ press := pointer.Event{
Kind: pointer.Press, Kind: pointer.Press,
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.24.0
require ( require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
gioui.org/shader v1.0.8 gioui.org/shader v1.0.8
github.com/go-text/typesetting v0.3.0 github.com/go-text/typesetting v0.3.4
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/image v0.26.0 golang.org/x/image v0.26.0
+4 -4
View File
@@ -3,10 +3,10 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8v
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4= github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY= github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfatHSRPHeW6+2WuxaVQuHftn80= golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfatHSRPHeW6+2WuxaVQuHftn80=
+2 -6
View File
@@ -41,14 +41,10 @@ var (
_eglWaitClient *syscall.Proc _eglWaitClient *syscall.Proc
) )
var loadOnce sync.Once var loadOnce = sync.OnceValue(loadDLLs)
func loadEGL() error { func loadEGL() error {
var err error return loadOnce()
loadOnce.Do(func() {
err = loadDLLs()
})
return err
} }
func loadDLLs() error { func loadDLLs() error {
+19 -4
View File
@@ -739,6 +739,10 @@ func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState,
state.pointers = nil state.pointers = nil
return state, evts return state, evts
} }
if e.Kind == pointer.Scroll {
// Scroll events are not bound to a pointer; see pointer.Event.PointerID.
return state, q.deliverScrollEvent(handlers, evts, e)
}
state, pidx := state.pointerOf(e) state, pidx := state.pointerOf(e)
p := state.pointers[pidx] p := state.pointers[pidx]
@@ -756,14 +760,13 @@ func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState,
if p.pressed { if p.pressed {
p, evts = q.deliverDragEvent(handlers, p, evts) p, evts = q.deliverDragEvent(handlers, p, evts)
} }
case pointer.Leave:
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
case pointer.Release: case pointer.Release:
evts = q.deliverEvent(handlers, p, evts, e) evts = q.deliverEvent(handlers, p, evts, e)
p.pressed = false p.pressed = false
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
p, evts = q.deliverDropEvent(handlers, p, evts) p, evts = q.deliverDropEvent(handlers, p, evts)
case pointer.Scroll:
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
evts = q.deliverEvent(handlers, p, evts, e)
default: default:
panic("unsupported pointer event type") panic("unsupported pointer event type")
} }
@@ -780,6 +783,18 @@ func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState,
return state, evts return state, evts
} }
// deliverScrollEvent delivers scroll events to the handlers hit by the event coordinate.
func (q *pointerQueue) deliverScrollEvent(handlers map[event.Tag]*handler, evts []taggedEvent, e pointer.Event) []taggedEvent {
var hits []event.Tag
q.hitTest(e.Position, func(n *hitNode) bool {
if _, ok := handlers[n.tag]; ok {
hits = addHandler(hits, n.tag)
}
return true
})
return q.deliverEvent(handlers, pointerInfo{handlers: hits}, evts, e)
}
func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent { func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent {
if p.pressed && len(p.handlers) == 1 { if p.pressed && len(p.handlers) == 1 {
e.Priority = pointer.Grabbed e.Priority = pointer.Grabbed
@@ -810,7 +825,7 @@ func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerIn
func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler, cursor pointer.Cursor, p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent, pointer.Cursor, bool) { func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler, cursor pointer.Cursor, p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent, pointer.Cursor, bool) {
changed := false changed := false
var hits []event.Tag var hits []event.Tag
if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press { if e.Kind == pointer.Leave || e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press {
// Consider non-mouse pointers leaving when they're released. // Consider non-mouse pointers leaving when they're released.
} else { } else {
var transSrc *pointerFilter var transSrc *pointerFilter
+76
View File
@@ -255,6 +255,45 @@ func TestPointerMove(t *testing.T) {
assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Enter, pointer.Move, pointer.Leave, pointer.Cancel) assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Enter, pointer.Move, pointer.Leave, pointer.Cancel)
} }
func TestPointerLeave(t *testing.T) {
handler := new(int)
var ops op.Ops
filter := pointer.Filter{
Target: handler,
Kinds: pointer.Move | pointer.Enter | pointer.Leave | pointer.Cancel,
}
defer clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop()
event.Op(&ops, handler)
var r Router
events(&r, -1, filter)
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
PointerID: 1,
Position: f32.Pt(50, 50),
},
pointer.Event{
Kind: pointer.Leave,
Source: pointer.Mouse,
PointerID: 1,
Position: f32.Pt(50, 50),
},
)
assertEventPointerTypeSequence(t, events(&r, -1, filter), pointer.Enter, pointer.Move, pointer.Leave)
r.Queue(pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
PointerID: 1,
Position: f32.Pt(50, 50),
})
assertEventPointerTypeSequence(t, events(&r, -1, filter), pointer.Enter, pointer.Move)
}
func TestPointerTypes(t *testing.T) { func TestPointerTypes(t *testing.T) {
handler := new(int) handler := new(int)
var ops op.Ops var ops op.Ops
@@ -1345,3 +1384,40 @@ func events(r *Router, n int, filters ...event.Filter) []event.Event {
} }
return events return events
} }
// TestPointerScrollDoesNotTrackPointer queues two events over two cursor
// regions. The Move puts the live pointer over the button (CursorPointer);
// the Scroll happens over the cell (CursorText) and must not update the
// cursor.
func TestPointerScrollDoesNotTrackPointer(t *testing.T) {
var ops op.Ops
button := clip.Rect(image.Rect(0, 0, 50, 50)).Push(&ops)
pointer.CursorPointer.Add(&ops)
button.Pop()
cell := clip.Rect(image.Rect(100, 0, 200, 50)).Push(&ops)
pointer.CursorText.Add(&ops)
cell.Pop()
var r Router
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
Position: f32.Pt(25, 25),
},
pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Mouse,
Position: f32.Pt(150, 25),
Scroll: f32.Pt(0, 1),
},
)
if got, want := r.Cursor(), pointer.CursorPointer; got != want {
t.Errorf("got %q, want %q (scroll position must not update the cursor; "+
"the live pointer's last position is what determines it)", got, want)
}
}
+8 -1
View File
@@ -419,7 +419,7 @@ func (f *filter) Merge(f2 filter) {
func (f *filter) Matches(e event.Event) bool { func (f *filter) Matches(e event.Event) bool {
switch e.(type) { switch e.(type) {
case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent: case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent, key.CompositionEvent:
return f.focusable return f.focusable
default: default:
return f.pointer.Matches(e) return f.pointer.Matches(e)
@@ -463,6 +463,13 @@ func (q *Router) processEvent(e event.Event, system bool) {
evts = append(evts, taggedEvent{tag: f, event: e}) evts = append(evts, taggedEvent{tag: f, event: e})
} }
q.changeState(e, state, evts) q.changeState(e, state, evts)
case key.CompositionEvent:
e = key.CompositionEvent(rangeNorm(key.Range(e)))
var evts []taggedEvent
if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
q.changeState(e, state, evts)
case key.EditEvent, key.FocusEvent, key.SelectionEvent: case key.EditEvent, key.FocusEvent, key.SelectionEvent:
var evts []taggedEvent var evts []taggedEvent
if f := state.focus; f != nil { if f := state.focus; f != nil {
+9 -5
View File
@@ -77,6 +77,9 @@ type Caret struct {
// SelectionEvent is generated when an input method changes the selection. // SelectionEvent is generated when an input method changes the selection.
type SelectionEvent Range type SelectionEvent Range
// CompositionEvent is generated when an input method changes the composing range.
type CompositionEvent Range
// SnippetEvent is generated when the snippet range is updated by an // SnippetEvent is generated when the snippet range is updated by an
// input method. // input method.
type SnippetEvent Range type SnippetEvent Range
@@ -243,11 +246,12 @@ func (h InputHintOp) Add(o *op.Ops) {
data[1] = byte(h.Hint) data[1] = byte(h.Hint)
} }
func (EditEvent) ImplementsEvent() {} func (EditEvent) ImplementsEvent() {}
func (Event) ImplementsEvent() {} func (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {} func (FocusEvent) ImplementsEvent() {}
func (SnippetEvent) ImplementsEvent() {} func (CompositionEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {} func (SnippetEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {}
func (FocusCmd) ImplementsCommand() {} func (FocusCmd) ImplementsCommand() {}
func (SoftKeyboardCmd) ImplementsCommand() {} func (SoftKeyboardCmd) ImplementsCommand() {}
+3 -1
View File
@@ -19,7 +19,9 @@ type Event struct {
Source Source Source Source
// PointerID is the id for the pointer and can be used // PointerID is the id for the pointer and can be used
// to track a particular pointer from Press to // to track a particular pointer from Press to
// Release. // Release. Populated for Press, Release, Move, Drag,
// Enter, Leave, and Cancel; Scroll events are not
// bound to a tracked pointer and leave it zero.
PointerID ID PointerID ID
// Priority is the priority of the receiving handler // Priority is the priority of the receiving handler
// for this event. // for this event.
+12 -14
View File
@@ -103,8 +103,7 @@ func (l *line) insertTrailingSyntheticNewline(newLineClusterIdx int) {
clusterIndex: newLineClusterIdx, clusterIndex: newLineClusterIdx,
glyphCount: 0, glyphCount: 0,
runeCount: 1, runeCount: 1,
xAdvance: 0, advance: 0,
yAdvance: 0,
xOffset: 0, xOffset: 0,
yOffset: 0, yOffset: 0,
} }
@@ -160,9 +159,9 @@ type glyph struct {
// runeCount is the quantity of runes in the source text that this glyph // runeCount is the quantity of runes in the source text that this glyph
// corresponds to. // corresponds to.
runeCount int runeCount int
// xAdvance and yAdvance describe the distance the dot moves when // advance is the distance the dot moves when laying out the glyph along
// laying out the glyph on the X or Y axis. // the run's primary axis.
xAdvance, yAdvance fixed.Int26_6 advance fixed.Int26_6
// xOffset and yOffset describe offsets from the dot that should be // xOffset and yOffset describe offsets from the dot that should be
// applied when rendering the glyph. // applied when rendering the glyph.
xOffset, yOffset fixed.Int26_6 xOffset, yOffset fixed.Int26_6
@@ -270,8 +269,9 @@ func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
// in the order in which they are loaded, with the first face being the default. // in the order in which they are loaded, with the first face being the default.
func (s *shaperImpl) Load(f FontFace) { func (s *shaperImpl) Load(f FontFace) {
desc := opentype.FontToDescription(f.Font) desc := opentype.FontToDescription(f.Font)
s.fontMap.AddFace(f.Face.Face(), fontscan.Location{File: fmt.Sprint(desc)}, desc) face := f.Face.Face()
s.addFace(f.Face.Face(), f.Font) s.fontMap.AddFace(face, fontscan.Location{File: fmt.Sprint(desc)}, desc)
s.addFace(face, f.Font)
} }
func (s *shaperImpl) addFace(f *font.Face, md giofont.Font) { func (s *shaperImpl) addFace(f *font.Face, md giofont.Font) {
@@ -437,8 +437,7 @@ func (s *shaperImpl) shapeText(ppem fixed.Int26_6, lc system.Locale, txt []rune)
Height: input.Size, Height: input.Size,
XBearing: 0, XBearing: 0,
YBearing: 0, YBearing: 0,
XAdvance: input.Size, Advance: input.Size,
YAdvance: input.Size,
XOffset: 0, XOffset: 0,
YOffset: 0, YOffset: 0,
ClusterIndex: input.RunStart, ClusterIndex: input.RunStart,
@@ -854,11 +853,10 @@ func toGioGlyphs(in []shaping.Glyph, ppem fixed.Int26_6, faceIdx int) []glyph {
bounds.Max = bounds.Min.Add(fixed.Point26_6{X: g.Width, Y: -g.Height}) bounds.Max = bounds.Min.Add(fixed.Point26_6{X: g.Width, Y: -g.Height})
out = append(out, glyph{ out = append(out, glyph{
id: newGlyphID(ppem, faceIdx, g.GlyphID), id: newGlyphID(ppem, faceIdx, g.GlyphID),
clusterIndex: g.ClusterIndex, clusterIndex: g.TextIndex(),
runeCount: g.RuneCount, runeCount: g.RunesCount(),
glyphCount: g.GlyphCount, glyphCount: g.GlyphsCount(),
xAdvance: g.XAdvance, advance: g.Advance,
yAdvance: g.YAdvance,
xOffset: g.XOffset, xOffset: g.XOffset,
yOffset: g.YOffset, yOffset: g.YOffset,
bounds: bounds, bounds: bounds,
+3 -7
View File
@@ -259,10 +259,6 @@ func WithCollection(collection []FontFace) ShaperOption {
} }
// NewShaper constructs a shaper with the provided options. // NewShaper constructs a shaper with the provided options.
//
// NewShaper must be called after [app.NewWindow], unless the [NoSystemFonts]
// option is specified. This is an unfortunate restriction caused by some platforms
// such as Android.
func NewShaper(options ...ShaperOption) *Shaper { func NewShaper(options ...ShaperOption) *Shaper {
l := &Shaper{} l := &Shaper{}
for _, opt := range options { for _, opt := range options {
@@ -468,7 +464,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
if rtl { if rtl {
// Modify the advance prior to computing runOffset to ensure that the // Modify the advance prior to computing runOffset to ensure that the
// current glyph's width is subtracted in RTL. // current glyph's width is subtracted in RTL.
l.advance += g.xAdvance l.advance += g.advance
} }
// runOffset computes how far into the run the dot should be positioned. // runOffset computes how far into the run the dot should be positioned.
runOffset := l.advance runOffset := l.advance
@@ -481,7 +477,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
Y: int32(line.yOffset), Y: int32(line.yOffset),
Ascent: line.ascent, Ascent: line.ascent,
Descent: line.descent, Descent: line.descent,
Advance: g.xAdvance, Advance: g.advance,
Runes: uint16(g.runeCount), Runes: uint16(g.runeCount),
Offset: fixed.Point26_6{ Offset: fixed.Point26_6{
X: g.xOffset, X: g.xOffset,
@@ -494,7 +490,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
} }
l.glyph++ l.glyph++
if !rtl { if !rtl {
l.advance += g.xAdvance l.advance += g.advance
} }
endOfRun := l.glyph == len(run.Glyphs) endOfRun := l.glyph == len(run.Glyphs)
+2 -2
View File
@@ -450,8 +450,8 @@ func printLinePositioning(t *testing.T, lines []line, glyphs []Glyph) {
for g := start; ; g += inc { for g := start; ; g += inc {
glyph := run.Glyphs[g] glyph := run.Glyphs[g]
if glyphCursor < len(glyphs) { if glyphCursor < len(glyphs) {
t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - glyphs[%2d] flags %s", g, glyph.xAdvance, glyph.runeCount, glyph.glyphCount, glyphCursor, glyphs[glyphCursor].Flags) t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - glyphs[%2d] flags %s", g, glyph.advance, glyph.runeCount, glyph.glyphCount, glyphCursor, glyphs[glyphCursor].Flags)
t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - n/a", g, glyph.xAdvance, glyph.runeCount, glyph.glyphCount) t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - n/a", g, glyph.advance, glyph.runeCount, glyph.glyphCount)
} }
glyphCursor++ glyphCursor++
if g == end { if g == end {
+30 -2
View File
@@ -25,6 +25,7 @@ import (
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text" "gioui.org/text"
"gioui.org/unit" "gioui.org/unit"
) )
@@ -107,8 +108,9 @@ type imeState struct {
rng key.Range rng key.Range
caret key.Caret caret key.Caret
} }
snippet key.Snippet snippet key.Snippet
start, end int composition key.Range
start, end int
} }
type maskReader struct { type maskReader struct {
@@ -398,9 +400,12 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
case key.FocusEvent: case key.FocusEvent:
// Reset IME state. // Reset IME state.
e.ime.imeState = imeState{} e.ime.imeState = imeState{}
e.ime.composition = key.Range{Start: -1, End: -1}
if ke.Focus && !e.ReadOnly { if ke.Focus && !e.ReadOnly {
gtx.Execute(key.SoftKeyboardCmd{Show: true}) gtx.Execute(key.SoftKeyboardCmd{Show: true})
} }
case key.CompositionEvent:
e.ime.composition = key.Range(ke)
case key.Event: case key.Event:
if !gtx.Focused(e) || ke.State != key.Press { if !gtx.Focused(e) || ke.State != key.Press {
break break
@@ -735,6 +740,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
if e.Len() > 0 { if e.Len() > 0 {
e.paintSelection(gtx, selectMaterial) e.paintSelection(gtx, selectMaterial)
e.paintText(gtx, textMaterial) e.paintText(gtx, textMaterial)
e.paintComposition(gtx, textMaterial)
} }
if gtx.Enabled() { if gtx.Enabled() {
e.paintCaret(gtx, textMaterial) e.paintCaret(gtx, textMaterial)
@@ -759,6 +765,28 @@ func (e *Editor) paintText(gtx layout.Context, material op.CallOp) {
e.text.PaintText(gtx, material) e.text.PaintText(gtx, material)
} }
func (e *Editor) paintComposition(gtx layout.Context, material op.CallOp) {
e.initBuffer()
r := e.ime.composition
if r.Start == -1 || r.Start == r.End {
return
}
e.text.regions = e.text.Regions(r.Start, r.End, e.text.regions)
thickness := max(gtx.Dp(unit.Dp(1)), 1)
for _, region := range e.text.regions {
y := region.Bounds.Max.Y - max(region.Baseline/3, thickness)
underline := image.Rect(region.Bounds.Min.X, y, region.Bounds.Max.X, y+thickness)
underline = underline.Intersect(image.Rectangle{Max: e.text.viewSize})
if underline.Empty() {
continue
}
stack := clip.Rect(underline).Push(gtx.Ops)
material.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
stack.Pop()
}
}
// paintCaret paints the text glyphs using the provided material to set the fill material // paintCaret paints the text glyphs using the provided material to set the fill material
// of the caret rectangle. // of the caret rectangle.
func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) { func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) {
+4 -16
View File
@@ -5,7 +5,6 @@ import (
"image/color" "image/color"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/semantic"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
@@ -86,21 +85,10 @@ func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimension
continue continue
} }
cl := d.Decorations.Clickable(a) cl := d.Decorations.Clickable(a)
dims := cl.Layout(gtx, func(gtx layout.Context) layout.Dimensions { dims := Clickable(gtx, cl, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops) system.ActionInputOp(a).Add(gtx.Ops)
return layout.Background{}.Layout(gtx, paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops)
func(gtx layout.Context) layout.Dimensions { return inset.Layout(gtx, w)
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
for _, c := range cl.History() {
drawInk(gtx, c)
}
return layout.Dimensions{Size: gtx.Constraints.Min}
},
func(gtx layout.Context) layout.Dimensions {
paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops)
return inset.Layout(gtx, w)
},
)
}) })
size.X += dims.Size.X size.X += dims.Size.X
if size.Y < dims.Size.Y { if size.Y < dims.Size.Y {