focus_visible.rs

  1use gpui::{
  2    App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window,
  3    WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
  4};
  5use gpui_platform::application;
  6
  7actions!(example, [Tab, TabPrev, Quit]);
  8
  9struct Example {
 10    focus_handle: FocusHandle,
 11    items: Vec<(FocusHandle, &'static str)>,
 12    message: SharedString,
 13}
 14
 15impl Example {
 16    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
 17        let items = vec![
 18            (
 19                cx.focus_handle().tab_index(1).tab_stop(true),
 20                "Button with .focus() - always shows border when focused",
 21            ),
 22            (
 23                cx.focus_handle().tab_index(2).tab_stop(true),
 24                "Button with .focus_visible() - only shows border with keyboard",
 25            ),
 26            (
 27                cx.focus_handle().tab_index(3).tab_stop(true),
 28                "Button with both .focus() and .focus_visible()",
 29            ),
 30        ];
 31
 32        let focus_handle = cx.focus_handle();
 33        window.focus(&focus_handle, cx);
 34
 35        Self {
 36            focus_handle,
 37            items,
 38            message: SharedString::from(
 39                "Try clicking vs tabbing! Click shows no border, Tab shows border.",
 40            ),
 41        }
 42    }
 43
 44    fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
 45        window.focus_next(cx);
 46        self.message = SharedString::from("Pressed Tab - focus-visible border should appear!");
 47    }
 48
 49    fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
 50        window.focus_prev(cx);
 51        self.message =
 52            SharedString::from("Pressed Shift-Tab - focus-visible border should appear!");
 53    }
 54
 55    fn on_quit(&mut self, _: &Quit, _window: &mut Window, cx: &mut Context<Self>) {
 56        cx.quit();
 57    }
 58}
 59
 60impl Render for Example {
 61    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 62        fn button_base(id: impl Into<ElementId>, label: &'static str) -> Stateful<Div> {
 63            div()
 64                .id(id)
 65                .h_16()
 66                .w_full()
 67                .flex()
 68                .justify_center()
 69                .items_center()
 70                .bg(gpui::rgb(0x2563eb))
 71                .text_color(gpui::white())
 72                .rounded_md()
 73                .cursor_pointer()
 74                .hover(|style| style.bg(gpui::rgb(0x1d4ed8)))
 75                .child(label)
 76        }
 77
 78        div()
 79            .id("app")
 80            .track_focus(&self.focus_handle)
 81            .on_action(cx.listener(Self::on_tab))
 82            .on_action(cx.listener(Self::on_tab_prev))
 83            .on_action(cx.listener(Self::on_quit))
 84            .size_full()
 85            .flex()
 86            .flex_col()
 87            .p_8()
 88            .gap_6()
 89            .bg(gpui::rgb(0xf3f4f6))
 90            .child(
 91                div()
 92                    .text_2xl()
 93                    .font_weight(gpui::FontWeight::BOLD)
 94                    .text_color(gpui::rgb(0x111827))
 95                    .child("CSS focus-visible Demo"),
 96            )
 97            .child(
 98                div()
 99                    .p_4()
100                    .rounded_md()
101                    .bg(gpui::rgb(0xdbeafe))
102                    .text_color(gpui::rgb(0x1e3a8a))
103                    .child(self.message.clone()),
104            )
105            .child(
106                div()
107                    .flex()
108                    .flex_col()
109                    .gap_4()
110                    .child(
111                        div()
112                            .flex()
113                            .flex_col()
114                            .gap_2()
115                            .child(
116                                div()
117                                    .text_sm()
118                                    .font_weight(gpui::FontWeight::BOLD)
119                                    .text_color(gpui::rgb(0x374151))
120                                    .child("1. Regular .focus() - always visible:"),
121                            )
122                            .child(
123                                button_base("button1", self.items[0].1)
124                                    .track_focus(&self.items[0].0)
125                                    .focus(|style| {
126                                        style.border_4().border_color(gpui::rgb(0xfbbf24))
127                                    })
128                                    .on_click(cx.listener(|this, _, _, cx| {
129                                        this.message =
130                                            "Clicked button 1 - focus border is visible!".into();
131                                        cx.notify();
132                                    })),
133                            ),
134                    )
135                    .child(
136                        div()
137                            .flex()
138                            .flex_col()
139                            .gap_2()
140                            .child(
141                                div()
142                                    .text_sm()
143                                    .font_weight(gpui::FontWeight::BOLD)
144                                    .text_color(gpui::rgb(0x374151))
145                                    .child("2. New .focus_visible() - only keyboard:"),
146                            )
147                            .child(
148                                button_base("button2", self.items[1].1)
149                                    .track_focus(&self.items[1].0)
150                                    .focus_visible(|style| {
151                                        style.border_4().border_color(gpui::rgb(0x10b981))
152                                    })
153                                    .on_click(cx.listener(|this, _, _, cx| {
154                                        this.message =
155                                            "Clicked button 2 - no border! Try Tab instead.".into();
156                                        cx.notify();
157                                    })),
158                            ),
159                    )
160                    .child(
161                        div()
162                            .flex()
163                            .flex_col()
164                            .gap_2()
165                            .child(
166                                div()
167                                    .text_sm()
168                                    .font_weight(gpui::FontWeight::BOLD)
169                                    .text_color(gpui::rgb(0x374151))
170                                    .child(
171                                        "3. Both .focus() (yellow) and .focus_visible() (green):",
172                                    ),
173                            )
174                            .child(
175                                button_base("button3", self.items[2].1)
176                                    .track_focus(&self.items[2].0)
177                                    .focus(|style| {
178                                        style.border_4().border_color(gpui::rgb(0xfbbf24))
179                                    })
180                                    .focus_visible(|style| {
181                                        style.border_4().border_color(gpui::rgb(0x10b981))
182                                    })
183                                    .on_click(cx.listener(|this, _, _, cx| {
184                                        this.message =
185                                            "Clicked button 3 - yellow border. Tab shows green!"
186                                                .into();
187                                        cx.notify();
188                                    })),
189                            ),
190                    ),
191            )
192    }
193}
194
195fn main() {
196    application().run(|cx: &mut App| {
197        cx.bind_keys([
198            KeyBinding::new("tab", Tab, None),
199            KeyBinding::new("shift-tab", TabPrev, None),
200            KeyBinding::new("cmd-q", Quit, None),
201        ]);
202
203        let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
204        cx.open_window(
205            WindowOptions {
206                window_bounds: Some(WindowBounds::Windowed(bounds)),
207                ..Default::default()
208            },
209            |window, cx| cx.new(|cx| Example::new(window, cx)),
210        )
211        .unwrap();
212
213        cx.activate(true);
214    });
215}