1#![cfg_attr(target_family = "wasm", no_main)]
2
3use gpui::{
4 App, Bounds, Context, FocusHandle, KeyBinding, Orientation, Role, SharedString, Toggled,
5 Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size,
6};
7use gpui_platform::application;
8
9actions!(a11y_example, [Tab, TabPrev, ToggleDarkMode]);
10
11// --- Data tables demo ---
12
13struct FileEntry {
14 name: &'static str,
15 kind: &'static str,
16 size: &'static str,
17}
18
19const FILES: &[FileEntry] = &[
20 FileEntry {
21 name: "README.md",
22 kind: "Markdown",
23 size: "4 KB",
24 },
25 FileEntry {
26 name: "main.rs",
27 kind: "Rust",
28 size: "12 KB",
29 },
30 FileEntry {
31 name: "Cargo.toml",
32 kind: "TOML",
33 size: "1 KB",
34 },
35 FileEntry {
36 name: "lib.rs",
37 kind: "Rust",
38 size: "8 KB",
39 },
40];
41
42// --- Tree data ---
43
44struct TreeNode {
45 label: &'static str,
46 depth: usize,
47 children: &'static [TreeNode],
48}
49
50const FILE_TREE: &[TreeNode] = &[
51 TreeNode {
52 label: "src",
53 depth: 1,
54 children: &[
55 TreeNode {
56 label: "main.rs",
57 depth: 2,
58 children: &[],
59 },
60 TreeNode {
61 label: "lib.rs",
62 depth: 2,
63 children: &[],
64 },
65 ],
66 },
67 TreeNode {
68 label: "tests",
69 depth: 1,
70 children: &[TreeNode {
71 label: "integration.rs",
72 depth: 2,
73 children: &[],
74 }],
75 },
76 TreeNode {
77 label: "README.md",
78 depth: 1,
79 children: &[],
80 },
81];
82
83// --- App state ---
84
85struct A11yExample {
86 focus_handle: FocusHandle,
87 dark_mode: bool,
88 notifications_enabled: bool,
89 auto_save: bool,
90 selected_tab: usize,
91 progress: f64,
92 expanded_tree_nodes: Vec<bool>,
93 selected_tree_node: Option<usize>,
94 selected_file_row: Option<usize>,
95 status_message: SharedString,
96}
97
98impl A11yExample {
99 fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
100 let focus_handle = cx.focus_handle();
101 window.focus(&focus_handle, cx);
102
103 Self {
104 focus_handle,
105 dark_mode: false,
106 notifications_enabled: true,
107 auto_save: false,
108 selected_tab: 0,
109 progress: 0.65,
110 expanded_tree_nodes: vec![true, true, false],
111 selected_tree_node: None,
112 selected_file_row: None,
113 status_message: "Welcome! This demo showcases GPUI accessibility features.".into(),
114 }
115 }
116
117 fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
118 window.focus_next(cx);
119 }
120
121 fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
122 window.focus_prev(cx);
123 }
124
125 fn bg(&self) -> gpui::Hsla {
126 if self.dark_mode {
127 rgb(0x1e1e2e).into()
128 } else {
129 rgb(0xf5f5f5).into()
130 }
131 }
132
133 fn fg(&self) -> gpui::Hsla {
134 if self.dark_mode {
135 rgb(0xcdd6f4).into()
136 } else {
137 rgb(0x1e1e2e).into()
138 }
139 }
140
141 fn subtle(&self) -> gpui::Hsla {
142 if self.dark_mode {
143 rgb(0x45475a).into()
144 } else {
145 rgb(0xd0d0d0).into()
146 }
147 }
148
149 fn surface(&self) -> gpui::Hsla {
150 if self.dark_mode {
151 rgb(0x313244).into()
152 } else {
153 rgb(0xffffff).into()
154 }
155 }
156
157 fn accent(&self) -> gpui::Hsla {
158 if self.dark_mode {
159 rgb(0x89b4fa).into()
160 } else {
161 rgb(0x1a73e8).into()
162 }
163 }
164
165 fn accent_fg(&self) -> gpui::Hsla {
166 rgb(0xffffff).into()
167 }
168
169 fn success(&self) -> gpui::Hsla {
170 if self.dark_mode {
171 rgb(0xa6e3a1).into()
172 } else {
173 rgb(0x2e7d32).into()
174 }
175 }
176
177 // --- Section builders ---
178
179 fn render_heading(&self, text: &str) -> impl IntoElement {
180 div()
181 .text_lg()
182 .font_weight(gpui::FontWeight::BOLD)
183 .text_color(self.fg())
184 .mb_1()
185 .child(text.to_string())
186 }
187
188 fn render_tab_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
189 let tabs = ["Overview", "Settings", "Data"];
190 let selected = self.selected_tab;
191
192 div()
193 .id("tab-bar")
194 .role(Role::TabList)
195 .aria_label("Main sections")
196 .aria_orientation(Orientation::Horizontal)
197 .flex()
198 .flex_row()
199 .gap_1()
200 .mb_4()
201 .children(tabs.iter().enumerate().map(|(index, label)| {
202 let is_selected = index == selected;
203 div()
204 .id(("tab", index))
205 .role(Role::Tab)
206 .aria_label(SharedString::from(*label))
207 .aria_selected(is_selected)
208 .aria_position_in_set(index + 1)
209 .aria_size_of_set(tabs.len())
210 .px_4()
211 .py_1()
212 .cursor_pointer()
213 .rounded_t_md()
214 .font_weight(if is_selected {
215 gpui::FontWeight::BOLD
216 } else {
217 gpui::FontWeight::NORMAL
218 })
219 .text_color(if is_selected {
220 self.accent()
221 } else {
222 self.fg()
223 })
224 .border_b_2()
225 .border_color(if is_selected {
226 self.accent()
227 } else {
228 gpui::transparent_black()
229 })
230 .hover(|s| s.bg(self.subtle().opacity(0.3)))
231 .on_click(cx.listener(move |this, _, _, cx| {
232 this.selected_tab = index;
233 this.status_message =
234 SharedString::from(format!("Switched to {} tab.", tabs[index]));
235 cx.notify();
236 }))
237 .child(label.to_string())
238 }))
239 }
240
241 fn render_overview_panel(&self, cx: &mut Context<Self>) -> impl IntoElement {
242 div()
243 .id("overview-panel")
244 .role(Role::TabPanel)
245 .aria_label("Overview")
246 .flex()
247 .flex_col()
248 .gap_4()
249 .child(self.render_heading("Buttons"))
250 .child(self.render_buttons(cx))
251 .child(self.render_heading("Progress"))
252 .child(self.render_progress_bar(cx))
253 .child(self.render_heading("File Tree"))
254 .child(self.render_tree(cx))
255 }
256
257 fn render_buttons(&self, cx: &mut Context<Self>) -> impl IntoElement {
258 div()
259 .id("button-group")
260 .role(Role::Group)
261 .aria_label("Actions")
262 .flex()
263 .flex_row()
264 .gap_2()
265 .child(
266 div()
267 .id("btn-primary")
268 .role(Role::Button)
269 .aria_label("Run build")
270 .px_4()
271 .py_1()
272 .rounded_md()
273 .bg(self.accent())
274 .text_color(self.accent_fg())
275 .cursor_pointer()
276 .hover(|s| s.opacity(0.85))
277 .on_click(cx.listener(|this, _, _, cx| {
278 this.status_message = "Build started!".into();
279 this.progress = 0.0;
280 cx.notify();
281 }))
282 .child("Run Build"),
283 )
284 .child(
285 div()
286 .id("btn-increment")
287 .role(Role::Button)
288 .aria_label("Increment progress by 10%")
289 .px_4()
290 .py_1()
291 .rounded_md()
292 .border_1()
293 .border_color(self.accent())
294 .text_color(self.accent())
295 .cursor_pointer()
296 .hover(|s| s.bg(self.accent().opacity(0.1)))
297 .on_click(cx.listener(|this, _, _, cx| {
298 this.progress = (this.progress + 0.1).min(1.0);
299 let pct = (this.progress * 100.0) as u32;
300 this.status_message =
301 SharedString::from(format!("Progress: {}%", pct));
302 cx.notify();
303 }))
304 .child("+10%"),
305 )
306 .child(
307 div()
308 .id("btn-reset")
309 .role(Role::Button)
310 .aria_label("Reset progress")
311 .px_4()
312 .py_1()
313 .rounded_md()
314 .border_1()
315 .border_color(self.subtle())
316 .text_color(self.fg())
317 .cursor_pointer()
318 .hover(|s| s.bg(self.subtle().opacity(0.3)))
319 .on_click(cx.listener(|this, _, _, cx| {
320 this.progress = 0.0;
321 this.status_message = "Progress reset.".into();
322 cx.notify();
323 }))
324 .child("Reset"),
325 )
326 }
327
328 fn render_progress_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
329 let pct = (self.progress * 100.0) as u32;
330 let bar_color = if self.progress >= 1.0 {
331 self.success()
332 } else {
333 self.accent()
334 };
335
336 div()
337 .flex()
338 .flex_col()
339 .gap_1()
340 .child(
341 div()
342 .id("progress-bar")
343 .role(Role::ProgressIndicator)
344 .aria_label("Build progress")
345 .aria_numeric_value(self.progress * 100.0)
346 .aria_min_numeric_value(0.0)
347 .aria_max_numeric_value(100.0)
348 .h(px(12.0))
349 .w_full()
350 .rounded_full()
351 .bg(self.subtle().opacity(0.5))
352 .overflow_hidden()
353 .child(
354 div()
355 .h_full()
356 .w(gpui::relative(self.progress as f32))
357 .rounded_full()
358 .bg(bar_color),
359 ),
360 )
361 .child(
362 div()
363 .text_xs()
364 .text_color(self.fg().opacity(0.7))
365 .child(format!("{}% complete", pct)),
366 )
367 .child(
368 div()
369 .flex()
370 .flex_row()
371 .gap_2()
372 .mt_1()
373 .children((0..5).map(|index| {
374 let step_progress = (index as f64 + 1.0) * 0.2;
375 let is_done = self.progress >= step_progress;
376 div()
377 .id(("progress-step", index))
378 .role(Role::ListItem)
379 .aria_label(SharedString::from(format!("Step {}", index + 1)))
380 .aria_position_in_set(index + 1)
381 .aria_size_of_set(5)
382 .size_6()
383 .rounded_full()
384 .flex()
385 .justify_center()
386 .items_center()
387 .text_xs()
388 .bg(if is_done {
389 bar_color
390 } else {
391 self.subtle().opacity(0.5)
392 })
393 .text_color(if is_done {
394 self.accent_fg()
395 } else {
396 self.fg().opacity(0.5)
397 })
398 .cursor_pointer()
399 .on_click(cx.listener(move |this, _, _, cx| {
400 this.progress = step_progress;
401 let pct = (step_progress * 100.0) as u32;
402 this.status_message =
403 SharedString::from(format!("Progress set to {}%.", pct));
404 cx.notify();
405 }))
406 .child(format!("{}", index + 1))
407 })),
408 )
409 }
410
411 fn render_tree(&self, cx: &mut Context<Self>) -> impl IntoElement {
412 let mut flat_index = 0usize;
413
414 div()
415 .id("file-tree")
416 .role(Role::Tree)
417 .aria_label("Project files")
418 .flex()
419 .flex_col()
420 .border_1()
421 .border_color(self.subtle())
422 .rounded_md()
423 .p_2()
424 .children(FILE_TREE.iter().enumerate().flat_map(
425 |(root_index, node)| {
426 let mut items = Vec::new();
427 let current_index = flat_index;
428 let is_expanded = self
429 .expanded_tree_nodes
430 .get(root_index)
431 .copied()
432 .unwrap_or(false);
433 let is_selected = self.selected_tree_node == Some(current_index);
434 let has_children = !node.children.is_empty();
435
436 items.push(
437 div()
438 .id(("tree-node", current_index))
439 .role(Role::TreeItem)
440 .aria_label(SharedString::from(node.label))
441 .aria_level(node.depth)
442 .aria_selected(is_selected)
443 .aria_position_in_set(root_index + 1)
444 .aria_size_of_set(FILE_TREE.len())
445 .when(has_children, |this| this.aria_expanded(is_expanded))
446 .pl(px(node.depth as f32 * 16.0))
447 .py(px(2.0))
448 .px_2()
449 .rounded_sm()
450 .cursor_pointer()
451 .text_color(self.fg())
452 .when(is_selected, |this| {
453 this.bg(self.accent().opacity(0.15))
454 })
455 .hover(|s| s.bg(self.subtle().opacity(0.3)))
456 .on_click(cx.listener(move |this, _, _, cx| {
457 this.selected_tree_node = Some(current_index);
458 if has_children {
459 if let Some(val) =
460 this.expanded_tree_nodes.get_mut(root_index)
461 {
462 *val = !*val;
463 }
464 }
465 this.status_message = SharedString::from(format!(
466 "Selected: {}",
467 node.label
468 ));
469 cx.notify();
470 }))
471 .child(format!(
472 "{} {}",
473 if has_children {
474 if is_expanded {
475 "▾"
476 } else {
477 "▸"
478 }
479 } else {
480 " "
481 },
482 node.label
483 )),
484 );
485 flat_index += 1;
486
487 if has_children && is_expanded {
488 for (child_index, child) in node.children.iter().enumerate() {
489 let child_flat_index = flat_index;
490 let child_is_selected =
491 self.selected_tree_node == Some(child_flat_index);
492
493 items.push(
494 div()
495 .id(("tree-node", child_flat_index))
496 .role(Role::TreeItem)
497 .aria_label(SharedString::from(child.label))
498 .aria_level(child.depth)
499 .aria_selected(child_is_selected)
500 .aria_position_in_set(child_index + 1)
501 .aria_size_of_set(node.children.len())
502 .pl(px(child.depth as f32 * 16.0))
503 .py(px(2.0))
504 .px_2()
505 .rounded_sm()
506 .cursor_pointer()
507 .text_color(self.fg())
508 .when(child_is_selected, |this| {
509 this.bg(self.accent().opacity(0.15))
510 })
511 .hover(|s| s.bg(self.subtle().opacity(0.3)))
512 .on_click(cx.listener(move |this, _, _, cx| {
513 this.selected_tree_node = Some(child_flat_index);
514 this.status_message = SharedString::from(format!(
515 "Selected: {}",
516 child.label
517 ));
518 cx.notify();
519 }))
520 .child(format!(" {}", child.label)),
521 );
522 flat_index += 1;
523 }
524 }
525
526 items
527 },
528 ))
529 }
530
531 fn render_settings_panel(&self, cx: &mut Context<Self>) -> impl IntoElement {
532 div()
533 .id("settings-panel")
534 .role(Role::TabPanel)
535 .aria_label("Settings")
536 .flex()
537 .flex_col()
538 .gap_4()
539 .child(self.render_heading("Preferences"))
540 .child(
541 div()
542 .id("settings-group")
543 .role(Role::Group)
544 .aria_label("Application preferences")
545 .flex()
546 .flex_col()
547 .gap_3()
548 .child(self.render_toggle(
549 "dark-mode",
550 "Dark mode",
551 self.dark_mode,
552 Role::Switch,
553 cx,
554 |this, _, _, cx| {
555 this.dark_mode = !this.dark_mode;
556 this.status_message = if this.dark_mode {
557 "Dark mode enabled.".into()
558 } else {
559 "Dark mode disabled.".into()
560 };
561 cx.notify();
562 },
563 ))
564 .child(self.render_toggle(
565 "notifications",
566 "Enable notifications",
567 self.notifications_enabled,
568 Role::Switch,
569 cx,
570 |this, _, _, cx| {
571 this.notifications_enabled = !this.notifications_enabled;
572 this.status_message = if this.notifications_enabled {
573 "Notifications enabled.".into()
574 } else {
575 "Notifications disabled.".into()
576 };
577 cx.notify();
578 },
579 ))
580 .child(self.render_toggle(
581 "auto-save",
582 "Auto-save files",
583 self.auto_save,
584 Role::CheckBox,
585 cx,
586 |this, _, _, cx| {
587 this.auto_save = !this.auto_save;
588 this.status_message = if this.auto_save {
589 "Auto-save enabled.".into()
590 } else {
591 "Auto-save disabled.".into()
592 };
593 cx.notify();
594 },
595 )),
596 )
597 }
598
599 fn render_toggle(
600 &self,
601 id: &'static str,
602 label: &'static str,
603 value: bool,
604 role: Role,
605 cx: &mut Context<Self>,
606 on_click: impl Fn(&mut Self, &gpui::ClickEvent, &mut Window, &mut Context<Self>) + 'static,
607 ) -> impl IntoElement {
608 let toggled = if value {
609 Toggled::True
610 } else {
611 Toggled::False
612 };
613
614 let is_switch = role == Role::Switch;
615
616 div()
617 .flex()
618 .flex_row()
619 .items_center()
620 .gap_3()
621 .child(
622 div()
623 .id(id)
624 .role(role)
625 .aria_label(SharedString::from(label))
626 .aria_toggled(toggled)
627 .cursor_pointer()
628 .on_click(cx.listener(on_click))
629 .when(is_switch, |this| {
630 this.w(px(40.0))
631 .h(px(22.0))
632 .rounded_full()
633 .bg(if value {
634 self.accent()
635 } else {
636 self.subtle()
637 })
638 .p(px(2.0))
639 .child(
640 div()
641 .size(px(18.0))
642 .rounded_full()
643 .bg(gpui::white())
644 .when(value, |this| this.ml(px(18.0))),
645 )
646 })
647 .when(!is_switch, |this| {
648 this.size(px(18.0))
649 .rounded_sm()
650 .border_2()
651 .border_color(if value {
652 self.accent()
653 } else {
654 self.subtle()
655 })
656 .bg(if value {
657 self.accent()
658 } else {
659 gpui::transparent_black()
660 })
661 .flex()
662 .justify_center()
663 .items_center()
664 .text_xs()
665 .text_color(self.accent_fg())
666 .when(value, |this| this.child("✓"))
667 }),
668 )
669 .child(
670 div()
671 .text_color(self.fg())
672 .child(label.to_string()),
673 )
674 }
675
676 fn render_data_panel(&self, cx: &mut Context<Self>) -> impl IntoElement {
677 let column_count = 3;
678 let row_count = FILES.len();
679
680 div()
681 .id("data-panel")
682 .role(Role::TabPanel)
683 .aria_label("Data")
684 .flex()
685 .flex_col()
686 .gap_4()
687 .child(self.render_heading("File Table"))
688 .child(
689 div()
690 .id("file-table")
691 .role(Role::Table)
692 .aria_label("Project files")
693 .aria_row_count(row_count + 1)
694 .aria_column_count(column_count)
695 .flex()
696 .flex_col()
697 .border_1()
698 .border_color(self.subtle())
699 .rounded_md()
700 .overflow_hidden()
701 .child(
702 div()
703 .id("table-header")
704 .role(Role::Row)
705 .aria_row_index(1)
706 .flex()
707 .flex_row()
708 .bg(self.subtle().opacity(0.3))
709 .font_weight(gpui::FontWeight::BOLD)
710 .text_color(self.fg())
711 .child(self.render_cell("header-name", "Name", 1, column_count, true))
712 .child(self.render_cell("header-type", "Type", 2, column_count, true))
713 .child(self.render_cell("header-size", "Size", 3, column_count, true)),
714 )
715 .children(FILES.iter().enumerate().map(|(row_index, file)| {
716 let is_selected = self.selected_file_row == Some(row_index);
717
718 div()
719 .id(("table-row", row_index))
720 .role(Role::Row)
721 .aria_row_index(row_index + 2)
722 .aria_selected(is_selected)
723 .flex()
724 .flex_row()
725 .cursor_pointer()
726 .text_color(self.fg())
727 .when(is_selected, |this| {
728 this.bg(self.accent().opacity(0.15))
729 })
730 .when(row_index % 2 == 1, |this| {
731 this.bg(self.subtle().opacity(0.1))
732 })
733 .hover(|s| s.bg(self.accent().opacity(0.1)))
734 .on_click(cx.listener(move |this, _, _, cx| {
735 this.selected_file_row = Some(row_index);
736 this.status_message = SharedString::from(format!(
737 "Selected file: {}",
738 FILES[row_index].name
739 ));
740 cx.notify();
741 }))
742 .child(self.render_cell(
743 ("cell-name", row_index),
744 file.name,
745 1,
746 column_count,
747 false,
748 ))
749 .child(self.render_cell(
750 ("cell-type", row_index),
751 file.kind,
752 2,
753 column_count,
754 false,
755 ))
756 .child(self.render_cell(
757 ("cell-size", row_index),
758 file.size,
759 3,
760 column_count,
761 false,
762 ))
763 })),
764 )
765 .child(self.render_heading("Item List"))
766 .child(self.render_list())
767 }
768
769 fn render_cell(
770 &self,
771 id: impl Into<gpui::ElementId>,
772 text: &str,
773 column: usize,
774 total_columns: usize,
775 is_header: bool,
776 ) -> impl IntoElement {
777 div()
778 .id(id.into())
779 .role(if is_header {
780 Role::ColumnHeader
781 } else {
782 Role::Cell
783 })
784 .aria_label(SharedString::from(text.to_string()))
785 .aria_column_index(column)
786 .aria_column_count(total_columns)
787 .flex_1()
788 .px_3()
789 .py_2()
790 .child(text.to_string())
791 }
792
793 fn render_list(&self) -> impl IntoElement {
794 let items = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
795
796 div()
797 .id("demo-list")
798 .role(Role::List)
799 .aria_label("Greek letters")
800 .flex()
801 .flex_col()
802 .border_1()
803 .border_color(self.subtle())
804 .rounded_md()
805 .children(items.iter().enumerate().map(|(index, label)| {
806 div()
807 .id(("list-item", index))
808 .role(Role::ListItem)
809 .aria_label(SharedString::from(*label))
810 .aria_position_in_set(index + 1)
811 .aria_size_of_set(items.len())
812 .px_3()
813 .py_1()
814 .text_color(self.fg())
815 .when(index % 2 == 1, |this| {
816 this.bg(self.subtle().opacity(0.1))
817 })
818 .child(format!("{}. {}", index + 1, label))
819 }))
820 }
821
822 fn render_status_bar(&self) -> impl IntoElement {
823 div()
824 .id("status-bar")
825 .role(Role::Status)
826 .aria_label(self.status_message.clone())
827 .w_full()
828 .px_4()
829 .py_2()
830 .bg(self.subtle().opacity(0.3))
831 .rounded_md()
832 .text_sm()
833 .text_color(self.fg().opacity(0.8))
834 .child(self.status_message.clone())
835 }
836}
837
838impl Render for A11yExample {
839 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
840 let tab_content: gpui::AnyElement = match self.selected_tab {
841 0 => self.render_overview_panel(cx).into_any_element(),
842 1 => self.render_settings_panel(cx).into_any_element(),
843 2 => self.render_data_panel(cx).into_any_element(),
844 _ => div().child("Unknown tab").into_any_element(),
845 };
846
847 div()
848 .id("app-root")
849 .role(Role::Application)
850 .aria_label("Accessibility Demo")
851 .track_focus(&self.focus_handle)
852 .on_action(cx.listener(Self::on_tab))
853 .on_action(cx.listener(Self::on_tab_prev))
854 .size_full()
855 .flex()
856 .flex_col()
857 .bg(self.bg())
858 .font_family("sans-serif")
859 .child(
860 div()
861 .id("header")
862 .role(Role::Banner)
863 .aria_label("Application header")
864 .w_full()
865 .px_6()
866 .py_3()
867 .bg(self.surface())
868 .border_b_1()
869 .border_color(self.subtle())
870 .flex()
871 .flex_row()
872 .items_center()
873 .justify_between()
874 .child(
875 div()
876 .flex()
877 .flex_row()
878 .items_center()
879 .gap_2()
880 .child(
881 div()
882 .text_xl()
883 .font_weight(gpui::FontWeight::BOLD)
884 .text_color(self.accent())
885 .child("♿"),
886 )
887 .child(
888 div()
889 .text_lg()
890 .font_weight(gpui::FontWeight::BOLD)
891 .text_color(self.fg())
892 .child("GPUI Accessibility Demo"),
893 ),
894 )
895 .child(
896 div()
897 .id("theme-toggle")
898 .role(Role::Button)
899 .aria_label(if self.dark_mode {
900 "Switch to light mode"
901 } else {
902 "Switch to dark mode"
903 })
904 .px_3()
905 .py_1()
906 .rounded_md()
907 .cursor_pointer()
908 .border_1()
909 .border_color(self.subtle())
910 .text_color(self.fg())
911 .hover(|s| s.bg(self.subtle().opacity(0.3)))
912 .on_click(cx.listener(|this, _, _, cx| {
913 this.dark_mode = !this.dark_mode;
914 this.status_message = if this.dark_mode {
915 "Dark mode enabled.".into()
916 } else {
917 "Dark mode disabled.".into()
918 };
919 cx.notify();
920 }))
921 .child(if self.dark_mode { "☀ Light" } else { "🌙 Dark" }),
922 ),
923 )
924 .child(
925 div()
926 .id("main-content")
927 .role(Role::Main)
928 .aria_label("Main content")
929 .flex_1()
930 .overflow_y_scroll()
931 .px_6()
932 .py_4()
933 .flex()
934 .flex_col()
935 .gap_2()
936 .child(self.render_tab_bar(cx))
937 .child(tab_content),
938 )
939 .child(
940 div()
941 .id("footer")
942 .role(Role::ContentInfo)
943 .aria_label("Status")
944 .px_6()
945 .py_2()
946 .border_t_1()
947 .border_color(self.subtle())
948 .child(self.render_status_bar()),
949 )
950 }
951}
952
953fn run_example() {
954 application().run(|cx: &mut App| {
955 cx.bind_keys([
956 KeyBinding::new("tab", Tab, None),
957 KeyBinding::new("shift-tab", TabPrev, None),
958 ]);
959
960 let bounds = Bounds::centered(None, size(px(800.), px(700.0)), cx);
961 cx.open_window(
962 WindowOptions {
963 window_bounds: Some(WindowBounds::Windowed(bounds)),
964 ..Default::default()
965 },
966 |window, cx| cx.new(|cx| A11yExample::new(window, cx)),
967 )
968 .unwrap();
969
970 cx.activate(true);
971 });
972}
973
974#[cfg(not(target_family = "wasm"))]
975fn main() {
976 run_example();
977}
978
979#[cfg(target_family = "wasm")]
980#[wasm_bindgen::prelude::wasm_bindgen(start)]
981pub fn start() {
982 gpui_platform::web_init();
983 run_example();
984}