15 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
16 changed files with 178 additions and 100 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
+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 {
+3 -1
View File
@@ -760,6 +760,8 @@ 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
@@ -823,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
+39
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
+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() {}
+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 -3
View File
@@ -464,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
@@ -477,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,
@@ -490,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 {