a11y.rs

  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}