Basic Slint accessibility support (#1294)

Implement basic accessibility (a11y) support, using the Qt backend.

_This should get us started, but accessibility support is an additional way to interact with UIs that is very different from the "graphical way" most users will interact with the UI. No single PR will "make a toolkit accessibility", this needs to be an ongoing effort!_

Parts of this PR:

* Add functions to access a11y-related properties to Component
* Add helper functions to Item struct 
* Handle accessible- properties in the compiler
* Add documentation, add description, enforce some basic rules
* Make the Text element accessible by default
* Don't optimize away accessibility property in the LLR
* Ensure that accessibility property are marked as used
* Add some accessibility properties to the native style widgets
* Support for bool and integer `accessible` properties
* Implement basic support for accessibility
* Make basic widgets accessible by default
* Make slider focus-able and interactable with keyboard
* Tell a11y layer about value changes
* Generate QAccessible constants using bindgen
* Don't expose the `accessible` properties when using the MCU backend: There is no backend to make use of them
* Handle focus change based on keyboard focus of the window
* Report accessible widgets at correct positions
* Allow for (virtual) focus delegation at the a11y level
* Calculate value step size dynamically
* Make sure to not send notifications to a11y backend about dead objects
This commit is contained in:
Tobias Hunger 2022-06-08 20:42:10 +02:00 committed by GitHub
parent dc7117eeb1
commit 07ad20a09c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1855 additions and 39 deletions

View file

@ -449,6 +449,7 @@ export NativeButton := _ {
property <string> text;
property <image> icon;
property <bool> pressed: native_output;
property <bool> has-focus: native_output;
callback clicked;
property <bool> enabled: true;
property <StandardButtonKind> standard-button-kind;
@ -464,6 +465,7 @@ export NativeCheckBox := _ {
property <bool> enabled: true;
property <string> text;
property <bool> checked: native_output;
property <bool> has-focus: native_output;
callback toggled;
//-is_internal
}

View file

@ -832,8 +832,8 @@ fn generate_item_tree(
let item_array_index = item_array.len() as u32;
item_tree_array.push(format!(
"slint::private_api::make_item_node({}, {}, {}, {})",
children_count, children_index, parent_index, item_array_index,
"slint::private_api::make_item_node({}, {}, {}, {}, {})",
children_count, children_index, parent_index, item_array_index, node.is_accessible
));
item_array.push(format!(
"{{ {}, {} offsetof({}, {}) }}",
@ -1022,6 +1022,38 @@ fn generate_item_tree(
}),
));
target_struct.members.push((
Access::Private,
Declaration::Function(Function {
name: "accessible_role".into(),
signature:
"([[maybe_unused]] slint::private_api::ComponentRef component, uintptr_t index) -> slint::cbindgen_private::AccessibleRole"
.into(),
is_static: true,
statements: Some(vec![format!(
"return reinterpret_cast<const {}*>(component.instance)->accessible_role(index);",
item_tree_class_name
)]),
..Default::default()
}),
));
target_struct.members.push((
Access::Private,
Declaration::Function(Function {
name: "accessible_string_property".into(),
signature:
"([[maybe_unused]] slint::private_api::ComponentRef component, uintptr_t index, slint::cbindgen_private::AccessibleStringProperty what, slint::SharedString *result) -> void"
.into(),
is_static: true,
statements: Some(vec![format!(
"*result = reinterpret_cast<const {}*>(component.instance)->accessible_string_property(index, what);",
item_tree_class_name
)]),
..Default::default()
}),
));
target_struct.members.push((
Access::Public,
Declaration::Var(Var {
@ -1035,9 +1067,12 @@ fn generate_item_tree(
ty: "const slint::private_api::ComponentVTable".to_owned(),
name: format!("{}::static_vtable", item_tree_class_name),
init: Some(format!(
"{{ visit_children, get_item_ref, get_subtree_range, get_subtree_component, get_item_tree, parent_node, subtree_index, layout_info, slint::private_api::drop_in_place<{}>, slint::private_api::dealloc }}",
item_tree_class_name)
),
"{{ visit_children, get_item_ref, get_subtree_range, get_subtree_component, \
get_item_tree, parent_node, subtree_index, layout_info, \
accessible_role, accessible_string_property, \
slint::private_api::drop_in_place<{}>, slint::private_api::dealloc }}",
item_tree_class_name
)),
..Default::default()
}));
@ -1446,6 +1481,72 @@ fn generate_sub_component(
}),
));
let mut accessible_function = |name: &str,
signature: &str,
forward_args: &str,
code: Vec<String>| {
let mut code = ["[[maybe_unused]] auto self = this;".into()]
.into_iter()
.chain(code.into_iter())
.collect::<Vec<_>>();
let mut else_ = "";
for sub in &component.sub_components {
let sub_items_count = sub.ty.child_item_count();
code.push(format!("{else_}if (index == {}) {{", sub.index_in_tree,));
code.push(format!(" return self->{}.{name}(0{forward_args});", ident(&sub.name)));
if sub_items_count > 1 {
code.push(format!(
"}} else if (index >= {} && index < {}) {{",
sub.index_of_first_child_in_tree,
sub.index_of_first_child_in_tree + sub_items_count - 1
));
code.push(format!(
" return self->{}.{name}(index - {}{forward_args});",
ident(&sub.name),
sub.index_of_first_child_in_tree - 1
));
}
else_ = "} else ";
}
code.push(format!("{else_}return {{}};"));
target_struct.members.push((
field_access,
Declaration::Function(Function {
name: name.into(),
signature: signature.into(),
statements: Some(code),
..Default::default()
}),
));
};
let mut accessible_role_cases = vec!["switch (index) {".into()];
let mut accessible_string_cases = vec!["switch ((index << 8) | uintptr_t(what)) {".into()];
for ((index, what), expr) in &component.accessible_prop {
let expr = compile_expression(&expr.borrow(), &ctx);
if what == "Role" {
accessible_role_cases.push(format!(" case {index}: return {expr};"));
} else {
accessible_string_cases.push(format!(" case ({index} << 8) | uintptr_t(slint::cbindgen_private::AccessibleStringProperty::{what}): return {expr};"));
}
}
accessible_role_cases.push("}".into());
accessible_string_cases.push("}".into());
accessible_function(
"accessible_role",
"(uintptr_t index) const -> slint::cbindgen_private::AccessibleRole",
"",
accessible_role_cases,
);
accessible_function(
"accessible_string_property",
"(uintptr_t index, slint::cbindgen_private::AccessibleStringProperty what) const -> slint::SharedString",
", what",
accessible_string_cases,
);
if !children_visitor_cases.is_empty() {
target_struct.members.push((
field_access,

View file

@ -668,6 +668,19 @@ fn generate_sub_component(
repeated_element_components.push(rep_inner_component_id);
}
let mut accessible_role_branch = vec![];
let mut accessible_string_property_branch = vec![];
for ((index, what), expr) in &component.accessible_prop {
let expr = compile_expression(&expr.borrow(), &ctx);
if what == "Role" {
accessible_role_branch.push(quote!(#index => #expr,));
} else {
let what = ident(&what);
accessible_string_property_branch
.push(quote!((#index, AccessibleStringProperty::#what) => #expr,));
}
}
let mut sub_component_names: Vec<Ident> = vec![];
let mut sub_component_types: Vec<Ident> = vec![];
@ -720,6 +733,25 @@ fn generate_sub_component(
));
}
let sub_items_count = sub.ty.child_item_count();
let local_tree_index = local_tree_index as usize;
accessible_role_branch.push(quote!(
#local_tree_index => #sub_compo_field.apply_pin(_self).accessible_role(0),
));
accessible_string_property_branch.push(quote!(
(#local_tree_index, _) => #sub_compo_field.apply_pin(_self).accessible_string_property(0, what),
));
if sub_items_count > 1 {
let range_begin = local_index_of_first_child as usize;
let range_end = range_begin + sub_items_count - 2;
accessible_role_branch.push(quote!(
#range_begin..=#range_end => #sub_compo_field.apply_pin(_self).accessible_role(index - #range_begin + 1),
));
accessible_string_property_branch.push(quote!(
(#range_begin..=#range_end, _) => #sub_compo_field.apply_pin(_self).accessible_string_property(index - #range_begin + 1, what),
));
}
sub_component_names.push(field_name);
sub_component_types.push(sub_component_id);
}
@ -866,6 +898,31 @@ fn generate_sub_component(
let _self = self;
#subtree_index_function
}
fn accessible_role(self: ::core::pin::Pin<&Self>, index: usize) -> slint::re_exports::AccessibleRole {
#![allow(unused)]
use slint::re_exports::*;
let _self = self;
match index {
#(#accessible_role_branch)*
//#(#forward_sub_ranges => #forward_sub_field.apply_pin(_self).accessible_role())*
_ => AccessibleRole::default(),
}
}
fn accessible_string_property(
self: ::core::pin::Pin<&Self>,
index: usize,
what: slint::re_exports::AccessibleStringProperty,
) -> slint::re_exports::SharedString {
#![allow(unused)]
use slint::re_exports::*;
let _self = self;
match (index, what) {
#(#accessible_string_property_branch)*
_ => Default::default(),
}
}
}
#(#extra_components)*
@ -1073,8 +1130,10 @@ fn generate_item_tree(
let children_count = node.children.len() as u32;
let children_index = children_offset as u32;
let item_array_len = item_array.len() as u32;
let is_accessible = node.is_accessible;
item_tree_array.push(quote!(
slint::re_exports::ItemTreeNode::Item{
slint::re_exports::ItemTreeNode::Item {
is_accessible: #is_accessible,
children_count: #children_count,
children_index: #children_index,
parent_index: #parent_index,
@ -1191,6 +1250,19 @@ fn generate_item_tree(
fn layout_info(self: ::core::pin::Pin<&Self>, orientation: slint::re_exports::Orientation) -> slint::re_exports::LayoutInfo {
self.layout_info(orientation)
}
fn accessible_role(self: ::core::pin::Pin<&Self>, index: usize) -> slint::re_exports::AccessibleRole {
self.accessible_role(index)
}
fn accessible_string_property(
self: ::core::pin::Pin<&Self>,
index: usize,
what: slint::re_exports::AccessibleStringProperty,
result: &mut slint::re_exports::SharedString,
) {
*result = self.accessible_string_property(index, what);
}
}

View file

@ -73,6 +73,9 @@ pub struct CompilerConfiguration {
/// Compile time scale factor to apply to embedded resources such as images and glyphs.
pub scale_factor: f64,
/// expose the accessible role and properties
pub accessibility: bool,
}
impl CompilerConfiguration {
@ -122,6 +125,7 @@ impl CompilerConfiguration {
open_import_fallback: Default::default(),
inline_all_elements,
scale_factor,
accessibility: true,
}
}
}

View file

@ -156,6 +156,7 @@ pub struct TreeNode {
pub item_index: usize,
pub repeated: bool,
pub children: Vec<TreeNode>,
pub is_accessible: bool,
}
impl TreeNode {
@ -216,12 +217,15 @@ pub struct SubComponent {
pub animations: HashMap<PropertyReference, Expression>,
pub two_way_bindings: Vec<(PropertyReference, PropertyReference)>,
pub const_properties: Vec<PropertyReference>,
// Code that is run in the sub component constructor, after property initializations
/// Code that is run in the sub component constructor, after property initializations
pub init_code: Vec<MutExpression>,
pub layout_info_h: MutExpression,
pub layout_info_v: MutExpression,
/// Maps (item_index, property) to an expression
pub accessible_prop: BTreeMap<(usize, String), MutExpression>,
pub prop_analysis: HashMap<PropertyReference, PropAnalysis>,
}
@ -241,6 +245,15 @@ impl SubComponent {
}
count
}
/// total count of items, including in sub components
pub fn child_item_count(&self) -> usize {
let mut count = self.items.len();
for x in self.sub_components.iter() {
count += x.ty.child_item_count();
}
count
}
}
pub struct SubComponentInstance {
@ -326,6 +339,9 @@ impl PublicComponent {
}
visitor(&sc.layout_info_h, ctx);
visitor(&sc.layout_info_v, ctx);
for (_, e) in &sc.accessible_prop {
visitor(e, ctx);
}
});
for g in &self.globals {
let ctx = EvaluationContext::new_global(self, g, ());

View file

@ -191,10 +191,12 @@ fn lower_sub_component(
// just initialize to dummy expression right now and it will be set later
layout_info_h: super::Expression::BoolLiteral(false).into(),
layout_info_v: super::Expression::BoolLiteral(false).into(),
accessible_prop: Default::default(),
prop_analysis: Default::default(),
};
let mut mapping = LoweredSubComponentMapping::default();
let mut repeated = vec![];
let mut accessible_prop = Vec::new();
if let Some(parent) = component.parent_element.upgrade() {
// Add properties for the model data and index
@ -280,6 +282,11 @@ fn lower_sub_component(
}
_ => unreachable!(),
};
for (key, nr) in &elem.accessibility_props.0 {
// TODO: we also want to split by type (role/string/...)
let enum_value = to_camel_case(key.strip_prefix("accessible-").unwrap());
accessible_prop.push((*elem.item_index.get().unwrap(), enum_value, nr.clone()));
}
Some(element.clone())
});
let ctx = ExpressionContext { mapping: &mapping, state, parent: parent_context, component };
@ -381,9 +388,50 @@ fn lower_sub_component(
)
.into();
sub_component.accessible_prop = accessible_prop
.into_iter()
.map(|(idx, key, nr)| {
let mut expr = super::Expression::PropertyReference(ctx.map_property_reference(&nr));
match nr.ty() {
Type::Bool => {
expr = super::Expression::Condition {
condition: expr.into(),
true_expr: super::Expression::StringLiteral("true".into()).into(),
false_expr: super::Expression::StringLiteral("false".into()).into(),
};
}
Type::Int32 | Type::Float32 => {
expr = super::Expression::Cast { from: expr.into(), to: Type::String };
}
Type::String => {}
Type::Enumeration(e) if e.name == "AccessibleRole" => {}
_ => panic!("Invalid type for accessible property"),
}
((idx, key), expr.into())
})
.collect();
LoweredSubComponent { sub_component: Rc::new(sub_component), mapping }
}
// Convert a ascii kebab string to camel case
fn to_camel_case(str: &str) -> String {
let mut result = Vec::with_capacity(str.len());
let mut next_upper = true;
for x in str.as_bytes() {
if *x == b'-' {
next_upper = true;
} else if next_upper {
result.push(x.to_ascii_uppercase());
next_upper = false;
} else {
result.push(*x);
}
}
String::from_utf8(result).unwrap()
}
fn get_property_analysis(elem: &ElementRc, p: &str) -> crate::object_tree::PropertyAnalysis {
let mut a = elem.borrow().property_analysis.borrow().get(p).cloned().unwrap_or_default();
let mut elem = elem.clone();
@ -595,15 +643,18 @@ fn make_tree(
&new_sub_component_path,
);
tree_node.children.extend(children);
tree_node.is_accessible |= !e.accessibility_props.0.is_empty();
tree_node
}
LoweredElement::NativeItem { item_index } => TreeNode {
is_accessible: !e.accessibility_props.0.is_empty(),
sub_component_path: sub_component_path.into(),
item_index: *item_index,
children: children.collect(),
repeated: false,
},
LoweredElement::Repeated { repeated_index } => TreeNode {
is_accessible: false,
sub_component_path: sub_component_path.into(),
item_index: *repeated_index,
children: vec![],

View file

@ -79,7 +79,12 @@ pub fn count_property_use(root: &PublicComponent) {
sc.layout_info_h.borrow().visit_recursive(&mut |e| visit_expression(e, ctx));
sc.layout_info_v.borrow().visit_recursive(&mut |e| visit_expression(e, ctx));
// 6. aliases (if they were not optimize, they are probably used)
// 6. accessibility props
for (_, b) in &sc.accessible_prop {
b.borrow().visit_recursive(&mut |e| visit_expression(e, ctx))
}
// 7. aliases (if they were not optimize, they are probably used)
for (a, b) in &sc.two_way_bindings {
visit_property(a, ctx);
visit_property(b, ctx);

View file

@ -420,6 +420,10 @@ impl Clone for PropertyAnimation {
}
}
/// Map the accessibility property (eg "accessible-role", "accessible-label") to its named reference
#[derive(Default, Clone)]
pub struct AccessibilityProps(pub BTreeMap<String, NamedReference>);
pub type BindingsMap = BTreeMap<String, RefCell<BindingExpression>>;
/// An Element is an instantiation of a Component
@ -457,6 +461,8 @@ pub struct Element {
/// The property pointing to the layout info. `(horizontal, vertical)`
pub layout_info_prop: Option<(NamedReference, NamedReference)>,
pub accessibility_props: AccessibilityProps,
/// true if this Element is the fake Flickable viewport
pub is_flickable_viewport: bool,
@ -1623,6 +1629,10 @@ pub fn visit_all_named_references_in_element(
layout_info_prop.as_mut().map(|(h, b)| (vis(h), vis(b)));
elem.borrow_mut().layout_info_prop = layout_info_prop;
let mut accessibility_props = std::mem::take(&mut elem.borrow_mut().accessibility_props);
accessibility_props.0.iter_mut().for_each(|(_, x)| vis(x));
elem.borrow_mut().accessibility_props = accessibility_props;
// visit two way bindings
for expr in elem.borrow().bindings.values() {
for nr in &mut expr.borrow_mut().two_way_bindings {

View file

@ -22,6 +22,7 @@ mod focus_item;
mod generate_item_indices;
mod infer_aliases_types;
mod inlining;
mod lower_accessibility;
mod lower_layout;
mod lower_popups;
mod lower_property_to_element;
@ -138,6 +139,9 @@ pub async fn run_passes(
lower_shadows::lower_shadow_properties(component, &doc.local_registry, diag);
clip::handle_clip(component, &global_type_registry.borrow(), diag);
visible::handle_visible(component, &global_type_registry.borrow());
if compiler_config.accessibility {
lower_accessibility::lower_accessibility_properties(component, diag);
}
materialize_fake_properties::materialize_fake_properties(component);
}
collect_globals::collect_globals(doc, diag);

View file

@ -146,6 +146,9 @@ fn analyze_element(
diag,
);
}
for (_, nr) in &elem.borrow().accessibility_props.0 {
process_property(&PropertyPath::from(nr.clone()), context, reverse_aliases, diag);
}
}
#[derive(Copy, Clone, dm::BitAnd, dm::BitOr, dm::BitAndAssign, dm::BitOrAssign)]

View file

@ -43,6 +43,7 @@ pub fn ensure_window(
child_of_layout: false,
has_popup_child: false,
layout_info_prop: Default::default(),
accessibility_props: Default::default(),
is_flickable_viewport: false,
item_index: Default::default(),
item_index_of_first_children: Default::default(),

View file

@ -214,6 +214,7 @@ fn duplicate_element_with_mapping(
.collect(),
child_of_layout: elem.child_of_layout,
layout_info_prop: elem.layout_info_prop.clone(),
accessibility_props: elem.accessibility_props.clone(),
named_references: Default::default(),
item_index: Default::default(), // Not determined yet
item_index_of_first_children: Default::default(),

View file

@ -0,0 +1,78 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
//! Pass that lowers synthetic `accessible-*` properties
use crate::diagnostics::BuildDiagnostics;
use crate::expression_tree::{Expression, NamedReference};
use crate::langtype::EnumerationValue;
use crate::object_tree::{Component, ElementRc};
use std::rc::Rc;
pub fn lower_accessibility_properties(component: &Rc<Component>, diag: &mut BuildDiagnostics) {
crate::object_tree::recurse_elem_including_sub_components_no_borrow(
component,
&(),
&mut |elem, _| {
if elem.borrow().repeated.is_some() {
return;
};
apply_builtin(elem);
let accessible_role_set = match elem.borrow().bindings.get("accessible-role") {
Some(role) => {
if let Expression::EnumerationValue(val) = &role.borrow().expression {
debug_assert_eq!(val.enumeration.name, "AccessibleRole");
debug_assert_eq!(val.enumeration.values[0], "none");
if val.value == 0 {
return;
}
} else {
diag.push_error(
"The `accessible-role` property must be a constant expression".into(),
&*role.borrow(),
);
}
true
}
// maybe it was set on the parent
None => elem.borrow().is_binding_set("accessible-role", false),
};
for prop_name in crate::typeregister::RESERVED_ACCESSIBILITY_PROPERTIES
.iter()
.map(|x| x.0)
.chain(std::iter::once("accessible-role"))
{
if accessible_role_set {
if elem.borrow().is_binding_set(prop_name, false) {
let nr = NamedReference::new(elem, prop_name);
elem.borrow_mut().accessibility_props.0.insert(prop_name.into(), nr);
}
} else if let Some(b) = elem.borrow().bindings.get(prop_name) {
diag.push_error(
format!("The `{prop_name}` property can only be set in combination to `accessible-role`"),
&*b.borrow(),
);
}
}
},
)
}
fn apply_builtin(e: &ElementRc) {
let bty = if let Some(bty) = e.borrow().builtin_type() { bty } else { return };
if bty.name == "Text" {
e.borrow_mut().set_binding_if_not_set("accessible-role".into(), || {
let enum_ty = crate::typeregister::BUILTIN_ENUMS.with(|e| e.AccessibleRole.clone());
Expression::EnumerationValue(EnumerationValue {
value: enum_ty.values.iter().position(|v| v == "text").unwrap(),
enumeration: enum_ty,
})
});
let text_prop = NamedReference::new(e, "text");
e.borrow_mut().set_binding_if_not_set("accessible-label".into(), || {
Expression::PropertyReference(text_prop)
})
}
}

View file

@ -41,6 +41,7 @@ fn create_repeater_components(component: &Rc<Component>) {
transitions: std::mem::take(&mut elem.transitions),
child_of_layout: elem.child_of_layout || is_listview.is_some(),
layout_info_prop: elem.layout_info_prop.take(),
accessibility_props: std::mem::take(&mut elem.accessibility_props),
is_flickable_viewport: elem.is_flickable_viewport,
has_popup_child: elem.has_popup_child,
item_index: Default::default(), // Not determined yet

View file

@ -0,0 +1,31 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
Button1 := Rectangle {
property <bool> cond;
accessible-role: cond ? button : AccessibleRole.text;
// ^error{The `accessible-role` property must be a constant expression}
}
Button2 := Rectangle {
accessible-label: "the button";
// ^error{The `accessible-label` property can only be set in combination to `accessible-role`}
}
Button3 := Rectangle {
Rectangle {
accessible-role: text;
accessible-label: "the button";
}
}
Test := Window {
Button1 { }
Button1 { accessible-description: "ok"; } // ok because Button1 has a role
Button2 { accessible-role: none; }
Button2 { }
Button3 {}
Button3 { accessible-description: "error";}
// ^error{The `accessible-description` property can only be set in combination to `accessible-role`}
}

View file

@ -1,6 +1,8 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
// cSpell: ignore imum
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
@ -84,6 +86,18 @@ pub(crate) const RESERVED_DROP_SHADOW_PROPERTIES: &[(&str, Type)] = &[
("drop-shadow-color", Type::Color),
];
pub(crate) const RESERVED_ACCESSIBILITY_PROPERTIES: &[(&str, Type)] = &[
//("accessible-role", ...)
("accessible-checked", Type::Bool),
("accessible-delegate-focus", Type::Int32),
("accessible-description", Type::String),
("accessible-label", Type::String),
("accessible-value", Type::String),
("accessible-value-maximum", Type::Float32),
("accessible-value-minimum", Type::Float32),
("accessible-value-step", Type::Float32),
];
/// list of reserved property injected in every item
pub fn reserved_properties() -> impl Iterator<Item = (&'static str, Type)> {
RESERVED_GEOMETRY_PROPERTIES
@ -91,6 +105,7 @@ pub fn reserved_properties() -> impl Iterator<Item = (&'static str, Type)> {
.chain(RESERVED_LAYOUT_PROPERTIES.iter())
.chain(RESERVED_OTHER_PROPERTIES.iter())
.chain(RESERVED_DROP_SHADOW_PROPERTIES.iter())
.chain(RESERVED_ACCESSIBILITY_PROPERTIES.iter())
.map(|(k, v)| (*k, v.clone()))
.chain(IntoIterator::into_iter([
("forward-focus", Type::ElementReference),
@ -99,6 +114,10 @@ pub fn reserved_properties() -> impl Iterator<Item = (&'static str, Type)> {
"dialog-button-role",
Type::Enumeration(BUILTIN_ENUMS.with(|e| e.DialogButtonRole.clone())),
),
(
"accessible-role",
Type::Enumeration(BUILTIN_ENUMS.with(|e| e.AccessibleRole.clone())),
),
]))
}

View file

@ -76,6 +76,9 @@ export Button := Rectangle {
property<image> icon;
property<length> font-size <=> text.font-size;
accessible-role: button;
accessible-label <=> text.text;
border-width: 1px;
border-radius: 2px;
border-color: !enabled ? Palette.neutralLighter : Palette.neutralSecondaryAlt;

View file

@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
// cSpell: ignore standardbutton
// cSpell: ignore combobox spinbox standardbutton
import { LineEditInner, TextEdit, AboutSlint } from "../common/common.slint";
import { StandardButton } from "../common/standardbutton.slint";
@ -18,6 +18,10 @@ export CheckBox := Rectangle {
horizontal-stretch: 0;
vertical-stretch: 0;
accessible-label <=> text.text;
accessible-checked <=> checked;
accessible-role: checkbox;
HorizontalLayout {
spacing: 8px;
@ -126,6 +130,12 @@ export SpinBox := FocusScope {
horizontal-stretch: 1;
vertical-stretch: 0;
accessible-role: spinbox;
accessible-value: value;
accessible-value-minimum: minimum;
accessible-value-maximum: maximum;
accessible-value-step: (maximum - minimum) / 100;
Rectangle {
background: !enabled ? Palette.neutralLighter : Palette.white;
}
@ -206,6 +216,7 @@ export Slider := Rectangle {
property<float> maximum: 100;
property<float> minimum: 0;
property<float> value;
property<bool> has-focus <=> fs.has-focus;
property<bool> enabled <=> touch.enabled;
callback changed(float);
@ -214,6 +225,13 @@ export Slider := Rectangle {
horizontal-stretch: 1;
vertical-stretch: 0;
accessible-role: slider;
accessible-value: value;
accessible-value-minimum: minimum;
accessible-value-maximum: maximum;
accessible-value-step: (maximum - minimum) / 100;
Rectangle {
width: parent.width - parent.min-height;
x: parent.height / 2;
@ -237,8 +255,9 @@ export Slider := Rectangle {
}
handle := Rectangle {
property<length> border: 3px;
width: height;
height: parent.height;
height: parent.height - 2 * border;
border-width: 3px;
border-radius: height / 2;
border-color: !root.enabled ? Palette.neutralTertiaryAlt
@ -246,6 +265,7 @@ export Slider := Rectangle {
: Palette.neutralSecondary;
background: Palette.white;
x: (root.width - handle.width) * (value - minimum)/(maximum - minimum);
y: border;
}
touch := TouchArea {
width: parent.width;
@ -264,6 +284,27 @@ export Slider := Rectangle {
}
}
}
fs := FocusScope {
width: 0px;
key-pressed(event) => {
if (enabled && event.text == Keys.RightArrow) {
value = Math.min(value + 1, maximum);
accept
} else if (enabled && event.text == Keys.LeftArrow) {
value = Math.max(value - 1, minimum);
accept
} else {
reject
}
}
}
Rectangle { // Focus rectangle
border-width: enabled && has-focus ? 1px : 0px;
border-color: Palette.black;
}
}
@ -287,8 +328,6 @@ export GroupBox := VerticalLayout {
}
}
export TabWidgetImpl := Rectangle {
property <length> content-x: 0;
property <length> content-y: tabbar-preferred-height;
@ -333,6 +372,9 @@ export TabImpl := Rectangle {
horizontal-stretch: 0;
vertical-stretch: 0;
accessible-role: tab;
accessible-label <=> title;
touch := TouchArea {
enabled <=> root.enabled;
clicked => {
@ -375,6 +417,9 @@ export TabBarImpl := Rectangle {
@children
}
accessible-role: tab;
accessible-delegate-focus: current-focused >= 0 ? current-focused : current;
fs := FocusScope {
width: 0px; // Do not react on clicks
property<int> focused-tab: 0;
@ -488,6 +533,9 @@ export ComboBox := FocusScope {
//property <bool> is-open: false;
callback selected(string);
accessible-role: combobox;
accessible-value <=> current-value;
key-pressed(event) => {
if (event.text == Keys.UpArrow) {
current-index = Math.max(current-index - 1, 0);

View file

@ -1,6 +1,8 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
// cSpell: ignore combobox spinbox
import { LineEditInner, TextEdit, AboutSlint } from "../common/common.slint";
import { StyleMetrics, ScrollView } from "std-widgets-impl.slint";
export { StyleMetrics, ScrollView, TextEdit, AboutSlint, AboutSlint as AboutSixtyFPS }
@ -8,6 +10,8 @@ export { StyleMetrics, ScrollView, TextEdit, AboutSlint, AboutSlint as AboutSixt
// FIXME: the font-size should be removed but is required right now to compile the printer-demo
export Button := NativeButton {
property<length> font-size;
accessible-label <=> text;
accessible-role: button;
enabled: true;
}
@ -15,11 +19,50 @@ export StandardButton := NativeButton {
property<StandardButtonKind> kind <=> self.standard-button-kind;
is-standard-button: true;
}
export CheckBox := NativeCheckBox { }
export CheckBox := NativeCheckBox {
accessible-role: checkbox;
accessible-label <=> text;
accessible-checked <=> checked;
}
export SpinBox := NativeSpinBox {
property<length> font-size;
accessible-role: spinbox;
accessible-value: value;
accessible-value-minimum: minimum;
accessible-value-maximum: maximum;
accessible-value-step: (maximum - minimum) / 100;
}
export Slider := NativeSlider { }
export Slider := NativeSlider {
accessible-role: slider;
accessible-value: value;
accessible-value-minimum: minimum;
accessible-value-maximum: maximum;
accessible-value-step: (maximum - minimum) / 100;
property <bool> has-focus <=> fs.has-focus;
fs := FocusScope {
width: 0px;
key-pressed(event) => {
if (root.enabled && event.text == Keys.RightArrow) {
root.value = Math.min(root.value + 1, root.maximum);
accept
} else if (root.enabled && event.text == Keys.LeftArrow) {
root.value = Math.max(root.value - 1, root.minimum);
accept
} else {
reject
}
}
}
}
export GroupBox := NativeGroupBox {
GridLayout {
padding-left: root.native-padding-left;
@ -28,7 +71,8 @@ export GroupBox := NativeGroupBox {
padding-bottom: root.native-padding-bottom;
@children
}
}
}
export LineEdit := NativeLineEdit {
property <length> font-size <=> inner.font-size;
property <string> text <=> inner.text;
@ -87,7 +131,6 @@ export StandardListView := ListView {
}
}
export ComboBox := NativeComboBox {
property <[string]> model;
property <int> current-index : -1;
@ -95,6 +138,9 @@ export ComboBox := NativeComboBox {
open-popup => { popup.show(); }
callback selected(string);
accessible-role: combobox;
accessible-value <=> current-value;
popup := PopupWindow {
Rectangle { background: NativeStyleMetrics.window-background; }
NativeComboBoxPopup {
@ -122,11 +168,33 @@ export ComboBox := NativeComboBox {
}
}
}
fs := FocusScope {
key-pressed(event) => {
if (event.text == Keys.UpArrow) {
root.current-index = Math.max(root.current-index - 1, 0);
root.current-value = model[root.current-index];
return accept;
} else if (event.text == Keys.DownArrow) {
root.current-index = Math.min(root.current-index + 1, root.model.length - 1);
root.current-value = model[root.current-index];
return accept;
// PopupWindow can not get hidden again at this time, so do not allow to pop that up.
// } else if (event.text == Keys.Return) {
// touch.clicked()
// return accept;
}
return reject;
}
}
}
export TabWidgetImpl := NativeTabWidget { }
export TabImpl := NativeTab { }
export TabImpl := NativeTab {
accessible-role: tab;
accessible-label <=> title;
}
export TabBarImpl := Rectangle {
// injected properties:
@ -134,6 +202,9 @@ export TabBarImpl := Rectangle {
property<int> current-focused: fs.has-focus ? current : -1; // The currently focused tab
property<int> num-tabs; // The total number of tabs
accessible-role: tab;
accessible-delegate-focus: current;
HorizontalLayout {
spacing: 0px; // Qt renders Tabs next to each other and renders "spacing" as part of the tab itself
alignment: NativeStyleMetrics.tab-bar-alignment;
@ -162,10 +233,12 @@ export VerticalBox := VerticalLayout {
spacing: NativeStyleMetrics.layout-spacing;
padding: NativeStyleMetrics.layout-spacing;
}
export HorizontalBox := HorizontalLayout {
spacing: NativeStyleMetrics.layout-spacing;
padding: NativeStyleMetrics.layout-spacing;
}
export GridBox := GridLayout {
spacing: NativeStyleMetrics.layout-spacing;
padding: NativeStyleMetrics.layout-spacing;