Add shared internal UI toolkit for both readers
This commit is contained in:
1 parent
672caca854
commit
e49ba198d7
34 files changed
+3471
No files matched your search
@@ -0,0 +1,107 @@
|
||||
// Fenrir console screen reader.
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package internalui
|
||||
|
||||
import (
|
||||
gridmodel "git.stormux.org/storm/screenreader-common/internalui/grid"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Grid selections index Choices, even while the visible results are filtered.
|
||||
// Choice IDs remain opaque to the model and can carry a host-owned value.
|
||||
func (s *Session) grid_matches(c Control) []int {
|
||||
var matches []int
|
||||
prefix := strings.ToLower(c.Text)
|
||||
for index, choice := range c.Choices {
|
||||
if !choice.Disabled && strings.HasPrefix(strings.ToLower(s.choice_label(choice)), prefix) {
|
||||
matches = append(matches, index)
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func (s *Session) filter_grid(c *Control) {
|
||||
c.Selected = -1
|
||||
if matches := s.grid_matches(*c); len(matches) > 0 {
|
||||
c.Selected = matches[0]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) describe_grid(c Control) string {
|
||||
matches := s.grid_matches(c)
|
||||
for position, index := range matches {
|
||||
if index == c.Selected {
|
||||
label := s.choice_label(c.Choices[index])
|
||||
return gridmodel.Describe(label, position, len(matches), c.Columns, s.announceGridCoordinates, s.announceItemPosition, s.messages)
|
||||
}
|
||||
}
|
||||
return s.messages.T("No matches")
|
||||
}
|
||||
|
||||
func (s *Session) grid(c *Control, key Key) Result {
|
||||
if key.Ctrl && key.Name != "backspace" {
|
||||
return Result{}
|
||||
}
|
||||
if key.Name == "backspace" || (key.Name == "" || key.Name == "space") && key.Text != "" && !key.Ctrl {
|
||||
runes := []rune(c.Text)
|
||||
if key.Name == "backspace" {
|
||||
if key.Ctrl {
|
||||
runes = nil
|
||||
} else if len(runes) > 0 {
|
||||
runes = runes[:len(runes)-1]
|
||||
}
|
||||
} else {
|
||||
if !utf8.ValidString(key.Text) {
|
||||
return Result{}
|
||||
}
|
||||
for _, char := range key.Text {
|
||||
if !unicode.IsPrint(char) {
|
||||
return Result{}
|
||||
}
|
||||
}
|
||||
if len(runes)+len([]rune(key.Text)) > 4096 {
|
||||
return Result{}
|
||||
}
|
||||
runes = append(runes, []rune(key.Text)...)
|
||||
}
|
||||
c.Text = string(runes)
|
||||
s.filter_grid(c)
|
||||
query := s.messages.T("Search cleared")
|
||||
if c.Text != "" {
|
||||
query = s.messages.Format("Search {0}", c.Text)
|
||||
}
|
||||
return Result{Speech: query + ". " + s.describe_grid(*c)}
|
||||
}
|
||||
matches := s.grid_matches(*c)
|
||||
if len(matches) == 0 {
|
||||
return Result{Speech: s.messages.T("No matches")}
|
||||
}
|
||||
position := 0
|
||||
for current, index := range matches {
|
||||
if index == c.Selected {
|
||||
position = current
|
||||
break
|
||||
}
|
||||
}
|
||||
if key.Name == "enter" {
|
||||
return Result{Action: c.Action}
|
||||
}
|
||||
if key.Name == "delete" {
|
||||
if key.Repeat {
|
||||
return Result{}
|
||||
}
|
||||
return Result{Action: c.DeleteAction}
|
||||
}
|
||||
var handled bool
|
||||
position, handled = gridmodel.Move(position, len(matches), c.Columns, key.Name)
|
||||
if !handled {
|
||||
return Result{}
|
||||
}
|
||||
|
||||
c.Selected = matches[position]
|
||||
return Result{Speech: s.describe_grid(*c)}
|
||||
}
|
||||
Reference in new issue
Block a user