settings_ui.rs

   1mod appearance_settings_controls;
   2
   3use std::{
   4    num::NonZeroU32,
   5    ops::{Not, Range},
   6    rc::Rc,
   7};
   8
   9use anyhow::Context as _;
  10use editor::{Editor, EditorSettingsControls};
  11use feature_flags::{FeatureFlag, FeatureFlagAppExt};
  12use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions};
  13use settings::{
  14    NumType, SettingsStore, SettingsUiEntry, SettingsUiEntryMetaData, SettingsUiItem,
  15    SettingsUiItemArray, SettingsUiItemDynamicMap, SettingsUiItemGroup, SettingsUiItemSingle,
  16    SettingsUiItemUnion, SettingsValue,
  17};
  18use smallvec::SmallVec;
  19use ui::{
  20    ContextMenu, DropdownMenu, NumericStepper, SwitchField, TableInteractionState,
  21    ToggleButtonGroup, ToggleButtonSimple, prelude::*,
  22};
  23use workspace::{
  24    Workspace,
  25    item::{Item, ItemEvent},
  26};
  27
  28use crate::appearance_settings_controls::AppearanceSettingsControls;
  29
  30pub struct SettingsUiFeatureFlag;
  31
  32impl FeatureFlag for SettingsUiFeatureFlag {
  33    const NAME: &'static str = "settings-ui";
  34}
  35
  36actions!(
  37    zed,
  38    [
  39        /// Opens settings UI.
  40        OpenSettingsUi
  41    ]
  42);
  43
  44pub fn open_settings_editor(
  45    workspace: &mut Workspace,
  46    _: &OpenSettingsUi,
  47    window: &mut Window,
  48    cx: &mut Context<Workspace>,
  49) {
  50    // todo(settings_ui) open in a local workspace if this is remote.
  51    let existing = workspace
  52        .active_pane()
  53        .read(cx)
  54        .items()
  55        .find_map(|item| item.downcast::<SettingsPage>());
  56
  57    if let Some(existing) = existing {
  58        workspace.activate_item(&existing, true, true, window, cx);
  59    } else {
  60        let settings_page = SettingsPage::new(workspace, cx);
  61        workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx)
  62    }
  63}
  64
  65pub fn init(cx: &mut App) {
  66    cx.observe_new(|workspace: &mut Workspace, _, _| {
  67        workspace.register_action_renderer(|div, _, _, cx| {
  68            let settings_ui_actions = [std::any::TypeId::of::<OpenSettingsUi>()];
  69            let has_flag = cx.has_flag::<SettingsUiFeatureFlag>();
  70            command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| {
  71                if has_flag {
  72                    filter.show_action_types(&settings_ui_actions);
  73                } else {
  74                    filter.hide_action_types(&settings_ui_actions);
  75                }
  76            });
  77            if has_flag {
  78                div.on_action(cx.listener(open_settings_editor))
  79            } else {
  80                div
  81            }
  82        });
  83    })
  84    .detach();
  85}
  86
  87pub struct SettingsPage {
  88    focus_handle: FocusHandle,
  89    settings_tree: SettingsUiTree,
  90}
  91
  92impl SettingsPage {
  93    pub fn new(_workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
  94        cx.new(|cx| Self {
  95            focus_handle: cx.focus_handle(),
  96            settings_tree: SettingsUiTree::new(cx),
  97        })
  98    }
  99}
 100
 101impl EventEmitter<ItemEvent> for SettingsPage {}
 102
 103impl Focusable for SettingsPage {
 104    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 105        self.focus_handle.clone()
 106    }
 107}
 108
 109impl Item for SettingsPage {
 110    type Event = ItemEvent;
 111
 112    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
 113        Some(Icon::new(IconName::Settings))
 114    }
 115
 116    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 117        "Settings".into()
 118    }
 119
 120    fn show_toolbar(&self) -> bool {
 121        false
 122    }
 123
 124    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
 125        f(*event)
 126    }
 127}
 128
 129// We want to iterate over the side bar with root groups
 130// - this is a loop over top level groups, and if any are expanded, recursively displaying their items
 131// - Should be able to get all items from a group (flatten a group)
 132// - Should be able to toggle/untoggle groups in UI (at least in sidebar)
 133// - Search should be available
 134//  - there should be an index of text -> item mappings, for using fuzzy::match
 135//   - Do we want to show the parent groups when a item is matched?
 136
 137struct UiEntry {
 138    title: SharedString,
 139    path: Option<SharedString>,
 140    documentation: Option<SharedString>,
 141    _depth: usize,
 142    // a
 143    //  b     < a descendant range < a total descendant range
 144    //    f   |                    |
 145    //    g   |                    |
 146    //  c     <                    |
 147    //    d                        |
 148    //    e                        <
 149    descendant_range: Range<usize>,
 150    total_descendant_range: Range<usize>,
 151    next_sibling: Option<usize>,
 152    // expanded: bool,
 153    render: Option<SettingsUiItemSingle>,
 154    dynamic_render: Option<SettingsUiItemUnion>,
 155    generate_items: Option<(
 156        SettingsUiItem,
 157        fn(&serde_json::Value, &App) -> Vec<SettingsUiEntryMetaData>,
 158        SmallVec<[SharedString; 1]>,
 159    )>,
 160    array: Option<SettingsUiItemArray>,
 161}
 162
 163impl UiEntry {
 164    fn first_descendant_index(&self) -> Option<usize> {
 165        return self
 166            .descendant_range
 167            .is_empty()
 168            .not()
 169            .then_some(self.descendant_range.start);
 170    }
 171
 172    fn nth_descendant_index(&self, tree: &[UiEntry], n: usize) -> Option<usize> {
 173        let first_descendant_index = self.first_descendant_index()?;
 174        let mut current_index = 0;
 175        let mut current_descendant_index = Some(first_descendant_index);
 176        while let Some(descendant_index) = current_descendant_index
 177            && current_index < n
 178        {
 179            current_index += 1;
 180            current_descendant_index = tree[descendant_index].next_sibling;
 181        }
 182        current_descendant_index
 183    }
 184}
 185
 186pub struct SettingsUiTree {
 187    root_entry_indices: Vec<usize>,
 188    entries: Vec<UiEntry>,
 189    active_entry_index: usize,
 190}
 191
 192fn build_tree_item(
 193    tree: &mut Vec<UiEntry>,
 194    entry: SettingsUiEntry,
 195    depth: usize,
 196    prev_index: Option<usize>,
 197) {
 198    // let tree: HashMap<Path, UiEntry>;
 199    let index = tree.len();
 200    tree.push(UiEntry {
 201        title: entry.title.into(),
 202        path: entry.path.map(SharedString::new_static),
 203        documentation: entry.documentation.map(SharedString::new_static),
 204        _depth: depth,
 205        descendant_range: index + 1..index + 1,
 206        total_descendant_range: index + 1..index + 1,
 207        render: None,
 208        next_sibling: None,
 209        dynamic_render: None,
 210        generate_items: None,
 211        array: None,
 212    });
 213    if let Some(prev_index) = prev_index {
 214        tree[prev_index].next_sibling = Some(index);
 215    }
 216    match entry.item {
 217        SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => {
 218            for group_item in group_items {
 219                let prev_index = tree[index]
 220                    .descendant_range
 221                    .is_empty()
 222                    .not()
 223                    .then_some(tree[index].descendant_range.end - 1);
 224                tree[index].descendant_range.end = tree.len() + 1;
 225                build_tree_item(tree, group_item, depth + 1, prev_index);
 226                tree[index].total_descendant_range.end = tree.len();
 227            }
 228        }
 229        SettingsUiItem::Single(item) => {
 230            tree[index].render = Some(item);
 231        }
 232        SettingsUiItem::Union(dynamic_render) => {
 233            // todo(settings_ui) take from item and store other fields instead of clone
 234            // will also require replacing usage in render_recursive so it can know
 235            // which options were actually rendered
 236            let options = dynamic_render.options.clone();
 237            tree[index].dynamic_render = Some(dynamic_render);
 238            for option in options {
 239                let Some(option) = option else { continue };
 240                let prev_index = tree[index]
 241                    .descendant_range
 242                    .is_empty()
 243                    .not()
 244                    .then_some(tree[index].descendant_range.end - 1);
 245                tree[index].descendant_range.end = tree.len() + 1;
 246                build_tree_item(tree, option, depth + 1, prev_index);
 247                tree[index].total_descendant_range.end = tree.len();
 248            }
 249        }
 250        SettingsUiItem::DynamicMap(SettingsUiItemDynamicMap {
 251            item: generate_settings_ui_item,
 252            determine_items,
 253            defaults_path,
 254        }) => {
 255            tree[index].generate_items = Some((
 256                generate_settings_ui_item(),
 257                determine_items,
 258                defaults_path
 259                    .into_iter()
 260                    .copied()
 261                    .map(SharedString::new_static)
 262                    .collect(),
 263            ));
 264        }
 265        SettingsUiItem::Array(array) => {
 266            tree[index].array = Some(array);
 267        }
 268        SettingsUiItem::None => {
 269            return;
 270        }
 271    }
 272}
 273
 274impl SettingsUiTree {
 275    pub fn new(cx: &App) -> Self {
 276        let settings_store = SettingsStore::global(cx);
 277        let mut tree = vec![];
 278        let mut root_entry_indices = vec![];
 279        for item in settings_store.settings_ui_items() {
 280            if matches!(item.item, SettingsUiItem::None)
 281            // todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to
 282            // link them to other groups
 283            || matches!(item.item, SettingsUiItem::Single(_))
 284            {
 285                continue;
 286            }
 287
 288            let prev_root_entry_index = root_entry_indices.last().copied();
 289            root_entry_indices.push(tree.len());
 290            build_tree_item(&mut tree, item, 0, prev_root_entry_index);
 291        }
 292
 293        root_entry_indices.sort_by_key(|i| &tree[*i].title);
 294
 295        let active_entry_index = root_entry_indices[0];
 296        Self {
 297            entries: tree,
 298            root_entry_indices,
 299            active_entry_index,
 300        }
 301    }
 302
 303    // todo(settings_ui): Make sure `Item::None` paths are added to the paths tree,
 304    // so that we can keep none/skip and still test in CI that all settings have
 305    #[cfg(feature = "test-support")]
 306    pub fn all_paths(&self, cx: &App) -> Vec<Vec<SharedString>> {
 307        fn all_paths_rec(
 308            tree: &[UiEntry],
 309            paths: &mut Vec<Vec<SharedString>>,
 310            current_path: &mut Vec<SharedString>,
 311            idx: usize,
 312            cx: &App,
 313        ) {
 314            let child = &tree[idx];
 315            let mut pushed_path = false;
 316            if let Some(path) = child.path.as_ref() {
 317                current_path.push(path.clone());
 318                paths.push(current_path.clone());
 319                pushed_path = true;
 320            }
 321            // todo(settings_ui): handle dynamic nodes here
 322            let selected_descendant_index = child
 323                .dynamic_render
 324                .as_ref()
 325                .map(|dynamic_render| {
 326                    read_settings_value_from_path(
 327                        SettingsStore::global(cx).raw_default_settings(),
 328                        &current_path,
 329                    )
 330                    .map(|value| (dynamic_render.determine_option)(value, cx))
 331                })
 332                .and_then(|selected_descendant_index| {
 333                    selected_descendant_index.map(|index| child.nth_descendant_index(tree, index))
 334                });
 335
 336            if let Some(selected_descendant_index) = selected_descendant_index {
 337                // just silently fail if we didn't find a setting value for the path
 338                if let Some(descendant_index) = selected_descendant_index {
 339                    all_paths_rec(tree, paths, current_path, descendant_index, cx);
 340                }
 341            } else if let Some(desc_idx) = child.first_descendant_index() {
 342                let mut desc_idx = Some(desc_idx);
 343                while let Some(descendant_index) = desc_idx {
 344                    all_paths_rec(&tree, paths, current_path, descendant_index, cx);
 345                    desc_idx = tree[descendant_index].next_sibling;
 346                }
 347            }
 348            if pushed_path {
 349                current_path.pop();
 350            }
 351        }
 352
 353        let mut paths = Vec::new();
 354        for &index in &self.root_entry_indices {
 355            all_paths_rec(&self.entries, &mut paths, &mut Vec::new(), index, cx);
 356        }
 357        paths
 358    }
 359}
 360
 361fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context<SettingsPage>) -> Div {
 362    let mut nav = v_flex().p_4().gap_2();
 363    for &index in &tree.root_entry_indices {
 364        nav = nav.child(
 365            div()
 366                .id(index)
 367                .on_click(cx.listener(move |settings, _, _, _| {
 368                    settings.settings_tree.active_entry_index = index;
 369                }))
 370                .child(
 371                    Label::new(tree.entries[index].title.clone())
 372                        .size(LabelSize::Large)
 373                        .when(tree.active_entry_index == index, |this| {
 374                            this.color(Color::Selected)
 375                        }),
 376                ),
 377        );
 378    }
 379    nav
 380}
 381
 382fn render_content(
 383    tree: &SettingsUiTree,
 384    window: &mut Window,
 385    cx: &mut Context<SettingsPage>,
 386) -> Div {
 387    let content = v_flex().size_full().gap_4();
 388
 389    let mut path = smallvec::smallvec![];
 390
 391    return render_recursive(
 392        &tree.entries,
 393        tree.active_entry_index,
 394        &mut path,
 395        content,
 396        None,
 397        true,
 398        window,
 399        cx,
 400    );
 401}
 402
 403fn render_recursive(
 404    tree: &[UiEntry],
 405    entry_index: usize,
 406    path: &mut SmallVec<[SharedString; 1]>,
 407    mut element: Div,
 408    parent_fallback_default_value: Option<&serde_json::Value>,
 409    render_next_title: bool,
 410    window: &mut Window,
 411    cx: &mut App,
 412) -> Div {
 413    let Some(entry) = tree.get(entry_index) else {
 414        return element
 415            .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error));
 416    };
 417
 418    if render_next_title {
 419        element = element.child(Label::new(entry.title.clone()).size(LabelSize::Large));
 420    }
 421
 422    // todo(settings_ui): subgroups?
 423    let mut pushed_path = false;
 424    let mut fallback_default_value = parent_fallback_default_value;
 425    if let Some(child_path) = entry.path.as_ref() {
 426        path.push(child_path.clone());
 427        if let Some(fallback_value) = parent_fallback_default_value.as_ref() {
 428            fallback_default_value =
 429                read_settings_value_from_path(fallback_value, std::slice::from_ref(&child_path));
 430        }
 431        pushed_path = true;
 432    }
 433    let settings_value = settings_value_from_settings_and_path(
 434        path.clone(),
 435        fallback_default_value,
 436        entry.title.clone(),
 437        entry.documentation.clone(),
 438        // PERF: how to structure this better? There feels like there's a way to avoid the clone
 439        // and every value lookup
 440        SettingsStore::global(cx).raw_user_settings(),
 441        SettingsStore::global(cx).raw_default_settings(),
 442    );
 443    if let Some(dynamic_render) = entry.dynamic_render.as_ref() {
 444        let value = settings_value.read();
 445        let selected_index = (dynamic_render.determine_option)(value, cx);
 446        element = element.child(div().child(render_toggle_button_group_inner(
 447            settings_value.title.clone(),
 448            dynamic_render.labels,
 449            Some(selected_index),
 450            {
 451                let path = settings_value.path.clone();
 452                let defaults = dynamic_render.defaults.clone();
 453                move |idx, cx| {
 454                    if idx == selected_index {
 455                        return;
 456                    }
 457                    let default = defaults.get(idx).cloned().unwrap_or_default();
 458                    SettingsValue::write_value(&path, default, cx);
 459                }
 460            },
 461        )));
 462        // we don't add descendants for unit options, so we adjust the selected index
 463        // by the number of options we didn't add descendants for, to get the descendant index
 464        let selected_descendant_index = selected_index
 465            - dynamic_render.options[..selected_index]
 466                .iter()
 467                .filter(|option| option.is_none())
 468                .count();
 469        if dynamic_render.options[selected_index].is_some()
 470            && let Some(descendant_index) =
 471                entry.nth_descendant_index(tree, selected_descendant_index)
 472        {
 473            element = render_recursive(
 474                tree,
 475                descendant_index,
 476                path,
 477                element,
 478                fallback_default_value,
 479                false,
 480                window,
 481                cx,
 482            );
 483        }
 484    } else if let Some(array) = entry.array.as_ref() {
 485        let generated_items = (array.determine_items)(settings_value.read(), cx);
 486        let mut ui_items = Vec::with_capacity(generated_items.len());
 487        let settings_ui_item = (array.item)();
 488
 489        let table_interaction_state =
 490            window.use_keyed_state(("settings_ui_table", entry_index), cx, |_, cx| {
 491                TableInteractionState::new(cx)
 492            });
 493
 494        let mut table = ui::Table::<2>::new()
 495            .column_widths([relative(0.1), relative(0.9)])
 496            .header(["#", "Value"])
 497            .interactable(&table_interaction_state)
 498            .striped();
 499        let mut row_count = 0;
 500
 501        // todo(settings_ui): Try to make the static item on these items built into the tree
 502        // because we already propagate the path down so they don't all have to be recreated
 503        for (index, item) in generated_items.iter().enumerate() {
 504            let settings_ui_entry = SettingsUiEntry {
 505                path: None,
 506                title: "",
 507                documentation: None,
 508                item: settings_ui_item.clone(),
 509            };
 510            let prev_index = if ui_items.is_empty() {
 511                None
 512            } else {
 513                Some(ui_items.len() - 1)
 514            };
 515            let item_index = ui_items.len();
 516            build_tree_item(
 517                &mut ui_items,
 518                settings_ui_entry,
 519                entry._depth + 1,
 520                prev_index,
 521            );
 522            if item_index < ui_items.len() {
 523                ui_items[item_index].path = None;
 524                ui_items[item_index].title = item.title.clone();
 525                ui_items[item_index].documentation = item.documentation.clone();
 526
 527                // push path instead of setting path on ui item so that the path isn't
 528                // pushed to default_default_value as well when we recurse
 529                path.push(item.path.clone());
 530                dbg!(path.join("."));
 531                let row_element = render_recursive(
 532                    &ui_items,
 533                    item_index,
 534                    path,
 535                    div(),
 536                    Some(&array.default_item),
 537                    false,
 538                    window,
 539                    cx,
 540                );
 541                path.pop();
 542                table = table.row([index.to_string().into_any_element(), row_element.into_any()]);
 543                row_count += 1;
 544            }
 545        }
 546        table = table.row([
 547            row_count.to_string().into_any_element(),
 548            "create".into_any(),
 549        ]);
 550        element = element.child(div().child(table).debug());
 551    } else if let Some((settings_ui_item, generate_items, defaults_path)) =
 552        entry.generate_items.as_ref()
 553    {
 554        let generated_items = generate_items(settings_value.read(), cx);
 555        let mut ui_items = Vec::with_capacity(generated_items.len());
 556        let default_value = read_settings_value_from_path(
 557            SettingsStore::global(cx).raw_default_settings(),
 558            &defaults_path,
 559        )
 560        .cloned();
 561        for item in generated_items {
 562            let settings_ui_entry = SettingsUiEntry {
 563                path: None,
 564                title: "",
 565                documentation: None,
 566                item: settings_ui_item.clone(),
 567            };
 568            let prev_index = if ui_items.is_empty() {
 569                None
 570            } else {
 571                Some(ui_items.len() - 1)
 572            };
 573            let item_index = ui_items.len();
 574            build_tree_item(
 575                &mut ui_items,
 576                settings_ui_entry,
 577                entry._depth + 1,
 578                prev_index,
 579            );
 580            if item_index < ui_items.len() {
 581                ui_items[item_index].path = None;
 582                ui_items[item_index].title = item.title.clone();
 583                ui_items[item_index].documentation = item.documentation.clone();
 584
 585                // push path instead of setting path on ui item so that the path isn't pushed to default_path as well
 586                // when we recurse
 587                path.push(item.path.clone());
 588                element = render_recursive(
 589                    &ui_items,
 590                    item_index,
 591                    path,
 592                    element,
 593                    default_value.as_ref(),
 594                    true,
 595                    window,
 596                    cx,
 597                );
 598                path.pop();
 599            }
 600        }
 601    } else if let Some(child_render) = entry.render.as_ref() {
 602        element = element.child(div().child(render_item_single(
 603            settings_value,
 604            child_render,
 605            window,
 606            cx,
 607        )));
 608    } else if let Some(child_index) = entry.first_descendant_index() {
 609        let mut index = Some(child_index);
 610        while let Some(sub_child_index) = index {
 611            element = render_recursive(
 612                tree,
 613                sub_child_index,
 614                path,
 615                element,
 616                fallback_default_value,
 617                true,
 618                window,
 619                cx,
 620            );
 621            index = tree[sub_child_index].next_sibling;
 622        }
 623    } else {
 624        element = element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted)))
 625    }
 626
 627    if pushed_path {
 628        path.pop();
 629    }
 630    return element;
 631}
 632
 633impl Render for SettingsPage {
 634    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 635        let scroll_handle = window.use_state(cx, |_, _| ScrollHandle::new());
 636        div()
 637            .grid()
 638            .grid_cols(16)
 639            .p_4()
 640            .bg(cx.theme().colors().editor_background)
 641            .size_full()
 642            .child(
 643                div()
 644                    .id("settings-ui-nav")
 645                    .col_span(2)
 646                    .h_full()
 647                    .child(render_nav(&self.settings_tree, window, cx)),
 648            )
 649            .child(
 650                div().col_span(6).h_full().child(
 651                    render_content(&self.settings_tree, window, cx)
 652                        .id("settings-ui-content")
 653                        .track_scroll(scroll_handle.read(cx))
 654                        .overflow_y_scroll(),
 655                ),
 656            )
 657    }
 658}
 659
 660// todo(settings_ui): remove, only here as inspiration
 661#[allow(dead_code)]
 662fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement {
 663    v_flex()
 664        .p_4()
 665        .size_full()
 666        .gap_4()
 667        .child(Label::new("Settings").size(LabelSize::Large))
 668        .child(
 669            v_flex().gap_1().child(Label::new("Appearance")).child(
 670                v_flex()
 671                    .elevation_2(cx)
 672                    .child(AppearanceSettingsControls::new()),
 673            ),
 674        )
 675        .child(
 676            v_flex().gap_1().child(Label::new("Editor")).child(
 677                v_flex()
 678                    .elevation_2(cx)
 679                    .child(EditorSettingsControls::new()),
 680            ),
 681        )
 682}
 683
 684fn element_id_from_path(path: &[SharedString]) -> ElementId {
 685    if path.len() == 0 {
 686        panic!("Path length must not be zero");
 687    } else if path.len() == 1 {
 688        ElementId::Name(path[0].clone())
 689    } else {
 690        ElementId::from((
 691            ElementId::from(path[path.len() - 2].clone()),
 692            path[path.len() - 1].clone(),
 693        ))
 694    }
 695}
 696
 697fn render_item_single(
 698    settings_value: SettingsValue<serde_json::Value>,
 699    item: &SettingsUiItemSingle,
 700    window: &mut Window,
 701    cx: &mut App,
 702) -> AnyElement {
 703    match item {
 704        SettingsUiItemSingle::Custom(_) => div()
 705            .child(format!("Item: {}", settings_value.path.join(".")))
 706            .into_any_element(),
 707        SettingsUiItemSingle::SwitchField => {
 708            render_any_item(settings_value, render_switch_field, window, cx)
 709        }
 710        SettingsUiItemSingle::NumericStepper(num_type) => {
 711            render_any_numeric_stepper(settings_value, *num_type, window, cx)
 712        }
 713        SettingsUiItemSingle::ToggleGroup {
 714            variants: values,
 715            labels: titles,
 716        } => render_toggle_button_group(settings_value, values, titles, window, cx),
 717        SettingsUiItemSingle::DropDown { variants, labels } => {
 718            render_dropdown(settings_value, variants, labels, window, cx)
 719        }
 720        SettingsUiItemSingle::TextField => render_text_field(settings_value, window, cx),
 721    }
 722}
 723
 724pub fn read_settings_value_from_path<'a>(
 725    settings_contents: &'a serde_json::Value,
 726    path: &[impl AsRef<str>],
 727) -> Option<&'a serde_json::Value> {
 728    // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested
 729    let Some((key, remaining)) = path.split_first() else {
 730        return Some(settings_contents);
 731    };
 732    let Some(value) = settings_contents.get(key.as_ref()) else {
 733        return None;
 734    };
 735
 736    read_settings_value_from_path(value, remaining)
 737}
 738
 739fn downcast_any_item<T: serde::de::DeserializeOwned>(
 740    settings_value: SettingsValue<serde_json::Value>,
 741) -> SettingsValue<T> {
 742    let value = settings_value.value.map(|value| {
 743        serde_json::from_value::<T>(value.clone())
 744            .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
 745            .with_context(|| format!("value is not a {}: {}", std::any::type_name::<T>(), value))
 746            .unwrap()
 747    });
 748    // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values
 749    let default_value = serde_json::from_value::<T>(settings_value.default_value)
 750        .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
 751        .with_context(|| format!("value is not a {}", std::any::type_name::<T>()))
 752        .unwrap();
 753    let deserialized_setting_value = SettingsValue {
 754        title: settings_value.title,
 755        path: settings_value.path,
 756        documentation: settings_value.documentation,
 757        value,
 758        default_value,
 759    };
 760    deserialized_setting_value
 761}
 762
 763fn render_any_item<T: serde::de::DeserializeOwned>(
 764    settings_value: SettingsValue<serde_json::Value>,
 765    render_fn: impl Fn(SettingsValue<T>, &mut Window, &mut App) -> AnyElement + 'static,
 766    window: &mut Window,
 767    cx: &mut App,
 768) -> AnyElement {
 769    let deserialized_setting_value = downcast_any_item(settings_value);
 770    render_fn(deserialized_setting_value, window, cx)
 771}
 772
 773fn render_any_numeric_stepper(
 774    settings_value: SettingsValue<serde_json::Value>,
 775    num_type: NumType,
 776    window: &mut Window,
 777    cx: &mut App,
 778) -> AnyElement {
 779    match num_type {
 780        NumType::U64 => render_numeric_stepper::<u64>(
 781            downcast_any_item(settings_value),
 782            |n| u64::saturating_sub(n, 1),
 783            |n| u64::saturating_add(n, 1),
 784            |n| {
 785                serde_json::Number::try_from(n)
 786                    .context("Failed to convert u64 to serde_json::Number")
 787            },
 788            window,
 789            cx,
 790        ),
 791        NumType::U32 => render_numeric_stepper::<u32>(
 792            downcast_any_item(settings_value),
 793            |n| u32::saturating_sub(n, 1),
 794            |n| u32::saturating_add(n, 1),
 795            |n| {
 796                serde_json::Number::try_from(n)
 797                    .context("Failed to convert u32 to serde_json::Number")
 798            },
 799            window,
 800            cx,
 801        ),
 802        NumType::F32 => render_numeric_stepper::<f32>(
 803            downcast_any_item(settings_value),
 804            |a| a - 1.0,
 805            |a| a + 1.0,
 806            |n| {
 807                serde_json::Number::from_f64(n as f64)
 808                    .context("Failed to convert f32 to serde_json::Number")
 809            },
 810            window,
 811            cx,
 812        ),
 813        NumType::USIZE => render_numeric_stepper::<usize>(
 814            downcast_any_item(settings_value),
 815            |n| usize::saturating_sub(n, 1),
 816            |n| usize::saturating_add(n, 1),
 817            |n| {
 818                serde_json::Number::try_from(n)
 819                    .context("Failed to convert usize to serde_json::Number")
 820            },
 821            window,
 822            cx,
 823        ),
 824        NumType::U32NONZERO => render_numeric_stepper::<NonZeroU32>(
 825            downcast_any_item(settings_value),
 826            |a| NonZeroU32::new(u32::saturating_sub(a.get(), 1)).unwrap_or(NonZeroU32::MIN),
 827            |a| NonZeroU32::new(u32::saturating_add(a.get(), 1)).unwrap_or(NonZeroU32::MAX),
 828            |n| {
 829                serde_json::Number::try_from(n.get())
 830                    .context("Failed to convert usize to serde_json::Number")
 831            },
 832            window,
 833            cx,
 834        ),
 835    }
 836}
 837
 838fn render_numeric_stepper<T: serde::de::DeserializeOwned + std::fmt::Display + Copy + 'static>(
 839    value: SettingsValue<T>,
 840    saturating_sub_1: fn(T) -> T,
 841    saturating_add_1: fn(T) -> T,
 842    to_serde_number: fn(T) -> anyhow::Result<serde_json::Number>,
 843    _window: &mut Window,
 844    _cx: &mut App,
 845) -> AnyElement {
 846    let id = element_id_from_path(&value.path);
 847    let path = value.path.clone();
 848    let num = *value.read();
 849
 850    NumericStepper::new(
 851        id,
 852        num.to_string(),
 853        {
 854            let path = value.path;
 855            move |_, _, cx| {
 856                let Some(number) = to_serde_number(saturating_sub_1(num)).ok() else {
 857                    return;
 858                };
 859                let new_value = serde_json::Value::Number(number);
 860                SettingsValue::write_value(&path, new_value, cx);
 861            }
 862        },
 863        move |_, _, cx| {
 864            let Some(number) = to_serde_number(saturating_add_1(num)).ok() else {
 865                return;
 866            };
 867
 868            let new_value = serde_json::Value::Number(number);
 869
 870            SettingsValue::write_value(&path, new_value, cx);
 871        },
 872    )
 873    .style(ui::NumericStepperStyle::Outlined)
 874    .into_any_element()
 875}
 876
 877fn render_switch_field(
 878    value: SettingsValue<bool>,
 879    _window: &mut Window,
 880    _cx: &mut App,
 881) -> AnyElement {
 882    let id = element_id_from_path(&value.path);
 883    let path = value.path.clone();
 884    SwitchField::new(
 885        id,
 886        value.title.clone(),
 887        value.documentation.clone(),
 888        match value.read() {
 889            true => ToggleState::Selected,
 890            false => ToggleState::Unselected,
 891        },
 892        move |toggle_state, _, cx| {
 893            let new_value = serde_json::Value::Bool(match toggle_state {
 894                ToggleState::Indeterminate => {
 895                    return;
 896                }
 897                ToggleState::Selected => true,
 898                ToggleState::Unselected => false,
 899            });
 900
 901            SettingsValue::write_value(&path, new_value, cx);
 902        },
 903    )
 904    .into_any_element()
 905}
 906
 907fn render_text_field(
 908    value: SettingsValue<serde_json::Value>,
 909    window: &mut Window,
 910    cx: &mut App,
 911) -> AnyElement {
 912    let value = downcast_any_item::<String>(value);
 913    let path = value.path.clone();
 914    let current_text = value.read().clone();
 915
 916    let dirty = window.use_keyed_state((element_id_from_path(&path), "dirty"), cx, |_, _| false);
 917
 918    let editor = window.use_keyed_state((element_id_from_path(&path), "editor"), cx, {
 919        let path = path.clone();
 920        let dirty = dirty.clone();
 921        move |window, cx| {
 922            let mut editor = Editor::single_line(window, cx);
 923            // editor.set_text(current_text, window, cx);
 924
 925            let dirty = dirty.downgrade();
 926            cx.subscribe_self(move |_, event: &editor::EditorEvent, cx| match event {
 927                editor::EditorEvent::BufferEdited => {
 928                    let Some(dirty) = dirty.upgrade() else { return };
 929                    dirty.write(cx, true);
 930                }
 931                _ => {}
 932            })
 933            .detach();
 934            // cx.observe_global_in::<SettingsStore>(window, move |editor, window, cx| {
 935            //     let user_settings = SettingsStore::global(cx).raw_user_settings();
 936            //     if let Some(value) = read_settings_value_from_path(&user_settings, &path)
 937            //         .and_then(serde_json::Value::as_str)
 938            //         .map(str::to_string)
 939            //     {
 940            //         editor.set_text(value, window, cx);
 941            //     }
 942            //     // else {
 943            //     //     editor.clear(window, cx);
 944            //     // }
 945            // })
 946            // .detach();
 947
 948            editor
 949        }
 950    });
 951
 952    // todo! WAAY to slow
 953    editor.update(cx, |editor, cx| {
 954        if &editor.text(cx) != &current_text && !*dirty.read(cx) {
 955            editor.set_text(current_text, window, cx);
 956        }
 957    });
 958
 959    let weak_editor = editor.downgrade();
 960    let theme_colors = cx.theme().colors();
 961
 962    div()
 963        .child(editor)
 964        .bg(theme_colors.editor_background)
 965        .border_1()
 966        .rounded_lg()
 967        .border_color(theme_colors.border)
 968        .on_action::<menu::Confirm>({
 969            move |_, _, cx| {
 970                let new_value = weak_editor.read_with(cx, |editor, cx| editor.text(cx)).ok();
 971
 972                if let Some(new_value) = new_value {
 973                    SettingsValue::write_value(&path, serde_json::Value::String(new_value), cx);
 974                }
 975            }
 976        })
 977        .into_any_element()
 978}
 979
 980fn render_toggle_button_group(
 981    value: SettingsValue<serde_json::Value>,
 982    variants: &'static [&'static str],
 983    labels: &'static [&'static str],
 984    _: &mut Window,
 985    _: &mut App,
 986) -> AnyElement {
 987    let value = downcast_any_item::<String>(value);
 988    let active_value = value.read();
 989    let selected_idx = variants.iter().position(|v| v == &active_value);
 990
 991    return render_toggle_button_group_inner(value.title, labels, selected_idx, {
 992        let path = value.path.clone();
 993        move |variant_index, cx| {
 994            SettingsValue::write_value(
 995                &path,
 996                serde_json::Value::String(variants[variant_index].to_string()),
 997                cx,
 998            );
 999        }
1000    });
1001}
1002
1003fn render_dropdown(
1004    value: SettingsValue<serde_json::Value>,
1005    variants: &'static [&'static str],
1006    labels: &'static [&'static str],
1007    window: &mut Window,
1008    cx: &mut App,
1009) -> AnyElement {
1010    let value = downcast_any_item::<String>(value);
1011    let id = element_id_from_path(&value.path);
1012
1013    let menu = window.use_keyed_state(id.clone(), cx, |window, cx| {
1014        let path = value.path.clone();
1015        let handler = Rc::new(move |variant: &'static str, cx: &mut App| {
1016            SettingsValue::write_value(&path, serde_json::Value::String(variant.to_string()), cx);
1017        });
1018
1019        ContextMenu::build(window, cx, |mut menu, _, _| {
1020            for (label, variant) in labels.iter().zip(variants) {
1021                menu = menu.entry(*label, None, {
1022                    let handler = handler.clone();
1023                    move |_, cx| {
1024                        handler(variant, cx);
1025                    }
1026                });
1027            }
1028
1029            menu
1030        })
1031    });
1032
1033    DropdownMenu::new(id, value.read(), menu.read(cx).clone())
1034        .style(ui::DropdownStyle::Outlined)
1035        .into_any_element()
1036}
1037
1038fn render_toggle_button_group_inner(
1039    title: SharedString,
1040    labels: &'static [&'static str],
1041    selected_idx: Option<usize>,
1042    on_write: impl Fn(usize, &mut App) + 'static,
1043) -> AnyElement {
1044    fn make_toggle_group<const LEN: usize>(
1045        title: SharedString,
1046        selected_idx: Option<usize>,
1047        on_write: Rc<dyn Fn(usize, &mut App)>,
1048        labels: &'static [&'static str],
1049    ) -> AnyElement {
1050        let labels_array: [&'static str; LEN] = {
1051            let mut arr = ["unused"; LEN];
1052            arr.copy_from_slice(labels);
1053            arr
1054        };
1055
1056        let mut idx = 0;
1057        ToggleButtonGroup::single_row(
1058            title,
1059            labels_array.map(|label| {
1060                idx += 1;
1061                let on_write = on_write.clone();
1062                ToggleButtonSimple::new(label, move |_, _, cx| {
1063                    on_write(idx - 1, cx);
1064                })
1065            }),
1066        )
1067        .when_some(selected_idx, |this, ix| this.selected_index(ix))
1068        .style(ui::ToggleButtonGroupStyle::Filled)
1069        .into_any_element()
1070    }
1071
1072    let on_write = Rc::new(on_write);
1073
1074    macro_rules! templ_toggl_with_const_param {
1075        ($len:expr) => {
1076            if labels.len() == $len {
1077                return make_toggle_group::<$len>(title.clone(), selected_idx, on_write, labels);
1078            }
1079        };
1080    }
1081    templ_toggl_with_const_param!(1);
1082    templ_toggl_with_const_param!(2);
1083    templ_toggl_with_const_param!(3);
1084    templ_toggl_with_const_param!(4);
1085    templ_toggl_with_const_param!(5);
1086    templ_toggl_with_const_param!(6);
1087    unreachable!("Too many variants");
1088}
1089
1090fn settings_value_from_settings_and_path(
1091    path: SmallVec<[SharedString; 1]>,
1092    fallback_value: Option<&serde_json::Value>,
1093    title: SharedString,
1094    documentation: Option<SharedString>,
1095    user_settings: &serde_json::Value,
1096    default_settings: &serde_json::Value,
1097) -> SettingsValue<serde_json::Value> {
1098    let default_value = read_settings_value_from_path(default_settings, &path)
1099        .or_else(|| fallback_value)
1100        .with_context(|| format!("No default value for item at path {:?}", path.join(".")))
1101        .expect("Default value set for item")
1102        .clone();
1103
1104    let value = read_settings_value_from_path(user_settings, &path).cloned();
1105    let settings_value = SettingsValue {
1106        default_value,
1107        value,
1108        documentation,
1109        path,
1110        // todo(settings_ui) is title required inside SettingsValue?
1111        title,
1112    };
1113    return settings_value;
1114}