use accesskit::{Live, NodeId, Toggled};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum LiveSetting {
#[default]
Off,
Polite,
Assertive,
}
impl From<LiveSetting> for Live {
#[inline]
fn from(value: LiveSetting) -> Self {
match value {
LiveSetting::Off => Live::Off,
LiveSetting::Polite => Live::Polite,
LiveSetting::Assertive => Live::Assertive,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum Toggled3 {
#[default]
False,
True,
Mixed,
}
impl From<bool> for Toggled3 {
#[inline]
fn from(b: bool) -> Self {
if b {
Toggled3::True
} else {
Toggled3::False
}
}
}
impl From<Toggled3> for Toggled {
#[inline]
fn from(value: Toggled3) -> Self {
match value {
Toggled3::False => Toggled::False,
Toggled3::True => Toggled::True,
Toggled3::Mixed => Toggled::Mixed,
}
}
}
pub type CheckedState = Toggled3;
impl From<&CheckedState> for Toggled3 {
#[inline]
fn from(c: &CheckedState) -> Toggled3 {
*c
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct TextCaret {
pub start: usize,
pub end: usize,
}
impl TextCaret {
#[inline]
pub const fn caret(pos: usize) -> Self {
Self {
start: pos,
end: pos,
}
}
#[inline]
pub const fn range(start: usize, end: usize) -> Self {
Self { start, end }
}
#[inline]
pub fn lo(&self) -> usize {
core::cmp::min(self.start, self.end)
}
#[inline]
pub fn hi(&self) -> usize {
core::cmp::max(self.start, self.end)
}
#[inline]
pub fn is_caret(&self) -> bool {
self.start == self.end
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct TextSelection {
pub anchor: usize,
pub focus: usize,
}
impl TextSelection {
#[inline]
pub const fn caret(pos: usize) -> Self {
Self {
anchor: pos,
focus: pos,
}
}
#[inline]
pub fn is_caret(&self) -> bool {
self.anchor == self.focus
}
}
#[derive(Debug, Clone, Default)]
pub struct TextRunChild {
pub text: String,
pub char_offset: usize,
pub byte_offset: usize,
pub is_selected: bool,
}
#[derive(Debug, Clone, Default)]
pub struct A11yNodeProps {
pub description: Option<String>,
pub placeholder: Option<String>,
pub key_shortcut: Option<String>,
pub disabled: bool,
pub expanded: Option<bool>,
pub selected: Option<bool>,
pub checked: Option<CheckedState>,
pub value_now: Option<f64>,
pub value_min: Option<f64>,
pub value_max: Option<f64>,
pub value_step: Option<f64>,
pub text_value: Option<String>,
pub text_selection: Option<TextSelection>,
pub labelled_by: Vec<NodeId>,
pub described_by: Vec<NodeId>,
pub controlled_by: Vec<NodeId>,
pub owns: Vec<NodeId>,
pub text_run_children: Vec<TextRunChild>,
pub tab_index: Option<u32>,
}
pub fn character_lengths_utf8(text: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(text.len());
for ch in text.chars() {
let len = ch.len_utf8() as u8;
out.push(len);
}
out
}
pub fn byte_offset_to_char_index(text: &str, byte_offset: usize) -> usize {
if byte_offset == 0 {
return 0;
}
let mut chars = 0usize;
let mut current_byte = 0usize;
for ch in text.chars() {
if current_byte >= byte_offset {
return chars;
}
current_byte += ch.len_utf8();
chars += 1;
}
chars
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn live_setting_maps_to_accesskit_live() {
assert!(matches!(Live::from(LiveSetting::Off), Live::Off));
assert!(matches!(Live::from(LiveSetting::Polite), Live::Polite));
assert!(matches!(
Live::from(LiveSetting::Assertive),
Live::Assertive
));
}
#[test]
fn toggled3_from_bool() {
assert_eq!(Toggled3::from(true), Toggled3::True);
assert_eq!(Toggled3::from(false), Toggled3::False);
}
#[test]
fn toggled3_maps_to_accesskit_toggled() {
assert!(matches!(Toggled::from(Toggled3::False), Toggled::False));
assert!(matches!(Toggled::from(Toggled3::True), Toggled::True));
assert!(matches!(Toggled::from(Toggled3::Mixed), Toggled::Mixed));
}
#[test]
fn text_caret_helpers() {
let c = TextCaret::caret(5);
assert!(c.is_caret());
assert_eq!(c.lo(), 5);
assert_eq!(c.hi(), 5);
let s = TextCaret::range(2, 9);
assert!(!s.is_caret());
assert_eq!(s.lo(), 2);
assert_eq!(s.hi(), 9);
let r = TextCaret::range(9, 2);
assert_eq!(r.lo(), 2);
assert_eq!(r.hi(), 9);
}
#[test]
fn text_selection_caret() {
let sel = TextSelection::caret(10);
assert!(sel.is_caret());
assert_eq!(sel.anchor, 10);
assert_eq!(sel.focus, 10);
}
#[test]
fn text_selection_range() {
let sel = TextSelection {
anchor: 3,
focus: 7,
};
assert!(!sel.is_caret());
}
#[test]
fn character_lengths_ascii() {
let v = character_lengths_utf8("hello");
assert_eq!(v, vec![1u8, 1, 1, 1, 1]);
}
#[test]
fn character_lengths_multibyte() {
let v = character_lengths_utf8("héllo");
assert_eq!(v, vec![1u8, 2, 1, 1, 1]);
}
#[test]
fn character_lengths_emoji() {
let v = character_lengths_utf8("a🦀b");
assert_eq!(v, vec![1u8, 4, 1]);
}
#[test]
fn character_lengths_empty() {
let v = character_lengths_utf8("");
assert!(v.is_empty());
}
#[test]
fn byte_offset_to_char_index_ascii() {
assert_eq!(byte_offset_to_char_index("hello", 0), 0);
assert_eq!(byte_offset_to_char_index("hello", 1), 1);
assert_eq!(byte_offset_to_char_index("hello", 5), 5);
assert_eq!(byte_offset_to_char_index("hello", 999), 5);
}
#[test]
fn byte_offset_to_char_index_multibyte() {
assert_eq!(byte_offset_to_char_index("héllo", 0), 0);
assert_eq!(byte_offset_to_char_index("héllo", 1), 1); assert_eq!(byte_offset_to_char_index("héllo", 3), 2); assert_eq!(byte_offset_to_char_index("héllo", 6), 5); }
#[test]
fn a11y_node_props_default_is_empty() {
let props = A11yNodeProps::default();
assert!(props.description.is_none());
assert!(props.placeholder.is_none());
assert!(props.key_shortcut.is_none());
assert!(!props.disabled);
assert!(props.expanded.is_none());
assert!(props.selected.is_none());
assert!(props.checked.is_none());
assert!(props.value_now.is_none());
assert!(props.value_min.is_none());
assert!(props.value_max.is_none());
assert!(props.value_step.is_none());
assert!(props.labelled_by.is_empty());
assert!(props.described_by.is_empty());
assert!(props.controlled_by.is_empty());
assert!(props.owns.is_empty());
}
}