Skip to content

Commit 4b6eeec

Browse files
authored
book: add chapter about accessibility (#2237)
* book: add chapter about accessibility * Some improvements * Another review round * Fix ci workflow and format * Address review comments
1 parent 8d0a781 commit 4b6eeec

24 files changed

+816
-1
lines changed

.github/workflows/book-listings.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ jobs:
2929

3030
- run: cargo xtask install
3131
name: Install Meson and schemas
32+
working-directory: book/listings
3233

3334
- run: cargo fmt --check --all
3435
name: "Format"

book/listings/Cargo.toml

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ adw = { version = "0.8", package = "libadwaita", features = ["v1_8"] }
77
anyhow = "1.0"
88
async-channel = "2.5"
99
gettext-rs = { version = "0.7" }
10-
gtk = { version = "0.10", package = "gtk4", features = ["v4_12"] }
10+
gtk = { version = "0.10", package = "gtk4", features = ["v4_14"] }
1111
reqwest = { version = "0.12", default-features = false, features = [
1212
"rustls-tls",
1313
] }
@@ -306,6 +306,31 @@ path = "todo/9/src/main.rs"
306306
name = "todo_10"
307307
path = "todo/10/src/main.rs"
308308

309+
# accessibility
310+
[[bin]]
311+
name = "accessibility_1"
312+
path = "accessibility/1/main.rs"
313+
314+
[[bin]]
315+
name = "accessibility_2"
316+
path = "accessibility/2/main.rs"
317+
318+
[[bin]]
319+
name = "accessibility_3"
320+
path = "accessibility/3/main.rs"
321+
322+
[[bin]]
323+
name = "accessibility_4"
324+
path = "accessibility/4/main.rs"
325+
326+
[[bin]]
327+
name = "accessibility_5"
328+
path = "accessibility/5/main.rs"
329+
330+
[[bin]]
331+
name = "accessibility_6"
332+
path = "accessibility/6/main.rs"
333+
309334
# xtask
310335
[[bin]]
311336
name = "xtask"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use gtk::prelude::*;
2+
use gtk::{Application, ApplicationWindow, Button, Orientation, accessible, glib};
3+
4+
const APP_ID: &str = "org.gtk_rs.Accessibility1";
5+
6+
fn main() -> glib::ExitCode {
7+
let app = Application::builder().application_id(APP_ID).build();
8+
app.connect_activate(build_ui);
9+
app.run()
10+
}
11+
12+
fn build_ui(app: &Application) {
13+
let container = gtk::Box::builder()
14+
.orientation(Orientation::Horizontal)
15+
.spacing(12)
16+
.halign(gtk::Align::Center)
17+
.valign(gtk::Align::Center)
18+
.build();
19+
20+
// ANCHOR: icon_button
21+
// Icon-only button needs an accessible label
22+
let search_button = Button::builder()
23+
.icon_name("system-search-symbolic")
24+
.build();
25+
search_button.update_property(&[accessible::Property::Label("Search")]);
26+
// ANCHOR_END: icon_button
27+
28+
// ANCHOR: description
29+
// Add additional context with a description
30+
let settings_button = Button::builder()
31+
.icon_name("emblem-system-symbolic")
32+
.build();
33+
settings_button.update_property(&[
34+
accessible::Property::Label("Settings"),
35+
accessible::Property::Description("Open application preferences"),
36+
]);
37+
// ANCHOR_END: description
38+
39+
container.append(&search_button);
40+
container.append(&settings_button);
41+
42+
let window = ApplicationWindow::builder()
43+
.application(app)
44+
.title("Icon Buttons")
45+
.default_width(300)
46+
.default_height(200)
47+
.child(&container)
48+
.build();
49+
50+
window.present();
51+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use gtk::prelude::*;
2+
use gtk::{
3+
Application, ApplicationWindow, Entry, Label, Orientation, accessible, glib,
4+
};
5+
6+
const APP_ID: &str = "org.gtk_rs.Accessibility2";
7+
8+
fn main() -> glib::ExitCode {
9+
let app = Application::builder().application_id(APP_ID).build();
10+
app.connect_activate(build_ui);
11+
app.run()
12+
}
13+
14+
// ANCHOR: labelled_by
15+
fn build_ui(app: &Application) {
16+
let container = gtk::Box::builder()
17+
.orientation(Orientation::Horizontal)
18+
.spacing(12)
19+
.margin_start(12)
20+
.margin_end(12)
21+
.margin_top(12)
22+
.margin_bottom(12)
23+
.build();
24+
25+
let label = Label::new(Some("Username:"));
26+
let entry = Entry::new();
27+
28+
// Tell assistive technologies that the entry is labelled by this label
29+
entry.update_relation(&[accessible::Relation::LabelledBy(&[label.upcast_ref()])]);
30+
31+
container.append(&label);
32+
container.append(&entry);
33+
34+
let window = ApplicationWindow::builder()
35+
.application(app)
36+
.title("Form Field")
37+
.default_width(300)
38+
.default_height(100)
39+
.child(&container)
40+
.build();
41+
42+
window.present();
43+
}
44+
// ANCHOR_END: labelled_by
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use std::cell::RefCell;
2+
use std::sync::OnceLock;
3+
4+
use glib::Properties;
5+
use gtk::gdk;
6+
use gtk::prelude::*;
7+
use gtk::subclass::prelude::*;
8+
use gtk::{AccessibleRole, GestureClick, Label, accessible, glib};
9+
10+
// ANCHOR: subclass
11+
#[derive(Properties, Default)]
12+
#[properties(wrapper_type = super::CustomButton)]
13+
pub struct CustomButton {
14+
#[property(get, set)]
15+
label: RefCell<String>,
16+
child: RefCell<Option<Label>>,
17+
}
18+
19+
#[glib::object_subclass]
20+
impl ObjectSubclass for CustomButton {
21+
const NAME: &'static str = "CustomButton";
22+
type Type = super::CustomButton;
23+
type ParentType = gtk::Widget;
24+
25+
fn class_init(klass: &mut Self::Class) {
26+
// Set the accessible role to Button
27+
klass.set_accessible_role(AccessibleRole::Button);
28+
klass.set_css_name("custom-button");
29+
klass.set_layout_manager_type::<gtk::BinLayout>();
30+
31+
// Bind keyboard shortcuts for activation (Enter and Space)
32+
klass.add_binding_signal(
33+
gdk::Key::space,
34+
gdk::ModifierType::empty(),
35+
"activate",
36+
);
37+
klass.add_binding_signal(
38+
gdk::Key::KP_Enter,
39+
gdk::ModifierType::empty(),
40+
"activate",
41+
);
42+
klass.add_binding_signal(
43+
gdk::Key::Return,
44+
gdk::ModifierType::empty(),
45+
"activate",
46+
);
47+
}
48+
}
49+
// ANCHOR_END: subclass
50+
51+
// ANCHOR: object_impl
52+
#[glib::derived_properties]
53+
impl ObjectImpl for CustomButton {
54+
fn signals() -> &'static [glib::subclass::Signal] {
55+
static SIGNALS: OnceLock<Vec<glib::subclass::Signal>> = OnceLock::new();
56+
SIGNALS.get_or_init(|| {
57+
vec![glib::subclass::Signal::builder("activate").action().build()]
58+
})
59+
}
60+
61+
fn constructed(&self) {
62+
self.parent_constructed();
63+
64+
let obj = self.obj();
65+
// Make the widget focusable so keyboard users can reach it
66+
obj.set_focusable(true);
67+
// Also allow focusing by clicking
68+
obj.set_focus_on_click(true);
69+
70+
// Create a child label and bind its text to our "label" property
71+
let child = Label::new(None);
72+
child.set_parent(&*obj);
73+
obj.update_relation(&[accessible::Relation::LabelledBy(&[child.upcast_ref()])]);
74+
obj.bind_property("label", &child, "label")
75+
.sync_create()
76+
.build();
77+
self.child.replace(Some(child));
78+
79+
// Handle click events
80+
let gesture = GestureClick::new();
81+
let button = obj.downgrade();
82+
gesture.connect_released(move |_, _, _, _| {
83+
if let Some(button) = button.upgrade() {
84+
button.emit_by_name::<()>("activate", &[]);
85+
}
86+
});
87+
obj.add_controller(gesture);
88+
89+
// Add an activation handler
90+
obj.connect_local("activate", false, move |values| {
91+
let button = values[0].get::<super::CustomButton>().unwrap();
92+
println!("Button '{}' activated!", button.label());
93+
None
94+
});
95+
}
96+
97+
fn dispose(&self) {
98+
while let Some(child) = self.obj().first_child() {
99+
child.unparent();
100+
}
101+
}
102+
}
103+
104+
impl WidgetImpl for CustomButton {}
105+
// ANCHOR_END: object_impl
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
mod imp;
2+
3+
use gtk::glib;
4+
5+
glib::wrapper! {
6+
pub struct CustomButton(ObjectSubclass<imp::CustomButton>)
7+
@extends gtk::Widget,
8+
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
9+
}
10+
11+
impl CustomButton {
12+
pub fn new(label: &str) -> Self {
13+
glib::Object::builder().property("label", label).build()
14+
}
15+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
mod custom_button;
2+
3+
use gtk::prelude::*;
4+
use gtk::{Application, ApplicationWindow, CssProvider, Orientation, gdk, gio, glib};
5+
6+
use custom_button::CustomButton;
7+
8+
const APP_ID: &str = "org.gtk_rs.Accessibility3";
9+
10+
// ANCHOR: main
11+
fn main() -> glib::ExitCode {
12+
gio::resources_register_include!("accessibility_3.gresource")
13+
.expect("Failed to register resources.");
14+
15+
let app = Application::builder().application_id(APP_ID).build();
16+
app.connect_startup(|_| load_css());
17+
app.connect_activate(build_ui);
18+
app.run()
19+
}
20+
21+
fn load_css() {
22+
let provider = CssProvider::new();
23+
provider.load_from_resource("/org/gtk_rs/Accessibility3/style.css");
24+
gtk::style_context_add_provider_for_display(
25+
&gdk::Display::default().expect("Could not connect to a display."),
26+
&provider,
27+
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
28+
);
29+
}
30+
31+
fn build_ui(app: &Application) {
32+
let button1 = CustomButton::new("Click me");
33+
let button2 = CustomButton::new("Or me");
34+
let container = gtk::Box::new(Orientation::Vertical, 12);
35+
container.append(&button1);
36+
container.append(&button2);
37+
38+
let window = ApplicationWindow::builder()
39+
.application(app)
40+
.title("Custom Button")
41+
.default_width(300)
42+
.default_height(200)
43+
.child(&container)
44+
.build();
45+
46+
window.present();
47+
}
48+
// ANCHOR_END: main
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<gresources>
3+
<gresource prefix="/org/gtk_rs/Accessibility3/">
4+
<file compressed="true">style.css</file>
5+
</gresource>
6+
</gresources>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
custom-button {
2+
padding: 12px;
3+
outline: 0 solid transparent;
4+
outline-offset: 4px;
5+
transition: outline-color 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
6+
outline-width 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
7+
outline-offset 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
8+
}
9+
10+
custom-button:focus:focus-visible {
11+
outline-color: rgba(53, 132, 228, 0.5);
12+
outline-width: 2px;
13+
outline-offset: -2px;
14+
}

0 commit comments

Comments
 (0)