use std::sync::Arc;
use cosmic_text::Buffer;
use unicode_segmentation::UnicodeSegmentation;
use crate::display::{Clip, NO_CLIP};
use crate::edit::EditStore;
use crate::geom::{Rect, Size, Vec2};
use crate::key::Key;
use crate::scroll::ScrollStore;
use crate::spec::NodeSpec;
use crate::text::TextSystem;
use crate::tree::{NIL, NodeContent, OriginId, Tree};
use crate::value::{Handles, Value};
use crate::window::{WindowButton, WindowRole};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Role {
None,
Button,
Checkbox,
Radio,
Switch,
Slider,
Tab,
TabList,
Link,
Heading,
List,
ListItem,
Image,
Dialog,
Group,
Window,
TitleBar,
StaticText,
TextInput,
MultilineTextInput,
ScrollView,
Line,
RadioGroup,
Menu,
MenuItem,
Terminal,
}
impl Role {
pub fn name(self) -> &'static str {
match self {
Role::None => "none",
Role::Button => "button",
Role::Checkbox => "checkbox",
Role::Radio => "radio",
Role::Switch => "switch",
Role::Slider => "slider",
Role::Tab => "tab",
Role::TabList => "tabList",
Role::Link => "link",
Role::Heading => "heading",
Role::List => "list",
Role::ListItem => "listItem",
Role::Image => "image",
Role::Dialog => "dialog",
Role::Group => "group",
Role::RadioGroup => "radioGroup",
Role::Menu => "menu",
Role::MenuItem => "menuItem",
Role::Terminal => "terminal",
Role::Window => "window",
Role::TitleBar => "titleBar",
Role::StaticText => "staticText",
Role::TextInput => "textInput",
Role::MultilineTextInput => "multilineTextInput",
Role::ScrollView => "scrollView",
Role::Line => "line",
}
}
pub fn parse(name: &str) -> Option<Role> {
Role::ALL.iter().copied().find(|r| r.name() == name)
}
pub const ALL: [Role; 26] = [
Role::None,
Role::Button,
Role::Checkbox,
Role::Radio,
Role::Switch,
Role::Slider,
Role::Tab,
Role::TabList,
Role::Link,
Role::Heading,
Role::List,
Role::ListItem,
Role::Image,
Role::Dialog,
Role::Group,
Role::Window,
Role::TitleBar,
Role::StaticText,
Role::TextInput,
Role::MultilineTextInput,
Role::ScrollView,
Role::Line,
Role::RadioGroup,
Role::Menu,
Role::MenuItem,
Role::Terminal,
];
pub fn is_control(self) -> bool {
matches!(
self,
Role::Button
| Role::Checkbox
| Role::Radio
| Role::Switch
| Role::Slider
| Role::Tab
| Role::MenuItem
| Role::Link
| Role::TextInput
| Role::MultilineTextInput
)
}
pub fn is_editor(self) -> bool {
matches!(self, Role::TextInput | Role::MultilineTextInput)
}
pub(crate) fn presentational(self) -> bool {
matches!(
self,
Role::Button
| Role::Checkbox
| Role::Radio
| Role::Switch
| Role::Slider
| Role::Tab
| Role::MenuItem
| Role::Link
| Role::Heading
| Role::Image
)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Live {
#[default]
Off,
Polite,
Assertive,
}
impl Live {
pub const ALL: &'static [Live] = &[Live::Off, Live::Polite, Live::Assertive];
pub fn name(self) -> &'static str {
match self {
Live::Off => "off",
Live::Polite => "polite",
Live::Assertive => "assertive",
}
}
pub fn from_index(i: usize) -> Live {
Self::ALL.get(i).copied().unwrap_or_default()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Announcement {
pub text: String,
pub live: Live,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Orientation {
Horizontal,
Vertical,
}
impl Orientation {
pub fn name(self) -> &'static str {
match self {
Orientation::Horizontal => "horizontal",
Orientation::Vertical => "vertical",
}
}
pub fn parse(name: &str) -> Option<Orientation> {
Orientation::ALL.iter().copied().find(|o| o.name() == name)
}
pub const ALL: [Orientation; 2] = [Orientation::Horizontal, Orientation::Vertical];
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AccessAction {
Click,
Focus,
Blur,
SetValue,
Increment,
Decrement,
ScrollIntoView,
ScrollUp,
ScrollDown,
ScrollLeft,
ScrollRight,
SetTextSelection,
ReplaceSelectedText,
}
impl AccessAction {
pub const ALL: [AccessAction; 13] = [
AccessAction::Click,
AccessAction::Focus,
AccessAction::Blur,
AccessAction::SetValue,
AccessAction::Increment,
AccessAction::Decrement,
AccessAction::ScrollIntoView,
AccessAction::ScrollUp,
AccessAction::ScrollDown,
AccessAction::ScrollLeft,
AccessAction::ScrollRight,
AccessAction::SetTextSelection,
AccessAction::ReplaceSelectedText,
];
pub fn bit(self) -> u32 {
1 << (self as u32)
}
pub fn name(self) -> &'static str {
match self {
AccessAction::Click => "click",
AccessAction::Focus => "focus",
AccessAction::Blur => "blur",
AccessAction::SetValue => "setValue",
AccessAction::Increment => "increment",
AccessAction::Decrement => "decrement",
AccessAction::ScrollIntoView => "scrollIntoView",
AccessAction::ScrollUp => "scrollUp",
AccessAction::ScrollDown => "scrollDown",
AccessAction::ScrollLeft => "scrollLeft",
AccessAction::ScrollRight => "scrollRight",
AccessAction::SetTextSelection => "setTextSelection",
AccessAction::ReplaceSelectedText => "replaceSelectedText",
}
}
pub fn parse(name: &str) -> Option<AccessAction> {
AccessAction::ALL.iter().copied().find(|a| a.name() == name)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TextPos {
pub run: Key,
pub character: usize,
}
impl TextPos {
pub fn to_value(self, h: Handles) -> Value {
Value::map([
("run", (h.key)(self.run)),
("character", Value::Int(self.character as i64)),
])
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AccessRequest {
pub key: Key,
pub action: AccessAction,
pub value: Option<String>,
pub anchor: Option<TextPos>,
pub focus: Option<TextPos>,
}
impl AccessRequest {
pub fn new(key: Key, action: AccessAction) -> Self {
AccessRequest {
key,
action,
value: None,
anchor: None,
focus: None,
}
}
pub fn with_value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
pub fn with_selection(mut self, anchor: TextPos, focus: TextPos) -> Self {
self.anchor = Some(anchor);
self.focus = Some(focus);
self
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ScrollState {
pub x: f32,
pub y: f32,
pub max_x: f32,
pub max_y: f32,
}
impl ScrollState {
pub fn to_value(self) -> Value {
Value::map([
("x", Value::float(self.x)),
("y", Value::float(self.y)),
("max_x", Value::float(self.max_x)),
("max_y", Value::float(self.max_y)),
])
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AccessRun {
pub key: Key,
pub line: usize,
pub start: usize,
pub end: usize,
pub text: String,
pub rect: Rect,
pub char_lengths: Vec<u8>,
pub char_positions: Vec<f32>,
pub char_widths: Vec<f32>,
pub word_starts: Vec<u8>,
pub rtl: bool,
}
impl AccessRun {
pub fn to_value(&self, h: Handles) -> Value {
let bytes = |v: &[u8]| Value::list(v.iter().map(|b| Value::Int(*b as i64)));
Value::map([
("key", (h.key)(self.key)),
("line", Value::Int(self.line as i64)),
("start", Value::Int(self.start as i64)),
("end", Value::Int(self.end as i64)),
("text", Value::Str(self.text.clone())),
("rect", self.rect.to_value()),
("char_lengths", bytes(&self.char_lengths)),
("char_positions", Value::floats(&self.char_positions)),
("char_widths", Value::floats(&self.char_widths)),
("word_starts", bytes(&self.word_starts)),
("rtl", Value::Bool(self.rtl)),
])
}
}
pub const RUN_CHARS: usize = 200;
#[derive(Clone, Debug, PartialEq)]
pub struct AccessNode {
pub key: Key,
pub parent: Option<Key>,
pub origin: OriginId,
pub role: Role,
pub name: Option<String>,
pub description: Option<String>,
pub rect: Rect,
pub value: Option<String>,
pub caret: Option<usize>,
pub selection: Option<(usize, usize)>,
pub runs: Vec<AccessRun>,
pub anchor: Option<TextPos>,
pub focus: Option<TextPos>,
pub checked: Option<bool>,
pub mixed: bool,
pub selected: Option<bool>,
pub expanded: Option<bool>,
pub pos_in_set: Option<usize>,
pub orientation: Option<Orientation>,
pub set_size: Option<usize>,
pub number: Option<f32>,
pub min: Option<f32>,
pub max: Option<f32>,
pub step: Option<f32>,
pub focused: bool,
pub disabled: bool,
pub modal: bool,
pub scroll: Option<ScrollState>,
pub actions: u32,
pub live: Live,
}
impl AccessNode {
pub fn to_value(&self, h: Handles) -> Value {
Value::map([
("key", (h.key)(self.key)),
("parent", h.opt_key(self.parent)),
("origin", Value::Int(self.origin.0 as i64)),
("role", Value::str(self.role.name())),
("name", Value::opt_str(&self.name)),
("description", Value::opt_str(&self.description)),
("rect", self.rect.to_value()),
("value", Value::opt_str(&self.value)),
("caret", Value::opt_usize(self.caret)),
(
"selection",
Value::opt(self.selection, |(a, b)| {
Value::list([Value::Int(a as i64), Value::Int(b as i64)])
}),
),
("anchor", Value::opt(self.anchor, |p| p.to_value(h))),
("focus", Value::opt(self.focus, |p| p.to_value(h))),
("runs", Value::list(self.runs.iter().map(|r| r.to_value(h)))),
("checked", Value::opt_bool(self.checked)),
("mixed", Value::Bool(self.mixed)),
("selected", Value::opt_bool(self.selected)),
("expanded", Value::opt_bool(self.expanded)),
("pos_in_set", Value::opt_usize(self.pos_in_set)),
("set_size", Value::opt_usize(self.set_size)),
(
"orientation",
Value::opt(self.orientation, |o| Value::str(o.name())),
),
("live", Value::str(self.live.name())),
("value_now", Value::opt_float(self.number)),
("value_min", Value::opt_float(self.min)),
("value_max", Value::opt_float(self.max)),
("value_step", Value::opt_float(self.step)),
("focused", Value::Bool(self.focused)),
("disabled", Value::Bool(self.disabled)),
("modal", Value::Bool(self.modal)),
("scroll", Value::opt(self.scroll, ScrollState::to_value)),
(
"actions",
Value::list(self.action_list().into_iter().map(|a| Value::str(a.name()))),
),
])
}
pub fn supports(&self, action: AccessAction) -> bool {
self.actions & action.bit() != 0
}
pub fn action_list(&self) -> Vec<AccessAction> {
AccessAction::ALL
.iter()
.copied()
.filter(|a| self.supports(*a))
.collect()
}
pub fn line_offset(&self, pos: TextPos) -> Option<(usize, usize)> {
let run = self.runs.iter().find(|r| r.key == pos.run)?;
let within = run
.text
.char_indices()
.nth(pos.character)
.map_or(run.text.len(), |(b, _)| b);
Some((run.line, run.start + within.min(run.end - run.start)))
}
pub fn text_pos(&self, line: usize, offset: usize) -> Option<TextPos> {
let chars_before =
|r: &AccessRun, at: usize| r.text[..r.text.floor_char_boundary(at)].chars().count();
let mut last = None;
for r in self.runs.iter().filter(|r| r.line == line) {
if offset >= r.start && offset < r.end {
return Some(TextPos {
run: r.key,
character: chars_before(r, offset - r.start),
});
}
last = Some(r);
}
let r = last?;
Some(TextPos {
run: r.key,
character: chars_before(r, (offset.max(r.start) - r.start).min(r.end - r.start)),
})
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct AccessTree {
pub nodes: Vec<AccessNode>,
pub focus: Option<Key>,
pub hash: u64,
}
impl AccessTree {
pub fn to_value(&self, h: Handles) -> Value {
Value::map([
(
"nodes",
Value::list(self.nodes.iter().map(|n| n.to_value(h))),
),
("focus", h.opt_key(self.focus)),
("hash", Value::Str(format!("{:016x}", self.hash))),
])
}
pub fn get(&self, key: Key) -> Option<&AccessNode> {
self.nodes.iter().find(|n| n.key == key)
}
pub fn children(&self, key: Key) -> impl Iterator<Item = &AccessNode> {
self.nodes.iter().filter(move |n| n.parent == Some(key))
}
pub fn root(&self) -> Option<&AccessNode> {
self.nodes.first()
}
}
pub(crate) struct Semantic {
pub role: Role,
pub name: Option<String>,
pub presentational: bool,
}
pub(crate) fn derived_role(tree: &Tree, i: usize) -> Option<Role> {
let spec = &tree.specs[i];
if let Some(r) = spec.access().role {
return (r != Role::Line).then_some(r);
}
if i == 0 {
return Some(Role::Window);
}
match tree.content[i] {
NodeContent::Text(_) => return Some(Role::StaticText),
NodeContent::Edit(_) => return Some(Role::TextInput),
NodeContent::Image(..) => return Some(Role::Image),
NodeContent::Line(_) | NodeContent::Polygon(_) | NodeContent::Path(_) => {}
NodeContent::Cells(_) => return Some(Role::Terminal),
NodeContent::Fragment(_) => {}
NodeContent::Container => {}
}
match spec.window {
Some(WindowRole::Drag) => return Some(Role::TitleBar),
Some(WindowRole::Button(_)) => return Some(Role::Button),
None => {}
}
if spec.events().modal.is_some() {
return Some(Role::Dialog);
}
if spec.events().on_click.is_some() {
return Some(Role::Button);
}
if spec.layout.scroll_x || spec.layout.scroll_y {
return Some(Role::ScrollView);
}
if spec.events().on_key.is_some() || spec.focusable {
return Some(Role::Group);
}
if spec.access().live != Live::Off {
return Some(Role::Group);
}
None
}
pub(crate) fn is_custom_editor(tree: &Tree, i: usize) -> bool {
tree.specs[i].access().role.is_some_and(Role::is_editor)
&& !matches!(tree.content[i], NodeContent::Edit(_))
}
pub(crate) fn focusable(tree: &Tree, i: usize) -> bool {
let spec = &tree.specs[i];
if spec.disabled || spec.access().role == Some(Role::None) || spec.window.is_some() {
return false;
}
if spec.focusable
|| spec.events().on_key.is_some()
|| matches!(tree.content[i], NodeContent::Edit(_))
{
return true;
}
match spec.access().role {
Some(r) => r.is_control(),
None => spec.events().on_click.is_some(),
}
}
pub(crate) fn semantic(
tree: &Tree,
text: &TextSystem,
edit: &EditStore,
title: Option<&str>,
i: usize,
) -> Option<Semantic> {
let mut role = derived_role(tree, i)?;
let spec = &tree.specs[i];
if role == Role::TextInput
&& let NodeContent::Edit(key) = tree.content[i]
&& edit.is_multiline(key)
{
role = Role::MultilineTextInput;
}
let presentational = role.presentational()
|| spec.access().live != Live::Off
|| matches!(spec.window, Some(WindowRole::Button(_)))
|| is_custom_editor(tree, i);
let name = match (&spec.access().label, spec.window) {
(Some(label), _) => Some(label.to_string()),
(None, Some(WindowRole::Button(b))) => Some(
match b {
WindowButton::Close => "Close",
WindowButton::Minimize => "Minimize",
WindowButton::Maximize => "Maximize",
}
.to_string(),
),
(None, _) => match role {
Role::StaticText => match tree.content[i] {
NodeContent::Text(id) => Some(text.content(id).to_string()),
_ => None,
},
Role::Window => title.map(str::to_string),
_ if role.presentational() || spec.access().live != Live::Off => {
content_name(tree, text, i)
}
_ => None,
},
};
Some(Semantic {
role,
name,
presentational,
})
}
pub(crate) fn live_region_speaks(tree: &Tree, text: &TextSystem, i: usize) -> bool {
tree.specs[i].access().label.is_some() || content_name(tree, text, i).is_some()
}
fn content_name(tree: &Tree, text: &TextSystem, i: usize) -> Option<String> {
let end = tree.subtree_end(i);
let mut out = String::new();
let mut j = i;
while j < end {
if j > i && tree.specs[j].access().role == Some(Role::None) {
j = tree.subtree_end(j);
continue;
}
let at = j;
j += 1;
if let NodeContent::Text(id) = tree.content[at] {
let s = text.content(id).trim();
if s.is_empty() {
continue;
}
if !out.is_empty() {
out.push(' ');
}
out.push_str(s);
}
}
(!out.is_empty()).then_some(out)
}
pub(crate) struct Sources<'a> {
pub text: &'a TextSystem,
pub cells: &'a crate::cells::CellStore,
pub edit: &'a EditStore,
pub scroll: &'a ScrollStore,
pub title: Option<&'a str>,
pub focus: Option<Key>,
pub modal: Option<Key>,
pub viewport: Size,
pub scale: f32,
pub clips: &'a [Clip],
}
fn clip_of(src: &Sources<'_>, i: usize) -> Rect {
src.clips.get(i).map_or(NO_CLIP, |c| c.rect)
}
fn clipped(rect: Rect, clip: Rect) -> Rect {
let x0 = rect.x.max(clip.x);
let y0 = rect.y.max(clip.y);
let x1 = (rect.x + rect.w).min(clip.x + clip.w);
let y1 = (rect.y + rect.h).min(clip.y + clip.h);
let gone = |lo: f32, hi: f32, extent: f32| hi < lo || (hi == lo && extent > 0.0);
if gone(x0, x1, rect.w) || gone(y0, y1, rect.h) {
let x = rect.x.max(clip.x).min(clip.x + clip.w);
let y = rect.y.max(clip.y).min(clip.y + clip.h);
return Rect::new(x, y, 0.0, 0.0);
}
Rect::new(x0, y0, x1 - x0, y1 - y0)
}
fn node_rect(tree: &Tree, src: &Sources<'_>, i: usize) -> Rect {
if i == 0 {
Rect::new(0.0, 0.0, src.viewport.w, src.viewport.h)
} else {
clipped(
Rect::from_pos_size(tree.pos[i], tree.size[i]),
clip_of(src, i),
)
}
}
fn edit_run_clip(tree: &Tree, src: &Sources<'_>, i: usize, edit_key: Key) -> Rect {
let clip = clip_of(src, i);
if src.edit.folds(edit_key) {
return clip;
}
let pad = tree.specs[i].layout.padding;
let across = Rect::new(
tree.pos[i].x + pad.l,
clip.y,
(tree.size[i].w - pad.x()).max(0.0),
clip.h,
);
clip.intersect(&across)
}
fn clip_runs(runs: &mut [AccessRun], clip: Rect) {
if clip == NO_CLIP {
return;
}
for run in runs {
let cut = clipped(run.rect, clip);
let dx = run.rect.x - cut.x;
if dx != 0.0 {
for p in &mut run.char_positions {
*p += dx;
}
}
run.rect = cut;
}
}
pub(crate) fn inputs_hash(tree: &Tree, src: &Sources<'_>) -> Option<u64> {
use std::hash::{Hash, Hasher};
let mut h = rustc_hash::FxHasher::default();
let f = |h: &mut rustc_hash::FxHasher, v: f32| v.to_bits().hash(h);
tree.len().hash(&mut h);
src.focus.map(|k| k.0).hash(&mut h);
src.modal.map(|k| k.0).hash(&mut h);
src.title.hash(&mut h);
f(&mut h, src.viewport.w);
f(&mut h, src.viewport.h);
f(&mut h, src.scale);
let mut skip_until = 0usize;
let mut i = 0usize;
while i < tree.len() {
if i < skip_until {
i += 1;
continue;
}
let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
i += 1;
continue;
};
if sem.role == Role::None {
skip_until = tree.subtree_end(i);
i += 1;
continue;
}
if is_custom_editor(tree, i) {
return None;
}
let key = tree.keys[i];
let spec = &tree.specs[i];
let ax = spec.access();
i.hash(&mut h);
key.0.hash(&mut h);
tree.parent[i].hash(&mut h);
tree.origins[i].0.hash(&mut h);
sem.role.hash(&mut h);
sem.name.hash(&mut h);
sem.presentational.hash(&mut h);
ax.description.as_deref().hash(&mut h);
let rect = node_rect(tree, src, i);
for v in [rect.x, rect.y, rect.w, rect.h] {
f(&mut h, v);
}
(src.focus == Some(key)).hash(&mut h);
(src.modal == Some(key)).hash(&mut h);
spec.disabled.hash(&mut h);
ax.live.hash(&mut h);
spec.window.hash(&mut h);
spec.events().on_click.is_some().hash(&mut h);
focusable(tree, i).hash(&mut h);
crate::composite::orientation(tree, i).hash(&mut h);
ax.expanded.hash(&mut h);
ax.checked.hash(&mut h);
ax.mixed.hash(&mut h);
ax.selected.hash(&mut h);
ax.value_text.as_deref().hash(&mut h);
for v in [ax.value_now, ax.value_min, ax.value_max, ax.value_step] {
v.is_some().hash(&mut h);
f(&mut h, v.unwrap_or(0.0));
}
match tree.content[i] {
NodeContent::Edit(edit_key) => {
let pad = spec.layout.padding;
f(&mut h, tree.pos[i].x + pad.l);
f(&mut h, tree.pos[i].y + pad.t);
src.edit.text(edit_key).hash(&mut h);
src.edit.caret_and_selection(edit_key).hash(&mut h);
src.edit.selection_cursors(edit_key).hash(&mut h);
src.edit.version(edit_key).hash(&mut h);
let clip = edit_run_clip(tree, src, i, edit_key);
for v in [clip.x, clip.y, clip.w, clip.h] {
f(&mut h, v);
}
}
NodeContent::Cells(id) => src.cells.value(id).hash(&mut h),
_ => {}
}
if spec.layout.scroll_x || spec.layout.scroll_y {
spec.layout.scroll_x.hash(&mut h);
spec.layout.scroll_y.hash(&mut h);
let off = src.scroll.drawn(key);
let max = tree.scroll_max[i];
for v in [off.x, off.y, max.x, max.y] {
f(&mut h, v);
}
}
if sem.presentational {
skip_until = tree.subtree_end(i);
}
i += 1;
}
Some(h.finish())
}
pub(crate) fn build(tree: &Tree, src: &Sources<'_>) -> AccessTree {
let mut out = AccessTree::default();
if tree.is_empty() {
return out;
}
let mut parents: Vec<Option<Key>> = vec![None; tree.len()];
let mut skip_until = 0usize;
let mut i = 0usize;
while i < tree.len() {
if i < skip_until {
i += 1;
continue;
}
let parent = tree.parent[i];
let inherited = if parent == NIL {
None
} else {
parents[parent as usize]
};
let Some(sem) = semantic(tree, src.text, src.edit, src.title, i) else {
parents[i] = inherited;
i += 1;
continue;
};
if sem.role == Role::None {
skip_until = tree.subtree_end(i);
i += 1;
continue;
}
let key = tree.keys[i];
parents[i] = Some(key);
let spec: &NodeSpec = &tree.specs[i];
let rect = node_rect(tree, src, i);
let mut node = AccessNode {
key,
parent: inherited,
origin: tree.origins[i],
role: sem.role,
name: sem.name,
description: spec.access().description.as_ref().map(|d| d.to_string()),
rect,
value: None,
caret: None,
selection: None,
runs: Vec::new(),
anchor: None,
focus: None,
checked: None,
mixed: false,
selected: None,
expanded: None,
orientation: crate::composite::orientation(tree, i),
pos_in_set: None,
set_size: None,
number: None,
min: None,
max: None,
step: None,
focused: src.focus == Some(key),
disabled: spec.disabled,
modal: src.modal == Some(key),
scroll: None,
actions: 0,
live: spec.access().live,
};
let mut actions = 0u32;
match spec.window {
Some(WindowRole::Button(_)) => actions |= AccessAction::Click.bit(),
Some(WindowRole::Drag) => {}
None => {
if spec.events().on_click.is_some() && !spec.disabled {
actions |= AccessAction::Click.bit();
}
}
}
if focusable(tree, i) {
actions |= AccessAction::Focus.bit() | AccessAction::Blur.bit();
}
if let NodeContent::Edit(edit_key) = tree.content[i] {
let pad = spec.layout.padding;
let origin = Vec2::new(tree.pos[i].x + pad.l, tree.pos[i].y + pad.t);
node.value = src.edit.text(edit_key);
node.runs = src.edit.runs(edit_key, key, origin, src.scale);
clip_runs(&mut node.runs, edit_run_clip(tree, src, i, edit_key));
if let Some((caret, selection)) = src.edit.caret_and_selection(edit_key) {
node.caret = Some(caret);
node.selection = selection;
}
if let Some((anchor, focus)) = src.edit.selection_cursors(edit_key) {
node.anchor = node.text_pos(anchor.0, anchor.1);
node.focus = node.text_pos(focus.0, focus.1);
}
if !spec.disabled {
actions |= AccessAction::Click.bit()
| AccessAction::SetValue.bit()
| AccessAction::SetTextSelection.bit()
| AccessAction::ReplaceSelectedText.bit();
}
} else if is_custom_editor(tree, i) {
custom_editor(tree, src, i, &mut node);
if !spec.disabled {
actions |= AccessAction::SetValue.bit()
| AccessAction::SetTextSelection.bit()
| AccessAction::ReplaceSelectedText.bit();
}
} else if let NodeContent::Cells(id) = tree.content[i] {
node.value = Some(src.cells.value(id));
}
let ax = spec.access();
node.expanded = ax.expanded;
match sem.role {
Role::Checkbox | Role::Radio | Role::Switch => {
node.checked = Some(ax.checked);
node.mixed = ax.mixed && sem.role == Role::Checkbox;
}
Role::Tab => node.selected = Some(ax.selected),
Role::ListItem | Role::Link if ax.selected => node.selected = Some(true),
Role::MenuItem if ax.checked => node.checked = Some(true),
Role::Slider => {
node.number = ax.value_now;
node.min = ax.value_min;
node.max = ax.value_max;
node.step = ax.value_step;
node.value = ax.value_text.as_deref().map(str::to_owned);
if !spec.disabled {
actions |= AccessAction::Increment.bit()
| AccessAction::Decrement.bit()
| AccessAction::SetValue.bit();
}
}
_ => {}
}
if spec.layout.scroll_x || spec.layout.scroll_y {
let off = src.scroll.drawn(key);
let max = tree.scroll_max[i];
node.scroll = Some(ScrollState {
x: off.x,
y: off.y,
max_x: max.x,
max_y: max.y,
});
if spec.layout.scroll_y {
actions |= AccessAction::ScrollUp.bit() | AccessAction::ScrollDown.bit();
}
if spec.layout.scroll_x {
actions |= AccessAction::ScrollLeft.bit() | AccessAction::ScrollRight.bit();
}
}
if i != 0 {
actions |= AccessAction::ScrollIntoView.bit();
}
node.actions = actions;
if node.focused {
out.focus = Some(key);
}
out.nodes.push(node);
if sem.presentational {
skip_until = tree.subtree_end(i);
}
i += 1;
}
set_positions(tree, &mut out);
out.hash = hash_of(&out);
out
}
fn set_positions(tree: &Tree, out: &mut AccessTree) {
let containers: Vec<(usize, Role)> = (0..tree.len())
.filter_map(|i| {
let item = crate::composite::item_role(tree.specs[i].access().role?)?;
Some((i, item))
})
.collect();
let mut items: Vec<usize> = Vec::new();
for (container, item) in containers {
crate::composite::items(tree, container, item, &mut items);
if items.is_empty() {
continue;
}
let size = items.len();
let container = tree.keys[container];
let keys: Vec<Key> = items.iter().map(|&i| tree.keys[i]).collect();
for n in &mut out.nodes {
if n.key == container {
n.set_size = Some(size);
} else if let Some(pos) = keys.iter().position(|k| *k == n.key) {
n.pos_in_set = Some(pos);
}
}
}
}
pub(crate) fn lines_under(tree: &Tree, i: usize) -> Vec<usize> {
let end = tree.subtree_end(i);
let mut lines: Vec<usize> = Vec::new();
let mut j = i + 1;
while j < end {
let spec = &tree.specs[j];
if spec.access().role == Some(Role::None) {
j = tree.subtree_end(j);
continue;
}
if spec.access().role == Some(Role::Line) {
lines.push(j);
j = tree.subtree_end(j);
continue;
}
j += 1;
}
lines
}
fn custom_editor(tree: &Tree, src: &Sources<'_>, i: usize, node: &mut AccessNode) {
let lines = lines_under(tree, i);
let mut value = String::new();
let mut run_no = 0usize;
let mut caret: Option<(usize, usize)> = None;
let mut anchor: Option<(usize, usize)> = None;
for (ln, &l) in lines.iter().enumerate() {
let last = ln + 1 == lines.len();
let mut line_text = String::new();
let mut texts: Vec<usize> = Vec::new();
let lend = tree.subtree_end(l);
let mut t = l;
while t < lend {
if tree.specs[t].access().role == Some(Role::None) {
t = tree.subtree_end(t);
continue;
}
if let NodeContent::Text(_) = tree.content[t] {
texts.push(t);
}
t += 1;
}
for (k, &t) in texts.iter().enumerate() {
let NodeContent::Text(id) = tree.content[t] else {
continue;
};
let base = line_text.len();
line_text.push_str(src.text.content(id));
let newline = !last && k + 1 == texts.len();
let first = node.runs.len();
src.text.access_runs(
id,
RunSource {
key: node.key,
line: ln,
byte_base: base,
origin: tree.pos[t],
scale: src.scale,
newline_after_last: newline,
},
&mut run_no,
&mut node.runs,
);
clip_runs(&mut node.runs[first..], clip_of(src, t));
}
if texts.is_empty() {
node.runs.push(empty_run(
node.key,
run_no,
ln,
Rect::from_pos_size(tree.pos[l], tree.size[l]),
!last,
));
let at = node.runs.len() - 1;
clip_runs(&mut node.runs[at..], clip_of(src, l));
run_no += 1;
}
if let Some(c) = tree.specs[l].access().caret {
caret = Some((ln, (c as usize).min(line_text.len())));
}
if let Some(a) = tree.specs[l].access().selection_anchor {
anchor = Some((ln, (a as usize).min(line_text.len())));
}
if ln > 0 {
value.push('\n');
}
value.push_str(&line_text);
}
node.value = Some(value);
if let Some((line, offset)) = caret {
node.focus = node.text_pos(line, offset);
let anchor = anchor.unwrap_or((line, offset));
node.anchor = node.text_pos(anchor.0, anchor.1);
let flat = |(line, offset): (usize, usize)| {
node.value
.as_ref()
.map(|v| v.split('\n').take(line).map(|l| l.len() + 1).sum::<usize>() + offset)
};
node.caret = flat((line, offset));
if anchor != (line, offset)
&& let (Some(a), Some(c)) = (flat(anchor), node.caret)
{
node.selection = Some((a.min(c), a.max(c)));
}
}
}
pub(crate) struct RunSource {
pub key: Key,
pub line: usize,
pub byte_base: usize,
pub origin: Vec2,
pub scale: f32,
pub newline_after_last: bool,
}
fn run_key(key: Key, n: usize) -> Key {
key.str("\u{1}run").index(n as u64)
}
fn empty_run(key: Key, n: usize, line: usize, rect: Rect, newline: bool) -> AccessRun {
AccessRun {
key: run_key(key, n),
line,
start: 0,
end: 0,
text: if newline { "\n".into() } else { String::new() },
rect: Rect::new(rect.x, rect.y, 0.0, rect.h),
char_lengths: if newline { vec![1] } else { Vec::new() },
char_positions: if newline { vec![0.0] } else { Vec::new() },
char_widths: if newline { vec![0.0] } else { Vec::new() },
word_starts: Vec::new(),
rtl: false,
}
}
pub(crate) fn runs_of_buffer(
buffer: &Buffer,
src: RunSource,
run_no: &mut usize,
out: &mut Vec<AccessRun>,
) {
let line_count = buffer.lines.len();
let runs: Vec<_> = buffer.layout_runs().collect();
for (r, run) in runs.iter().enumerate() {
let last_of_line = runs.get(r + 1).is_none_or(|next| next.line_i != run.line_i);
let newline = last_of_line && (run.line_i + 1 < line_count || src.newline_after_last);
push_row_runs(
&RowGlyphs {
glyphs: run.glyphs,
text: run.text,
line: run.line_i,
top: run.line_top,
height: run.line_height,
rtl: run.rtl,
dx: 0.0,
base: 0,
newline,
},
&src,
run_no,
out,
);
}
}
pub(crate) struct RowGlyphs<'a> {
pub glyphs: &'a [cosmic_text::LayoutGlyph],
pub text: &'a str,
pub line: usize,
pub top: f32,
pub height: f32,
pub rtl: bool,
pub dx: f32,
pub base: usize,
pub newline: bool,
}
pub(crate) fn push_row_runs(
row: &RowGlyphs<'_>,
src: &RunSource,
run_no: &mut usize,
out: &mut Vec<AccessRun>,
) {
let scale = src.scale.max(f32::EPSILON);
let newline = row.newline;
{
let (start, end) = row.glyphs.iter().fold((usize::MAX, 0usize), |(s, e), g| {
(s.min(g.start), e.max(g.end))
});
let (start, end) = if row.glyphs.is_empty() {
(0, 0)
} else {
(start, end)
};
let slice = &row.text[start..end];
let x0 = row
.glyphs
.iter()
.map(|g| g.x + row.dx)
.fold(f32::INFINITY, f32::min);
let x0 = if x0.is_finite() { x0 } else { 0.0 };
let mut chars: Vec<(usize, u8, f32, f32)> = Vec::new(); for (rel, c) in slice.char_indices() {
let idx = start + rel;
let len = c.len_utf8();
let (x, w) = match row.glyphs.iter().find(|g| g.start <= idx && idx < g.end) {
Some(g) => {
let span = (g.end - g.start).max(1) as f32;
(
g.x + row.dx + g.w * (idx - g.start) as f32 / span,
g.w * len as f32 / span,
)
}
None => chars.last().map_or((x0, 0.0), |&(_, _, x, w)| (x + w, 0.0)),
};
chars.push((idx, len as u8, x, w));
}
let line_end_x = chars.last().map_or(x0, |&(_, _, x, w)| x + w);
if newline {
chars.push((end, 1, line_end_x, 0.0));
}
let starts: Vec<usize> = slice
.split_word_bound_indices()
.filter(|(_, w)| !w.chars().all(char::is_whitespace))
.map(|(b, _)| slice[..b].chars().count())
.collect();
let mut at = 0usize;
loop {
let chunk_end = (at + RUN_CHARS).min(chars.len());
let chunk = &chars[at..chunk_end];
let chunk_x = chunk.first().map_or(x0, |c| c.2);
let chunk_end_x = chunk.last().map_or(chunk_x, |c| c.2 + c.3);
let byte_start = chunk.first().map_or(end, |c| c.0);
let byte_end = chunk
.last()
.map_or(end, |c| {
if newline && c.0 == end {
end
} else {
c.0 + c.1 as usize
}
})
.min(end);
let mut text = row.text[byte_start.min(end)..byte_end].to_string();
if newline && chunk_end == chars.len() {
text.push('\n');
}
out.push(AccessRun {
key: run_key(src.key, *run_no),
line: src.line + row.line,
start: src.byte_base + row.base + byte_start.min(end),
end: src.byte_base + row.base + byte_end,
text,
rect: Rect::new(
src.origin.x + chunk_x / scale,
src.origin.y + row.top / scale,
(chunk_end_x - chunk_x) / scale,
row.height / scale,
),
char_lengths: chunk.iter().map(|c| c.1).collect(),
char_positions: chunk.iter().map(|c| (c.2 - chunk_x) / scale).collect(),
char_widths: chunk.iter().map(|c| c.3 / scale).collect(),
word_starts: starts
.iter()
.filter(|&&s| s >= at && s < chunk_end)
.map(|&s| (s - at) as u8)
.collect(),
rtl: row.rtl,
});
*run_no += 1;
at = chunk_end;
if at >= chars.len() {
break;
}
}
}
}
fn hash_of(tree: &AccessTree) -> u64 {
let mut h = crate::key::FNV_OFFSET;
let mut mix = |bytes: &[u8]| h = crate::key::fnv(h, bytes);
let mix_str = |mix: &mut dyn FnMut(&[u8]), s: &Option<String>| match s {
Some(s) => {
mix(&[1]);
mix(s.as_bytes());
mix(&[0]);
}
None => mix(&[0]),
};
let mix_f32 = |mix: &mut dyn FnMut(&[u8]), v: Option<f32>| match v {
Some(v) => {
mix(&[1]);
mix(&v.to_bits().to_le_bytes());
}
None => mix(&[0]),
};
let mix_pos = |mix: &mut dyn FnMut(&[u8]), p: Option<TextPos>| match p {
Some(p) => {
mix(&p.run.0.to_le_bytes());
mix(&p.character.to_le_bytes());
}
None => mix(&[0]),
};
for n in &tree.nodes {
mix(&n.key.0.to_le_bytes());
mix(&n.parent.map_or(0, |k| k.0).to_le_bytes());
mix(&[n.role as u8]);
mix_str(&mut mix, &n.name);
mix_str(&mut mix, &n.description);
for v in [n.rect.x, n.rect.y, n.rect.w, n.rect.h] {
mix(&v.to_bits().to_le_bytes());
}
mix_str(&mut mix, &n.value);
mix(&n.caret.unwrap_or(usize::MAX).to_le_bytes());
let (a, b) = n.selection.unwrap_or((usize::MAX, usize::MAX));
mix(&a.to_le_bytes());
mix(&b.to_le_bytes());
for r in &n.runs {
mix(&r.key.0.to_le_bytes());
mix(r.text.as_bytes());
mix(&[0]);
for v in [r.rect.x, r.rect.y, r.rect.w, r.rect.h] {
mix(&v.to_bits().to_le_bytes());
}
for v in &r.char_positions {
mix(&v.to_bits().to_le_bytes());
}
}
mix_pos(&mut mix, n.anchor);
mix_pos(&mut mix, n.focus);
mix(&[n.checked.map_or(2, |c| c as u8), n.mixed as u8]);
mix(&n.step.map_or(u32::MAX, f32::to_bits).to_le_bytes());
mix(&[n.selected.map_or(2, |c| c as u8)]);
mix(&[n.expanded.map_or(2, |c| c as u8)]);
mix(&n.pos_in_set.unwrap_or(usize::MAX).to_le_bytes());
mix(&n.set_size.unwrap_or(usize::MAX).to_le_bytes());
mix_f32(&mut mix, n.number);
mix_f32(&mut mix, n.min);
mix_f32(&mut mix, n.max);
mix(&[n.focused as u8, n.disabled as u8, n.modal as u8]);
if let Some(s) = n.scroll {
for v in [s.x, s.y, s.max_x, s.max_y] {
mix(&v.to_bits().to_le_bytes());
}
}
mix(&n.actions.to_le_bytes());
mix(&[n.live as u8]);
}
mix(&tree.focus.map_or(0, |k| k.0).to_le_bytes());
h
}
pub type Label = Arc<str>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn role_names_round_trip() {
for r in Role::ALL {
assert_eq!(Role::parse(r.name()), Some(r));
}
assert_eq!(Role::parse("nope"), None);
}
#[test]
fn action_bits_are_distinct_and_named() {
let mut seen = 0u32;
for a in AccessAction::ALL {
assert_eq!(seen & a.bit(), 0);
seen |= a.bit();
assert_eq!(AccessAction::parse(a.name()), Some(a));
}
}
#[test]
fn run_keys_never_collide_with_node_keys() {
let k = Key::ROOT.str("editor");
assert_ne!(run_key(k, 0), k.index(0));
assert_ne!(run_key(k, 0), k.str("run"));
assert_ne!(run_key(k, 0), run_key(k, 1));
}
}