focus_visible.rs

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