Skip to main content

gpui_cn/folder_picker/
mod.rs

1//! A dialog to choose a folder, on any file system: the path is typed
2//! text, and the folders of its directory are listed, filtered fuzzily by
3//! what follows the last separator.
4//!
5//! [`FolderPickerState`] holds the path, the listings, and the filtered
6//! rows; [`FolderPicker`] draws them in a [`Dialog`]. Nothing waits on
7//! the UI thread: a [`FolderSource`] returns a task for each page of a
8//! listing, and the filter runs on a background task that the next
9//! keystroke cancels.
10
11mod source;
12
13use std::rc::Rc;
14
15use gpui_kit::{
16    App, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _,
17    IntoElement, ParentElement as _, RenderOnce, Role, SharedString,
18    StatefulInteractiveElement as _, Styled as _, Subscription, Window,
19    assets::IconName,
20    base::{
21        Disableable as _, TestSupportExt as _,
22        actions::{Confirm, SelectDown, SelectUp},
23        h_flex,
24    },
25    div,
26    prelude::FluentBuilder as _,
27};
28
29pub use crate::path_browser::{MoreState, PageToken};
30pub use source::{FolderEntry, FolderPage, FolderSource, LocalFolders, MemoryFolders};
31
32use crate::{
33    ActiveTheme as _, Button, Dialog, Icon, ScrollArea, Theme,
34    menu::MenuLook,
35    path_browser::{
36        self, Body, Browser, CreateFolderError, Host, NewFolder, SourcePath, Submit, view,
37    },
38};
39
40/// The key context of the picker, which takes Up, Down, and Enter the
41/// path field passes on.
42const CONTEXT: &str = "GpuiCnFolderPicker";
43
44const DEFAULT_TITLE: &str = "Choose a folder";
45
46pub(crate) fn init(cx: &mut App) {
47    path_browser::bind_keys(CONTEXT, cx);
48}
49
50/// The folders of a directory loaded so far, and whether more follow.
51pub type Loaded = path_browser::Loaded<FolderEntry>;
52
53/// What a [`FolderPickerState`] knows about one directory.
54pub type Listing = path_browser::Listing<FolderEntry>;
55
56/// What a [`FolderPickerState`] reports.
57#[derive(Clone, Debug, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum FolderPickerEvent {
60    /// A folder was chosen. The path has no trailing separator, except
61    /// for a root, and is in the style of the picker's source.
62    Chosen(SourcePath),
63    /// The picker was cancelled.
64    Cancelled,
65    /// A folder was made in the directory that is listed, from the "New
66    /// folder" row or [`FolderPickerState::create_folder`]. The listing is
67    /// read again, and the new folder is the one "Use folder" chooses.
68    FolderCreated(SourcePath),
69}
70
71/// The path text, the listings, and the filtered rows of a
72/// [`FolderPicker`].
73///
74/// The text is a path. Everything up to and including its last
75/// separator is the directory that is listed; what follows is a fuzzy
76/// query over that directory's folders. Typing a separator after a
77/// query goes inside the folder named exactly like it, or else the best
78/// match. Enter or a click on a row goes inside it too, and the Up
79/// button goes to the parent. A leading `~` stands for the source's home.
80///
81/// A source can list a directory in pages. While more follow, the list
82/// ends with a "Load more" row, and reaching it loads the next page. The
83/// query filters what is loaded, so a match on a later page shows once
84/// that page is loaded.
85///
86/// Every listing is cached, so going back is instant. An answer for a
87/// directory the path has left is dropped, and a new query cancels the
88/// filter before it.
89pub struct FolderPickerState {
90    browser: Browser<FolderEntry>,
91    _subscription: Subscription,
92}
93
94impl Host<FolderEntry> for FolderPickerState {
95    fn browser(&self) -> &Browser<FolderEntry> {
96        &self.browser
97    }
98
99    fn browser_mut(&mut self) -> &mut Browser<FolderEntry> {
100        &mut self.browser
101    }
102
103    fn folder_created(&mut self, path: SourcePath, window: &mut Window, cx: &mut Context<Self>) {
104        if path.parent().as_ref() == Some(&self.browser.dir) {
105            let text = format!(
106                "{}{}",
107                self.browser.dir_text,
108                path.file_name().unwrap_or_default()
109            );
110            self.browser.set_text(&text, window, cx);
111            self.browser.focus_path(window, cx);
112        }
113        cx.emit(FolderPickerEvent::FolderCreated(path));
114    }
115}
116
117impl EventEmitter<FolderPickerEvent> for FolderPickerState {}
118
119/// The path field's focus, which holds the keyboard for the picker.
120impl Focusable for FolderPickerState {
121    fn focus_handle(&self, cx: &App) -> FocusHandle {
122        self.browser.input.read(cx).focus_handle(cx)
123    }
124}
125
126impl FolderPickerState {
127    /// A picker on the local file system, at the user's home folder (the
128    /// root when there is none). Name another start with
129    /// [`with_initial`](Self::with_initial).
130    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
131        let mut browser = Browser::new(Rc::new(source::Adapter(Rc::new(LocalFolders))), window, cx);
132        let subscription = browser.watch(window, cx);
133        let start = browser.default_start();
134        browser.set_text(&start, window, cx);
135        Self {
136            browser,
137            _subscription: subscription,
138        }
139    }
140
141    /// Lists folders from `source` instead of the local file system. The
142    /// listings cached so far are dropped and the current directory is
143    /// listed again. The picker starts at the new source's home, unless
144    /// [`with_initial`](Self::with_initial) named a start, and at the root
145    /// when the source has no home. A leading `~` in the path stands for
146    /// the source's home. See [`FolderSource`] for how to write one.
147    pub fn with_source(
148        mut self,
149        source: impl FolderSource,
150        window: &mut Window,
151        cx: &mut Context<Self>,
152    ) -> Self {
153        self.set_source(source, window, cx);
154        self
155    }
156
157    /// Lists from `source` from now on, as [`with_source`](Self::with_source)
158    /// does for a picker being built: the listings cached so far are
159    /// dropped, the requests in flight are cancelled, and the picker goes
160    /// to the new source's home, or keeps the path it was given with
161    /// `with_initial`.
162    pub fn set_source(
163        &mut self,
164        source: impl FolderSource,
165        window: &mut Window,
166        cx: &mut Context<Self>,
167    ) {
168        self.browser
169            .set_source(Rc::new(source::Adapter(Rc::new(source))), window, cx);
170        cx.notify();
171    }
172
173    /// Starts in `path`, which is listed at once. A path that starts with
174    /// `~` is in the source's home. This wins over the source's home as
175    /// the start.
176    pub fn with_initial(
177        mut self,
178        path: impl AsRef<str>,
179        window: &mut Window,
180        cx: &mut Context<Self>,
181    ) -> Self {
182        self.browser.start_in(path.as_ref(), window, cx);
183        self
184    }
185
186    /// Sets what the "Sign in" button does. A source that needs the user to
187    /// log in fails a listing with
188    /// [`ListError::AuthRequired`](crate::ListError::AuthRequired), and the
189    /// list then shows the source's message with a "Sign in" button that
190    /// calls `handler`. The picker holds no login code: the application
191    /// signs the user in, then calls [`retry`](Self::retry) to list again.
192    /// Without a handler the button is not shown.
193    pub fn on_auth_required(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
194        self.browser.set_auth_handler(Rc::new(handler));
195        self
196    }
197
198    /// Lists the directory again: the spinner shows and the source is
199    /// asked for the first page. Call it after the user signed in, or let
200    /// the "Retry" button call it.
201    pub fn retry(&mut self, cx: &mut Context<Self>) {
202        self.browser.retry(cx);
203    }
204
205    /// Why the directory could not be listed, when it could not.
206    pub fn failure(&self) -> Option<&crate::ListError> {
207        self.browser.failure()
208    }
209
210    /// Offers a "New folder" button, which opens a row at the top of the
211    /// list to name a folder in the directory that is listed. It is off
212    /// by default, and shows only when the source
213    /// [can make folders](FolderSource::can_create_folders). The new folder
214    /// becomes the one "Use folder" chooses.
215    pub fn allow_new_folder(mut self, allow: bool) -> Self {
216        self.browser.set_allow_new_folder_seed(allow);
217        self
218    }
219
220    /// Whether the picker is set to offer a "New folder" button.
221    pub fn allows_new_folder(&self) -> bool {
222        self.browser.allows_new_folder()
223    }
224
225    /// Offers or withdraws the "New folder" button.
226    pub fn set_allow_new_folder(&mut self, allow: bool, cx: &mut Context<Self>) {
227        self.browser.set_allow_new_folder(allow, cx);
228    }
229
230    /// Whether the button shows: it is on and the source can make folders.
231    pub fn can_create_folder(&self) -> bool {
232        self.browser.can_create_folder()
233    }
234
235    /// Whether the row that names a new folder is open.
236    pub fn is_naming_folder(&self) -> bool {
237        self.browser.new_folder.is_some()
238    }
239
240    /// The name typed in the row that names a new folder, or `None` while
241    /// the row is closed.
242    pub fn new_folder_name(&self, cx: &App) -> Option<SharedString> {
243        self.browser
244            .new_folder
245            .as_ref()
246            .map(|row| row.input.read(cx).value())
247    }
248
249    /// Opens the row that names a new folder, with "Untitled folder"
250    /// selected. Nothing is made until the name is confirmed with Enter.
251    /// It does nothing while the directory loads, or when the source
252    /// cannot make folders.
253    pub fn begin_new_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
254        self.browser.begin_default_new_folder(window, cx);
255    }
256
257    /// Makes the folder `name` in the directory that is listed, the way
258    /// the row does: the row opens with `name`, or takes it when it is
259    /// already open, the name is checked, the source is asked, and the
260    /// result shows in the row. The picker reports
261    /// [`FolderPickerEvent::FolderCreated`] when the folder exists.
262    ///
263    /// Fails with [`CreateFolderError::NotReady`] while the directory
264    /// loads or a folder is being made, [`CreateFolderError::Unsupported`]
265    /// when new folders are off or the source cannot make them, and
266    /// [`CreateFolderError::InvalidName`] or
267    /// [`CreateFolderError::Exists`] for a name that is refused before the
268    /// source is asked.
269    pub fn create_folder(
270        &mut self,
271        name: &str,
272        window: &mut Window,
273        cx: &mut Context<Self>,
274    ) -> Result<(), CreateFolderError> {
275        self.browser.create_folder(name, window, cx)
276    }
277
278    /// The path field's state.
279    pub fn input(&self) -> &Entity<gpui_kit::base::input::InputState> {
280        &self.browser.input
281    }
282
283    /// The directory that is listed.
284    pub fn dir(&self) -> &SourcePath {
285        &self.browser.dir
286    }
287
288    /// The query: the text after the last separator.
289    pub fn query(&self) -> &str {
290        &self.browser.query
291    }
292
293    /// The path text, as the field shows it.
294    pub fn text(&self, cx: &App) -> SharedString {
295        self.browser.input.read(cx).value()
296    }
297
298    /// What is known about the directory that is listed.
299    pub fn listing(&self) -> Option<&Listing> {
300        self.browser.listing()
301    }
302
303    /// How many folders the list shows, not counting the "Load more"
304    /// row.
305    pub fn row_count(&self) -> usize {
306        self.browser.row_count()
307    }
308
309    /// The name of the folder the keyboard is on.
310    pub fn highlighted(&self) -> Option<SharedString> {
311        self.browser
312            .entry_at(self.browser.highlighted?)
313            .map(|(entry, _)| entry.name().clone())
314    }
315
316    /// The folder "Use folder" would choose: the listed directory while
317    /// the query is empty, or the folder the query names exactly. `None`
318    /// while the directory is loading or failed, or the query names no
319    /// folder.
320    pub fn selected(&self) -> Option<SourcePath> {
321        let loaded = self.browser.loaded()?;
322        let dir = self.browser.dir.clone();
323        if self.browser.query.is_empty() {
324            Some(dir)
325        } else {
326            let entry = loaded.entry_named(&self.browser.query)?;
327            Some(dir.join(entry.name()))
328        }
329    }
330
331    /// Chooses [`selected`](Self::selected), if there is one.
332    pub fn choose(&mut self, cx: &mut Context<Self>) {
333        if let Some(path) = self.selected() {
334            self.browser.stop_requests(cx);
335            cx.emit(FolderPickerEvent::Chosen(path));
336        }
337    }
338
339    /// Cancels the picker and every request to the source that is still
340    /// running: their answers never arrive. A directory that was still
341    /// loading is listed again when the picker shows.
342    pub fn cancel(&mut self, cx: &mut Context<Self>) {
343        self.browser.stop_requests(cx);
344        cx.emit(FolderPickerEvent::Cancelled);
345    }
346
347    /// Goes to the parent of the listed directory. The root is its own
348    /// parent.
349    pub fn go_up(&mut self, window: &mut Window, cx: &mut Context<Self>) {
350        self.browser.go_up(window, cx);
351    }
352
353    /// Goes inside the folder `name` of the listed directory.
354    pub fn descend(&mut self, name: &str, window: &mut Window, cx: &mut Context<Self>) {
355        self.browser.descend(name, window, cx);
356    }
357
358    /// Asks the source for the next page of the listed directory, when it
359    /// has one and no request is running. After a failure it asks again.
360    pub fn load_more(&mut self, cx: &mut Context<Self>) {
361        self.browser.load_more(cx);
362    }
363
364    /// Enter: goes inside the highlighted folder, or loads more when the
365    /// highlight is on the "Load more" row.
366    fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
367        if let Some(row) = self.browser.highlighted {
368            self.enter_row(row, window, cx);
369        }
370    }
371
372    /// Goes inside the folder at `row`, or loads more for the row after
373    /// the last folder.
374    fn enter_row(&mut self, row: usize, window: &mut Window, cx: &mut Context<Self>) {
375        if row == self.browser.row_count() && self.browser.has_more_row() {
376            self.browser.load_more(cx);
377        } else if let Some(name) = self
378            .browser
379            .entry_at(row)
380            .map(|(entry, _)| entry.name().clone())
381        {
382            self.browser.descend(&name, window, cx);
383        }
384    }
385}
386
387type CloseHandler = Rc<dyn Fn(&mut Window, &mut App)>;
388type OpenChange = Rc<dyn Fn(bool, &mut Window, &mut App)>;
389
390/// A dialog to choose a folder on a [`FolderPickerState`], as in the
391/// reference app: an Up button and the path field, a row that chooses
392/// the folder shown, a bordered list of its folders, and Cancel and
393/// "Use folder" in the footer.
394///
395/// ```
396/// use gpui_cn::{FolderPicker, FolderPickerState};
397/// use gpui_kit::{Entity, IntoElement};
398///
399/// fn picker(state: &Entity<FolderPickerState>, open: bool) -> impl IntoElement {
400///     FolderPicker::new("source-folder", state)
401///         .open(open)
402///         .title("Choose a source folder")
403/// }
404/// ```
405///
406/// The application owns the open state: [`open`](Self::open) sets it, and
407/// it closes the picker when the state reports
408/// [`FolderPickerEvent::Chosen`] or [`FolderPickerEvent::Cancelled`].
409/// The id must be stable across frames.
410#[derive(IntoElement)]
411pub struct FolderPicker {
412    id: ElementId,
413    state: Entity<FolderPickerState>,
414    open: bool,
415    title: SharedString,
416    on_open_change: Option<OpenChange>,
417}
418
419impl FolderPicker {
420    /// A closed picker on `state`, with a stable id.
421    pub fn new(id: impl Into<ElementId>, state: &Entity<FolderPickerState>) -> Self {
422        Self {
423            id: id.into(),
424            state: state.clone(),
425            open: false,
426            title: DEFAULT_TITLE.into(),
427            on_open_change: None,
428        }
429    }
430
431    /// Shows or hides the picker.
432    pub fn open(mut self, open: bool) -> Self {
433        self.open = open;
434        self
435    }
436
437    /// The title. The default is "Choose a folder".
438    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
439        self.title = title.into();
440        self
441    }
442
443    /// Called when Escape, a press on the backdrop, the close button, or
444    /// Cancel asks to close the picker, after the state reports
445    /// [`FolderPickerEvent::Cancelled`].
446    pub fn on_open_change(
447        mut self,
448        handler: impl Fn(bool, &mut Window, &mut App) + 'static,
449    ) -> Self {
450        self.on_open_change = Some(Rc::new(handler));
451        self
452    }
453}
454
455impl RenderOnce for FolderPicker {
456    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
457        let theme = cx.theme();
458        let look = MenuLook::of(theme, window.rem_size());
459        let list_height = theme.metrics.folder_list_height;
460        let pointer_cursors = Theme::global(cx).pointer_cursors;
461        let state = self.state;
462        let child = |name: &'static str| ElementId::NamedChild(self.id.clone().into(), name.into());
463        if self.open {
464            let state = state.clone();
465            window.defer(cx, move |_, cx| {
466                state.update(cx, |state, cx| state.browser.ensure_listed(cx));
467            });
468        }
469        let (failure, can_sign_in) = {
470            let browser = &state.read(cx).browser;
471            (browser.failure().cloned(), browser.auth_handler().is_some())
472        };
473        let (input, list, body_kind, selected, query_empty, has_entries) = {
474            let state = state.read(cx);
475            let has_entries = state
476                .browser
477                .loaded()
478                .is_some_and(|loaded| !loaded.entries().is_empty());
479            (
480                state.browser.input.clone(),
481                state.browser.list.clone(),
482                state.browser.body(),
483                state.selected(),
484                state.browser.query.is_empty(),
485                has_entries,
486            )
487        };
488        let focus = input.read(cx).focus_handle(cx);
489
490        let close: CloseHandler = {
491            let state = state.clone();
492            let handler = self.on_open_change;
493            Rc::new(move |window, cx| {
494                state.update(cx, |state, cx| state.cancel(cx));
495                if let Some(handler) = &handler {
496                    handler(false, window, cx);
497                }
498            })
499        };
500
501        let body = match body_kind {
502            Body::Loading => view::loading(child("loading"), &look),
503            Body::Failed => match failure {
504                Some(error) => view::failure::<FolderEntry, FolderPickerState>(
505                    &self.id,
506                    &look,
507                    &state,
508                    &error,
509                    can_sign_in,
510                ),
511                None => div().into_any_element(),
512            },
513            Body::Empty => view::message(child("message"), &look, view::EMPTY_FOLDERS),
514            Body::NoMatch => view::message(child("message"), &look, view::NO_MATCH),
515            Body::List => {
516                let rows = Rows {
517                    id: self.id.clone(),
518                    state: state.clone(),
519                    look: look.clone(),
520                    pointer_cursors,
521                };
522                let scrolled = state.clone();
523                ScrollArea::list(
524                    child("rows"),
525                    &list,
526                    look.row_height,
527                    move |row, window, cx| rows.render(row, window, cx),
528                )
529                .on_scroll(move |_, cx| {
530                    scrolled.update(cx, |state, cx| state.browser_mut().rows_scrolled(cx));
531                })
532                .size_full()
533                .into_any_element()
534            }
535        };
536
537        let up = {
538            let state = state.clone();
539            Button::new(child("up"))
540                .ghost()
541                .icon(Icon::from(IconName::ArrowUp))
542                .accessibility_label("Parent folder")
543                .on_click(move |_, window, cx| {
544                    state.update(cx, |state, cx| state.go_up(window, cx));
545                })
546        };
547        let current = (query_empty && has_entries).then(|| {
548            let state = state.clone();
549            h_flex()
550                .id(child("current"))
551                .test_support()
552                .role(Role::Button)
553                .size_full()
554                .px(look.row_padding)
555                .items_center()
556                .text_color(look.muted_foreground)
557                .when(pointer_cursors, |this| this.cursor_pointer())
558                .child("Select current folder")
559                .on_click(move |_, _, cx| state.update(cx, |state, cx| state.choose(cx)))
560                .into_any_element()
561        });
562        let top = {
563            let read = state.read(cx);
564            read.browser.new_folder.as_ref().map(|row| {
565                view::new_folder_row::<FolderEntry, FolderPickerState>(
566                    &self.id, &look, &state, row, cx,
567                )
568            })
569        };
570        let new_folder = state.read(cx).can_create_folder().then(|| {
571            let ready = state.read(cx).browser.new_folder_ready();
572            let state = state.clone();
573            Button::new(child("new-folder-button"))
574                .ghost()
575                .icon(Icon::from(IconName::Plus))
576                .label("New folder")
577                .disabled(!ready)
578                .on_click(move |_, window, cx| {
579                    state.update(cx, |state, cx| state.begin_new_folder(window, cx));
580                })
581        });
582        let (up_key, down_key, confirm_key) = (state.clone(), state.clone(), state.clone());
583        let content = view::stack()
584            .key_context(CONTEXT)
585            .on_action(move |_: &SelectUp, _, cx| {
586                up_key.update(cx, |state, cx| state.browser.move_highlight(-1, cx));
587            })
588            .on_action(move |_: &SelectDown, _, cx| {
589                down_key.update(cx, |state, cx| state.browser.move_highlight(1, cx));
590            })
591            .on_action({
592                let state = state.clone();
593                move |_: &Submit, _, cx| state.update(cx, |state, cx| state.choose(cx))
594            })
595            .on_action({
596                let state = state.clone();
597                move |_: &NewFolder, window, cx| {
598                    state.update(cx, |state, cx| state.begin_new_folder(window, cx));
599                }
600            })
601            .on_action(move |_: &Confirm, window, cx| {
602                confirm_key.update(cx, |state, cx| state.confirm(window, cx));
603            })
604            .child(
605                h_flex()
606                    .gap_2()
607                    .items_center()
608                    .child(up)
609                    .child(div().flex_1().child(crate::Input::new(&input))),
610            )
611            .child(view::slot(&look, current))
612            .child(view::list_box(child("list"), &look, list_height, top, body));
613
614        let footer = h_flex()
615            .w_full()
616            .justify_between()
617            .child(div().children(new_folder))
618            .child(
619                h_flex()
620                    .gap_2()
621                    .child({
622                        let close = close.clone();
623                        Button::new(child("cancel"))
624                            .ghost()
625                            .label("Cancel")
626                            .on_click(move |_, window, cx| close(window, cx))
627                    })
628                    .child({
629                        let state = state.clone();
630                        Button::new(child("use"))
631                            .primary()
632                            .label("Use folder")
633                            .disabled(selected.is_none())
634                            .on_click(move |_, _, cx| {
635                                state.update(cx, |state, cx| state.choose(cx))
636                            })
637                    }),
638            );
639
640        Dialog::new(self.id.clone())
641            .open(self.open)
642            .title(self.title)
643            .track_focus(&focus)
644            .on_open_change(move |_, window, cx| close(window, cx))
645            .child(content)
646            .footer(footer)
647    }
648}
649
650/// What a row is drawn from, owned by the list's row builder, which
651/// outlives the render that made it.
652struct Rows {
653    id: ElementId,
654    state: Entity<FolderPickerState>,
655    look: MenuLook,
656    pointer_cursors: bool,
657}
658
659impl Rows {
660    fn render(&self, row: usize, _: &mut Window, cx: &mut App) -> gpui_kit::AnyElement {
661        let look = &self.look;
662        let (name, ranges, highlighted) = {
663            let state = self.state.read(cx);
664            let browser = &state.browser;
665            if row == browser.row_count() && browser.has_more_row() {
666                return view::more_row::<FolderEntry, FolderPickerState>(
667                    &self.id,
668                    look,
669                    &self.state,
670                    view::MoreRow {
671                        state: browser.more_state(),
672                        error: browser.more_error().cloned(),
673                        can_sign_in: browser.auth_handler().is_some(),
674                    },
675                    browser.highlighted == Some(row),
676                    row,
677                    self.pointer_cursors,
678                );
679            }
680            let Some((entry, ranges)) = browser.entry_at(row) else {
681                return div().into_any_element();
682            };
683            (
684                entry.name().clone(),
685                ranges.to_vec(),
686                browser.highlighted == Some(row),
687            )
688        };
689        let label = crate::menu::matched_text(name.clone(), &ranges, look);
690        let state = self.state.clone();
691        let hover = self.state.clone();
692        view::row_frame(
693            ElementId::NamedChild(
694                ElementId::NamedChild(self.id.clone().into(), "entry".into()).into(),
695                name,
696            ),
697            look,
698            Role::ListBoxOption,
699            highlighted,
700            false,
701            self.pointer_cursors,
702        )
703        .child(
704            Icon::from(IconName::Folder)
705                .size_4()
706                .flex_shrink_0()
707                .text_color(look.muted_foreground),
708        )
709        .child(
710            div()
711                .flex_1()
712                .min_w_0()
713                .overflow_hidden()
714                .whitespace_nowrap()
715                .child(label),
716        )
717        .when(!look.touch, |this| {
718            this.on_mouse_move(move |_, _, cx| {
719                view::highlight_on_hover::<FolderEntry, FolderPickerState>(&hover, row, cx)
720            })
721        })
722        .on_click(move |_, window, cx| {
723            state.update(cx, |state, cx| state.enter_row(row, window, cx));
724        })
725        .into_any_element()
726    }
727}