// Cthulhu screen reader. // Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program. If not, see . package presentation import ( "fmt" "math" "git.stormux.org/storm/cthulhu/internal/atspi" ) // ComboBoxSelectionText presents the current choice without repeating the // control's label or role. Native combo snapshots prefer selected-child text. func ComboBoxSelectionText(snapshot atspi.Snapshot) string { if value := controlValue(snapshot); value != "" { return value } if snapshot.Text != "" { return snapshot.Text } return snapshot.Name } func controlValue(snapshot atspi.Snapshot) string { switch snapshot.Role { case "combo box": if snapshot.ValueText != "" { return snapshot.ValueText } return snapshot.Description case "spin button": if snapshot.Text != "" { return snapshot.Text } if snapshot.ValueText != "" { return snapshot.ValueText } if !snapshot.ValueKnown { return snapshot.Text } case "dial", "progress bar", "scroll bar", "slider", "split pane": default: return "" } if snapshot.ValueText != "" { return snapshot.ValueText } if !snapshot.ValueKnown || math.IsNaN(snapshot.CurrentValue) || math.IsInf(snapshot.CurrentValue, 0) { return "" } if snapshot.Role != "progress bar" { // Python's textForValue formats the actual value using the provider's // minimum increment, rather than normalizing controls to percentages. increment := snapshot.MinimumIncrement if increment < 0 || math.IsNaN(increment) || math.IsInf(increment, 0) { return "" } places := 0 if increment > 0 { places = int(math.Ceil(math.Max(0, -math.Log10(increment)))) } else if math.Abs(snapshot.CurrentValue) < 1 { places = 1 } return fmt.Sprintf("%.*f", places, snapshot.CurrentValue) } if snapshot.MaximumValue == snapshot.MinimumValue { return "" } percentage := math.Round((snapshot.CurrentValue - snapshot.MinimumValue) / (snapshot.MaximumValue - snapshot.MinimumValue) * 100) return fmt.Sprintf("%.0f percent", percentage) } func controlStates(snapshot atspi.Snapshot) []string { var states []string switch snapshot.ControlRole() { case "check box", "check menu item": switch { case snapshot.Indeterminate: states = append(states, "partially checked") case snapshot.Checked: states = append(states, "checked") default: states = append(states, "not checked") } case "radio button", "radio menu item": if snapshot.Checked { states = append(states, "selected") } else { states = append(states, "not selected") } case "switch": if snapshot.Checked { states = append(states, "on") } else { states = append(states, "off") } case "toggle button": if snapshot.Checked { states = append(states, "pressed") } else { states = append(states, "not pressed") } } if snapshot.Expandable { if snapshot.Expanded { states = append(states, "expanded") } else { states = append(states, "collapsed") } } if snapshot.Selected && presentsSelectedState(snapshot.Role) { states = append(states, "selected") } if snapshot.Multiselectable { states = append(states, "multi-select") } if snapshot.ReadOnly { states = append(states, "read only") } if snapshot.Required { states = append(states, "required") } if snapshot.Invalid { states = append(states, "invalid entry") } if snapshot.SensitiveKnown && !snapshot.Sensitive { states = append(states, "grayed") } return states } func presentsSelectedState(role string) bool { switch role { case "icon", "list item", "tree item": return true default: return false } } func stateChangeText(snapshot atspi.Snapshot, eventType string) string { switch eventType { case "object:state-changed:checked", "object:state-changed:indeterminate", "object:state-changed:pressed": for _, state := range controlStates(snapshot) { switch state { case "partially checked", "checked", "not checked", "selected", "not selected", "on", "off", "pressed", "not pressed": return state } } case "object:state-changed:expanded": if snapshot.Expanded { return "expanded" } return "collapsed" case "object:state-changed:invalid-entry": if snapshot.Invalid { return "invalid entry" } case "object:state-changed:read-only": if snapshot.ReadOnly { return "read only" } return "editable" case "object:state-changed:required": if snapshot.Required { return "required" } case "object:state-changed:selected": if snapshot.Role == "table cell" { if snapshot.Selected { return "selected" } return "" } if presentsSelectedState(snapshot.Role) { if snapshot.Selected { return "selected" } return "not selected" } case "object:state-changed:sensitive": if snapshot.SensitiveKnown && !snapshot.Sensitive { return "grayed" } } return "" }