focus_visible.rs

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