8 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
6 changed files with 80 additions and 22 deletions
+4 -2
View File
@@ -207,7 +207,9 @@ const (
CFS_POINT = 0x0002
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
HTCLIENT = 1
@@ -782,7 +784,7 @@ func SetWindowPlacement(hwnd syscall.Handle, wp *WindowPlacement) {
_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),
uintptr(x), uintptr(y),
uintptr(dx), uintptr(dy),
+27 -11
View File
@@ -412,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))
windows.ImmSetCompositionWindow(imc, icaret.X, icaret.Y)
windows.ImmSetCandidateWindow(imc, icaret.X, icaret.Y)
return windows.TRUE
case windows.WM_IME_COMPOSITION:
imc := windows.ImmGetContext(w.hwnd)
if imc == 0 {
@@ -698,7 +699,13 @@ func (w *window) 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 {
return err
}
@@ -713,13 +720,17 @@ func (w *window) readClipboard() error {
}
defer windows.GlobalUnlock(mem)
content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
w.processDataEvent(content)
return nil
}
func (w *window) processDataEvent(content string) {
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
return nil
}
func (w *window) Configure(options []Option) {
@@ -736,7 +747,16 @@ func (w *window) Configure(options []Option) {
style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE)
var showMode 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)
style &^= winStyle
switch cnf.Mode {
@@ -788,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
// 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.ShowWindow(w.hwnd, showMode)
}
@@ -909,19 +929,15 @@ func (w *window) Perform(acts system.Action) {
y := (mi.Bottom - mi.Top - dy) / 2
windows.SetWindowPos(w.hwnd, 0, x, y, dx, dy, windows.SWP_NOZORDER|windows.SWP_FRAMECHANGED)
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:
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) {
if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' {
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) {
c.w.imeState.compose = r
c.w.driver.ProcessEvent(key.CompositionEvent(r))
}
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 are only supported on MacOS currently.
// TopMost windows are supported on macOS, Windows.
func TopMost(enabled bool) Option {
return func(_ unit.Metric, cnf *Config) {
cnf.TopMost = enabled
+8 -1
View File
@@ -419,7 +419,7 @@ func (f *filter) Merge(f2 filter) {
func (f *filter) Matches(e event.Event) bool {
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
default:
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})
}
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:
var evts []taggedEvent
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.
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
// input method.
type SnippetEvent Range
@@ -243,11 +246,12 @@ func (h InputHintOp) Add(o *op.Ops) {
data[1] = byte(h.Hint)
}
func (EditEvent) ImplementsEvent() {}
func (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {}
func (SnippetEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {}
func (EditEvent) ImplementsEvent() {}
func (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {}
func (CompositionEvent) ImplementsEvent() {}
func (SnippetEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {}
func (FocusCmd) ImplementsCommand() {}
func (SoftKeyboardCmd) ImplementsCommand() {}
+30 -2
View File
@@ -25,6 +25,7 @@ import (
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
)
@@ -107,8 +108,9 @@ type imeState struct {
rng key.Range
caret key.Caret
}
snippet key.Snippet
start, end int
snippet key.Snippet
composition key.Range
start, end int
}
type maskReader struct {
@@ -398,9 +400,12 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
case key.FocusEvent:
// Reset IME state.
e.ime.imeState = imeState{}
e.ime.composition = key.Range{Start: -1, End: -1}
if ke.Focus && !e.ReadOnly {
gtx.Execute(key.SoftKeyboardCmd{Show: true})
}
case key.CompositionEvent:
e.ime.composition = key.Range(ke)
case key.Event:
if !gtx.Focused(e) || ke.State != key.Press {
break
@@ -735,6 +740,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
if e.Len() > 0 {
e.paintSelection(gtx, selectMaterial)
e.paintText(gtx, textMaterial)
e.paintComposition(gtx, textMaterial)
}
if gtx.Enabled() {
e.paintCaret(gtx, textMaterial)
@@ -759,6 +765,28 @@ func (e *Editor) paintText(gtx layout.Context, material op.CallOp) {
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
// of the caret rectangle.
func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) {