95 lines
3.1 KiB
Go
95 lines
3.1 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
)
|
|
|
|
// Inline completions are selected by the browser. Echo the inserted portion
|
|
// outside that selection; it contains the user's edit, not the suggested suffix.
|
|
func autocompleteInsertion(event atspi.Event) string {
|
|
text := []rune(event.Payload)
|
|
start, end := event.Source.SelectionStart, event.Source.SelectionEnd
|
|
if end <= start || end <= event.Detail1 || start >= event.Detail1+len(text) {
|
|
return event.Payload
|
|
}
|
|
left := max(0, start-event.Detail1)
|
|
right := min(len(text), end-event.Detail1)
|
|
return string(text[:left]) + string(text[right:])
|
|
}
|
|
|
|
func recentInput(source any) input.Activity {
|
|
if provider, ok := source.(interface{ RecentInput() input.Activity }); ok {
|
|
return provider.RecentInput()
|
|
}
|
|
return input.Activity{}
|
|
}
|
|
|
|
type autocompleteEcho struct {
|
|
sequence uint64
|
|
source atspi.ObjectID
|
|
}
|
|
|
|
// autocompleteAvailability presents the selected address when it changes.
|
|
// Chromium can send several insertion and popup events for the same suggestion.
|
|
type autocompleteAvailability struct {
|
|
entry atspi.ObjectID
|
|
lastAddress string
|
|
}
|
|
|
|
func supportsAutocomplete(snapshot atspi.Snapshot) bool {
|
|
return snapshot.Autocomplete || snapshot.Role == "autocomplete"
|
|
}
|
|
|
|
func (availability *autocompleteAvailability) focus(event atspi.Event) {
|
|
if !isFocusGainEvent(event) || event.Source.AutocompletePopup {
|
|
return
|
|
}
|
|
entry := atspi.ObjectID{}
|
|
if supportsAutocomplete(event.Source) {
|
|
entry = event.Source.ID()
|
|
}
|
|
if entry != availability.entry {
|
|
availability.entry = entry
|
|
availability.lastAddress = ""
|
|
}
|
|
}
|
|
|
|
func (availability *autocompleteAvailability) offered(event atspi.Event, activity input.Activity) string {
|
|
if !availability.entry.Valid() || event.Type != atspi.TextInsertedEvent ||
|
|
!supportsAutocomplete(event.Source) || event.Source.ID() != availability.entry ||
|
|
(activity.Intent != input.TypingIntent && activity.Intent != input.DeleteIntent) {
|
|
return ""
|
|
}
|
|
address := strings.TrimSpace(event.Source.Text)
|
|
if event.Source.SelectionEnd <= event.Source.SelectionStart ||
|
|
event.Source.SelectionEnd > len([]rune(address)) || address == "" ||
|
|
address == availability.lastAddress {
|
|
return ""
|
|
}
|
|
availability.lastAddress = address
|
|
return address
|
|
}
|
|
|
|
func (echo *autocompleteEcho) insertion(event atspi.Event, activity input.Activity) string {
|
|
text := autocompleteInsertion(event)
|
|
if text == "" && activity.Sequence != 0 &&
|
|
(activity.Sequence != echo.sequence || event.Source.ID() != echo.source) &&
|
|
event.Detail1 == event.Source.SelectionStart {
|
|
// Chromium can retain an unchanged typed prefix and emit only a replacement
|
|
// of the selected suffix. Recover that character from accessible text once
|
|
// for this key press; a normal insertion already covers the other case.
|
|
text = event.Source.CompletionPrefixCharacter
|
|
}
|
|
if text != "" {
|
|
echo.sequence, echo.source = activity.Sequence, event.Source.ID()
|
|
}
|
|
return text
|
|
}
|