Repository: longbridge/gpui-component Branch: main Commit: 311c7e0d104d Files: 547 Total size: 4.2 MB Directory structure: gitextract_xlkzxety/ ├── .cargo/ │ └── config.toml ├── .claude/ │ ├── COMPONENT_TEST_RULES.md │ └── skills/ │ ├── generate-component-documentation/ │ │ └── SKILL.md │ ├── generate-component-story/ │ │ └── SKILL.md │ ├── github-pull-request-description/ │ │ └── SKILL.md │ ├── gpui-action/ │ │ └── SKILL.md │ ├── gpui-async/ │ │ └── SKILL.md │ ├── gpui-context/ │ │ └── SKILL.md │ ├── gpui-element/ │ │ ├── SKILL.md │ │ └── references/ │ │ ├── advanced-patterns.md │ │ ├── api-reference.md │ │ ├── best-practices.md │ │ ├── examples.md │ │ └── patterns.md │ ├── gpui-entity/ │ │ ├── SKILL.md │ │ └── references/ │ │ ├── advanced.md │ │ ├── api-reference.md │ │ ├── best-practices.md │ │ └── patterns.md │ ├── gpui-event/ │ │ └── SKILL.md │ ├── gpui-focus-handle/ │ │ └── SKILL.md │ ├── gpui-global/ │ │ └── SKILL.md │ ├── gpui-layout-and-style/ │ │ └── SKILL.md │ ├── gpui-style-guide/ │ │ └── SKILL.md │ ├── gpui-test/ │ │ ├── SKILL.md │ │ ├── examples.md │ │ └── reference.md │ └── new-component/ │ └── SKILL.md ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── 01_bug.md │ ├── dependabot.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── ci.yml │ ├── release-docs.yml │ ├── release.yml │ └── test-docs.yml ├── .gitignore ├── .rustfmt.toml ├── .theme-schema.json ├── CLAUDE.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── Makefile ├── README.md ├── crates/ │ ├── assets/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── assets/ │ │ │ └── .gitkeep │ │ └── src/ │ │ ├── lib.rs │ │ ├── native_assets.rs │ │ └── wasm_assets.rs │ ├── macros/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── derive_into_plot.rs │ │ └── lib.rs │ ├── story/ │ │ ├── Cargo.toml │ │ ├── examples/ │ │ │ ├── brush.rs │ │ │ ├── dock.rs │ │ │ ├── editor.rs │ │ │ ├── fixtures/ │ │ │ │ ├── completion_items.json │ │ │ │ ├── test.astro │ │ │ │ ├── test.c │ │ │ │ ├── test.go │ │ │ │ ├── test.html │ │ │ │ ├── test.js │ │ │ │ ├── test.json │ │ │ │ ├── test.kt │ │ │ │ ├── test.lua │ │ │ │ ├── test.md │ │ │ │ ├── test.nv │ │ │ │ ├── test.php │ │ │ │ ├── test.py │ │ │ │ ├── test.rb │ │ │ │ ├── test.rs │ │ │ │ ├── test.sql │ │ │ │ ├── test.svelte │ │ │ │ ├── test.ts │ │ │ │ └── test.zig │ │ │ ├── html.rs │ │ │ ├── large-text.rs │ │ │ ├── markdown.rs │ │ │ ├── stream_markdown.rs │ │ │ └── tiles.rs │ │ └── src/ │ │ ├── app_menus.rs │ │ ├── embedded_themes.rs │ │ ├── fixtures/ │ │ │ ├── counters.json │ │ │ ├── countries.json │ │ │ ├── daily-devices.json │ │ │ ├── monthly-devices.json │ │ │ └── stock-prices.json │ │ ├── gallery.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── stories/ │ │ │ ├── accordion_story.rs │ │ │ ├── alert_dialog_story.rs │ │ │ ├── alert_story.rs │ │ │ ├── avatar_story.rs │ │ │ ├── badge_story.rs │ │ │ ├── breadcrumb_story.rs │ │ │ ├── button_story.rs │ │ │ ├── calendar_story.rs │ │ │ ├── chart_story/ │ │ │ │ ├── chart_story.rs │ │ │ │ └── stacked_bar_chart.rs │ │ │ ├── chart_story.rs │ │ │ ├── checkbox_story.rs │ │ │ ├── clipboard_story.rs │ │ │ ├── collapsible_story.rs │ │ │ ├── color_picker_story.rs │ │ │ ├── data_table_story.rs │ │ │ ├── date_picker_story.rs │ │ │ ├── description_list_story.rs │ │ │ ├── dialog_story.rs │ │ │ ├── divider_story.rs │ │ │ ├── dropdown_button_story.rs │ │ │ ├── editor_story.rs │ │ │ ├── form_story.rs │ │ │ ├── group_box_story.rs │ │ │ ├── hover_card_story.rs │ │ │ ├── icon_story.rs │ │ │ ├── image_story.rs │ │ │ ├── input_story.rs │ │ │ ├── kbd_story.rs │ │ │ ├── label_story.rs │ │ │ ├── list_story.rs │ │ │ ├── menu_story.rs │ │ │ ├── mod.rs │ │ │ ├── notification_story.rs │ │ │ ├── number_input_story.rs │ │ │ ├── otp_input_story.rs │ │ │ ├── pagination_story.rs │ │ │ ├── popover_story.rs │ │ │ ├── progress_story.rs │ │ │ ├── radio_story.rs │ │ │ ├── rating_story.rs │ │ │ ├── resizable_story.rs │ │ │ ├── scrollbar_story.rs │ │ │ ├── select_story.rs │ │ │ ├── settings_story.rs │ │ │ ├── sheet_story.rs │ │ │ ├── sidebar_story.rs │ │ │ ├── skeleton_story.rs │ │ │ ├── slider_story.rs │ │ │ ├── spinner_story.rs │ │ │ ├── stepper_story.rs │ │ │ ├── switch_story.rs │ │ │ ├── table_story.rs │ │ │ ├── tabs_story.rs │ │ │ ├── tag_story.rs │ │ │ ├── textarea_story.rs │ │ │ ├── theme_story/ │ │ │ │ ├── checkerboard.rs │ │ │ │ ├── color_theme_story.rs │ │ │ │ ├── mapper.rs │ │ │ │ └── mod.rs │ │ │ ├── toggle_story.rs │ │ │ ├── tooltip_story.rs │ │ │ ├── tree_story.rs │ │ │ ├── virtual_list_story.rs │ │ │ └── welcome_story.rs │ │ ├── themes.rs │ │ └── title_bar.rs │ ├── story-web/ │ │ ├── .cargo/ │ │ │ └── config.toml │ │ ├── Cargo.toml │ │ ├── Makefile │ │ ├── README.md │ │ ├── rust-toolchain.toml │ │ ├── scripts/ │ │ │ └── build-wasm.sh │ │ ├── src/ │ │ │ └── lib.rs │ │ └── www/ │ │ ├── .gitignore │ │ ├── .prettierrc │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ └── main.js │ │ └── vite.config.js │ ├── ui/ │ │ ├── Cargo.toml │ │ ├── LICENSE-APACHE │ │ ├── build.rs │ │ ├── locales/ │ │ │ └── ui.yml │ │ └── src/ │ │ ├── accordion.rs │ │ ├── actions.rs │ │ ├── alert.rs │ │ ├── anchored.rs │ │ ├── animation.rs │ │ ├── async_util.rs │ │ ├── avatar/ │ │ │ ├── avatar.rs │ │ │ ├── avatar_group.rs │ │ │ └── mod.rs │ │ ├── badge.rs │ │ ├── breadcrumb.rs │ │ ├── button/ │ │ │ ├── button.rs │ │ │ ├── button_group.rs │ │ │ ├── button_icon.rs │ │ │ ├── dropdown_button.rs │ │ │ ├── mod.rs │ │ │ └── toggle.rs │ │ ├── chart/ │ │ │ ├── area_chart.rs │ │ │ ├── bar_chart.rs │ │ │ ├── candlestick_chart.rs │ │ │ ├── line_chart.rs │ │ │ ├── mod.rs │ │ │ └── pie_chart.rs │ │ ├── checkbox.rs │ │ ├── clipboard.rs │ │ ├── collapsible.rs │ │ ├── color_picker.rs │ │ ├── description_list.rs │ │ ├── dialog/ │ │ │ ├── alert_dialog.rs │ │ │ ├── content.rs │ │ │ ├── description.rs │ │ │ ├── dialog.rs │ │ │ ├── footer.rs │ │ │ ├── header.rs │ │ │ ├── mod.rs │ │ │ └── title.rs │ │ ├── divider.rs │ │ ├── dock/ │ │ │ ├── dock.rs │ │ │ ├── invalid_panel.rs │ │ │ ├── mod.rs │ │ │ ├── panel.rs │ │ │ ├── stack_panel.rs │ │ │ ├── state.rs │ │ │ ├── tab_panel.rs │ │ │ └── tiles.rs │ │ ├── element_ext.rs │ │ ├── event.rs │ │ ├── fixtures/ │ │ │ └── layout.json │ │ ├── focus_trap.rs │ │ ├── form/ │ │ │ ├── field.rs │ │ │ ├── form.rs │ │ │ └── mod.rs │ │ ├── geometry.rs │ │ ├── global_state.rs │ │ ├── group_box.rs │ │ ├── highlighter/ │ │ │ ├── diagnostics.rs │ │ │ ├── highlighter.rs │ │ │ ├── languages/ │ │ │ │ ├── go/ │ │ │ │ │ └── highlights.scm │ │ │ │ ├── html/ │ │ │ │ │ ├── highlights.scm │ │ │ │ │ └── injections.scm │ │ │ │ ├── javascript/ │ │ │ │ │ ├── highlights.scm │ │ │ │ │ └── injections.scm │ │ │ │ ├── json/ │ │ │ │ │ └── highlights.scm │ │ │ │ ├── kotlin/ │ │ │ │ │ └── highlights.scm │ │ │ │ ├── lua/ │ │ │ │ │ └── highlights.scm │ │ │ │ ├── markdown/ │ │ │ │ │ ├── highlights.scm │ │ │ │ │ └── injections.scm │ │ │ │ ├── markdown_inline/ │ │ │ │ │ └── highlights.scm │ │ │ │ ├── php/ │ │ │ │ │ └── injections.scm │ │ │ │ ├── rust/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── highlights.scm │ │ │ │ │ └── injections.scm │ │ │ │ ├── typescript/ │ │ │ │ │ └── highlights.scm │ │ │ │ └── zig/ │ │ │ │ ├── highlights.scm │ │ │ │ └── injections.scm │ │ │ ├── languages.rs │ │ │ ├── mod.rs │ │ │ ├── registry.rs │ │ │ └── wasm_stub.rs │ │ ├── history.rs │ │ ├── hover_card.rs │ │ ├── icon.rs │ │ ├── index_path.rs │ │ ├── input/ │ │ │ ├── blink_cursor.rs │ │ │ ├── change.rs │ │ │ ├── clear_button.rs │ │ │ ├── cursor.rs │ │ │ ├── display_map/ │ │ │ │ ├── README.md │ │ │ │ ├── display_map.rs │ │ │ │ ├── fold_map.rs │ │ │ │ ├── folding.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── text_wrapper.rs │ │ │ │ └── wrap_map.rs │ │ │ ├── element.rs │ │ │ ├── indent.rs │ │ │ ├── input.rs │ │ │ ├── lsp/ │ │ │ │ ├── code_actions.rs │ │ │ │ ├── completions.rs │ │ │ │ ├── definitions.rs │ │ │ │ ├── document_colors.rs │ │ │ │ ├── hover.rs │ │ │ │ └── mod.rs │ │ │ ├── mask_pattern.rs │ │ │ ├── mod.rs │ │ │ ├── mode.rs │ │ │ ├── movement.rs │ │ │ ├── number_input.rs │ │ │ ├── otp_input.rs │ │ │ ├── popovers/ │ │ │ │ ├── code_action_menu.rs │ │ │ │ ├── completion_menu.rs │ │ │ │ ├── context_menu.rs │ │ │ │ ├── diagnostic_popover.rs │ │ │ │ ├── hover_popover.rs │ │ │ │ └── mod.rs │ │ │ ├── rope_ext.rs │ │ │ ├── search.rs │ │ │ ├── selection.rs │ │ │ └── state.rs │ │ ├── inspector.rs │ │ ├── kbd.rs │ │ ├── label.rs │ │ ├── lib.rs │ │ ├── link.rs │ │ ├── list/ │ │ │ ├── cache.rs │ │ │ ├── delegate.rs │ │ │ ├── list.rs │ │ │ ├── list_item.rs │ │ │ ├── loading.rs │ │ │ ├── mod.rs │ │ │ └── separator_item.rs │ │ ├── menu/ │ │ │ ├── app_menu_bar.rs │ │ │ ├── context_menu.rs │ │ │ ├── dropdown_menu.rs │ │ │ ├── menu_item.rs │ │ │ ├── mod.rs │ │ │ └── popup_menu.rs │ │ ├── notification.rs │ │ ├── pagination.rs │ │ ├── plot/ │ │ │ ├── axis.rs │ │ │ ├── grid.rs │ │ │ ├── label.rs │ │ │ ├── mod.rs │ │ │ ├── scale/ │ │ │ │ ├── band.rs │ │ │ │ ├── linear.rs │ │ │ │ ├── ordinal.rs │ │ │ │ ├── point.rs │ │ │ │ └── sealed.rs │ │ │ ├── scale.rs │ │ │ ├── shape/ │ │ │ │ ├── arc.rs │ │ │ │ ├── area.rs │ │ │ │ ├── bar.rs │ │ │ │ ├── line.rs │ │ │ │ ├── pie.rs │ │ │ │ └── stack.rs │ │ │ ├── shape.rs │ │ │ └── tooltip.rs │ │ ├── popover.rs │ │ ├── progress/ │ │ │ ├── mod.rs │ │ │ ├── progress.rs │ │ │ └── progress_circle.rs │ │ ├── radio.rs │ │ ├── rating.rs │ │ ├── resizable/ │ │ │ ├── mod.rs │ │ │ ├── panel.rs │ │ │ └── resize_handle.rs │ │ ├── root.rs │ │ ├── scroll/ │ │ │ ├── mod.rs │ │ │ ├── scrollable.rs │ │ │ ├── scrollable_mask.rs │ │ │ └── scrollbar.rs │ │ ├── select.rs │ │ ├── setting/ │ │ │ ├── fields/ │ │ │ │ ├── bool.rs │ │ │ │ ├── dropdown.rs │ │ │ │ ├── element.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── number.rs │ │ │ │ └── string.rs │ │ │ ├── group.rs │ │ │ ├── item.rs │ │ │ ├── mod.rs │ │ │ ├── page.rs │ │ │ └── settings.rs │ │ ├── sheet.rs │ │ ├── sidebar/ │ │ │ ├── footer.rs │ │ │ ├── group.rs │ │ │ ├── header.rs │ │ │ ├── menu.rs │ │ │ └── mod.rs │ │ ├── skeleton.rs │ │ ├── slider.rs │ │ ├── spinner.rs │ │ ├── stepper/ │ │ │ ├── item.rs │ │ │ ├── mod.rs │ │ │ ├── stepper.rs │ │ │ └── trigger.rs │ │ ├── styled.rs │ │ ├── switch.rs │ │ ├── tab/ │ │ │ ├── mod.rs │ │ │ ├── tab.rs │ │ │ └── tab_bar.rs │ │ ├── table/ │ │ │ ├── column.rs │ │ │ ├── data_table.rs │ │ │ ├── delegate.rs │ │ │ ├── loading.rs │ │ │ ├── mod.rs │ │ │ ├── state.rs │ │ │ └── table.rs │ │ ├── tag.rs │ │ ├── text/ │ │ │ ├── document.rs │ │ │ ├── format/ │ │ │ │ ├── html.rs │ │ │ │ ├── html5minify/ │ │ │ │ │ └── mod.rs │ │ │ │ ├── markdown.rs │ │ │ │ └── mod.rs │ │ │ ├── inline.rs │ │ │ ├── mod.rs │ │ │ ├── node.rs │ │ │ ├── state.rs │ │ │ ├── style.rs │ │ │ ├── text_view.rs │ │ │ └── utils.rs │ │ ├── theme/ │ │ │ ├── color.rs │ │ │ ├── default-colors.json │ │ │ ├── default-theme.json │ │ │ ├── mod.rs │ │ │ ├── registry.rs │ │ │ ├── schema.rs │ │ │ └── theme_color.rs │ │ ├── time/ │ │ │ ├── calendar.rs │ │ │ ├── date_picker.rs │ │ │ ├── mod.rs │ │ │ └── utils.rs │ │ ├── title_bar.rs │ │ ├── tooltip.rs │ │ ├── tree.rs │ │ ├── virtual_list.rs │ │ ├── window_border.rs │ │ └── window_ext.rs │ └── webview/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ └── lib.rs ├── docs/ │ ├── .gitignore │ ├── .vitepress/ │ │ ├── config.mts │ │ ├── language.ts │ │ └── theme/ │ │ ├── components/ │ │ │ └── GitHubStar.vue │ │ ├── index.ts │ │ └── style.css │ ├── README.md │ ├── contributors.md │ ├── contributors.vue │ ├── data/ │ │ ├── contributors.data.js │ │ ├── repo.data.js │ │ └── skills.data.js │ ├── docs/ │ │ ├── assets.md │ │ ├── components/ │ │ │ ├── accordion.md │ │ │ ├── alert-dialog.md │ │ │ ├── alert.md │ │ │ ├── avatar.md │ │ │ ├── badge.md │ │ │ ├── button.md │ │ │ ├── calendar.md │ │ │ ├── chart.md │ │ │ ├── checkbox.md │ │ │ ├── clipboard.md │ │ │ ├── collapsible.md │ │ │ ├── color-picker.md │ │ │ ├── data-table.md │ │ │ ├── date-picker.md │ │ │ ├── description-list.md │ │ │ ├── dialog.md │ │ │ ├── dropdown_button.md │ │ │ ├── editor.md │ │ │ ├── focus-trap.md │ │ │ ├── form.md │ │ │ ├── group-box.md │ │ │ ├── hover-card.md │ │ │ ├── icon.md │ │ │ ├── image.md │ │ │ ├── index.md │ │ │ ├── input.md │ │ │ ├── kbd.md │ │ │ ├── label.md │ │ │ ├── list.md │ │ │ ├── menu.md │ │ │ ├── notification.md │ │ │ ├── number-input.md │ │ │ ├── otp-input.md │ │ │ ├── pagination.md │ │ │ ├── plot.md │ │ │ ├── popover.md │ │ │ ├── progress.md │ │ │ ├── radio.md │ │ │ ├── rating.md │ │ │ ├── resizable.md │ │ │ ├── scrollable.md │ │ │ ├── select.md │ │ │ ├── settings.md │ │ │ ├── sheet.md │ │ │ ├── sidebar.md │ │ │ ├── skeleton.md │ │ │ ├── slider.md │ │ │ ├── spinner.md │ │ │ ├── stepper.md │ │ │ ├── switch.md │ │ │ ├── table.md │ │ │ ├── tabs.md │ │ │ ├── tag.md │ │ │ ├── title-bar.md │ │ │ ├── toggle.md │ │ │ ├── tooltip.md │ │ │ ├── tree.md │ │ │ └── virtual-list.md │ │ ├── context.md │ │ ├── element_id.md │ │ ├── getting-started.md │ │ ├── index.md │ │ ├── installation.md │ │ ├── root.md │ │ └── theme.md │ ├── index.md │ ├── index.vue │ ├── package.json │ ├── postcss.config.mjs │ ├── skills.md │ ├── skills.vue │ └── src/ │ ├── dark.theme.json │ └── light.theme.json ├── examples/ │ ├── README.md │ ├── app_assets/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── main.rs │ ├── color_mix_oklab.rs │ ├── dialog_overlay/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── focus_trap/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── hello_world/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── input/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── system_monitor/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── webview/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ └── window_title/ │ ├── Cargo.toml │ └── src/ │ └── main.rs ├── flake.nix ├── script/ │ ├── bootstrap │ ├── bump-version.sh │ ├── install-linux.sh │ └── install-window.ps1 └── themes/ ├── adventure.json ├── alduin.json ├── asciinema.json ├── ayu.json ├── catppuccin.json ├── everforest.json ├── fahrenheit.json ├── flexoki.json ├── gruvbox.json ├── harper.json ├── hybrid.json ├── jellybeans.json ├── kibble.json ├── macos-classic.json ├── matrix.json ├── mellifluous.json ├── molokai.json ├── solarized.json ├── spaceduck.json ├── tokyonight.json └── twilight.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [target.x86_64-pc-windows-msvc] rustflags = ["-C", "link-arg=/STACK:8000000"] ================================================ FILE: .claude/COMPONENT_TEST_RULES.md ================================================ # GPUI Component Testing Rules ## Testing Principles ### 1. **Simplicity First** - Avoid excessive simple tests - Focus on complex logic and core functionality ### 2. **Builder Pattern Testing** - Every component should have a `test_*_builder` test for coverage of the builder pattern - Tests should cover all major configuration options - Use method chaining to demonstrate complete API usage #### Example: ```rust #[gpui::test] fn test_button_builder(_cx: &mut gpui::TestAppContext) { let button = Button::new("complex-button") .label("Save Changes") .primary() .outline() .large() .tooltip("Click to save") .compact() .loading(false) .disabled(false) .selected(false) .on_click(|_, _, _| {}); // Assert all key properties assert_eq!(button.label, Some("Save Changes".into())); assert_eq!(button.variant, ButtonVariant::Primary); assert!(button.outline); assert_eq!(button.size, Size::Large); } ``` ### 3. **Complex Logic Testing** - Test conditional branching logic - Test state transitions and interactions - Test edge cases #### Example: ```rust #[gpui::test] fn test_button_clickable_logic(_cx: &mut gpui::TestAppContext) { // Test behavior under multiple conditions let clickable = Button::new("test").on_click(|_, _, _| {}); assert!(clickable.clickable()); let disabled = Button::new("test").disabled(true).on_click(|_, _, _| {}); assert!(!disabled.clickable()); let loading = Button::new("test").loading(true).on_click(|_, _, _| {}); assert!(!loading.clickable()); } ``` ### 4. **Helper Method Testing** - Test component helper methods and validation logic - Combine related tests into a single function #### Example: ```rust #[gpui::test] fn test_button_variant_methods(_cx: &mut gpui::TestAppContext) { // Test variant check methods assert!(ButtonVariant::Link.is_link()); assert!(ButtonVariant::Text.is_text()); assert!(ButtonVariant::Ghost.is_ghost()); // Test related logic assert!(ButtonVariant::Link.no_padding()); assert!(ButtonVariant::Text.no_padding()); } ``` ## What NOT to Test ### ❌ Anti-patterns to Avoid 1. **Simple getter/setter tests** ```rust // ❌ Don't write tests like this #[gpui::test] fn test_button_with_label(_cx: &mut gpui::TestAppContext) { let button = Button::new("test").label("Click Me"); assert_eq!(button.label, Some("Click Me".into())); } ``` 2. **Individual property tests** ```rust // ❌ Don't write separate tests for each property #[gpui::test] fn test_button_disabled(_cx: &mut gpui::TestAppContext) { let button = Button::new("test").disabled(true); assert!(button.disabled); } #[gpui::test] fn test_button_selected(_cx: &mut gpui::TestAppContext) { let button = Button::new("test").selected(true); assert!(button.selected); } ``` _These should be merged into the builder pattern test_ 3. **Individual size/variant tests** ```rust // ❌ Don't write separate tests for each size #[gpui::test] fn test_button_xsmall(_cx: &mut gpui::TestAppContext) { let button = Button::new("test").xsmall(); assert_eq!(button.size, Size::XSmall); } #[gpui::test] fn test_button_small(_cx: &mut gpui::TestAppContext) { let button = Button::new("test").small(); assert_eq!(button.size, Size::Small); } ``` ## Test File Structure ```rust #[cfg(test)] mod tests { use super::*; // 1. Builder pattern test (required) #[gpui::test] fn test_component_builder(_cx: &mut gpui::TestAppContext) { // Test complete method chaining } // 2. Complex logic test (if applicable) #[gpui::test] fn test_component_complex_logic(_cx: &mut gpui::TestAppContext) { // Test conditional branches, state transitions, etc. } // 3. Helper method test (if applicable) #[gpui::test] fn test_component_helper_methods(_cx: &mut gpui::TestAppContext) { // Test helper methods } } ``` ## Test Count Guidelines | Component Type | Recommended Tests | Notes | | ----------------- | ----------------- | -------------------------------- | | Simple component | 1-2 tests | Builder + complex logic (if any) | | Medium component | 2-3 tests | Builder + logic + helper methods | | Complex component | 3-5 tests | Based on actual complexity | ## Real-world Examples ### Button Component (3 tests) - `test_button_builder` - Complete configuration test - `test_button_clickable_logic` - Click logic test - `test_button_variant_methods` - Variant method test ### ButtonIcon Component (2 tests) - `test_button_icon_builder` - Complete configuration test - `test_button_icon_variant_types` - Variant type test ### ButtonGroup Component (1 test) - `test_button_group_builder` - Complete configuration test (covers all important features) ### DropdownButton Component (1 test) - `test_dropdown_button_builder` - Complete configuration test ### Toggle Component (2 tests) - `test_toggle_builder` - Toggle configuration test - `test_toggle_group_builder` - ToggleGroup configuration test ## GPUI Test Usage ### When to Use `#[gpui::test]` - When testing UI component rendering - When testing window-dependent behavior - When testing interactive elements that require event handling ### When NOT to Use `#[gpui::test]` - For pure logic tests that don't involve rendering - For utility function tests - For simple data structure tests - For validation logic that doesn't require app context #### Example: ```rust // ✅ Use regular Rust test for simple logic #[test] fn test_button_variant_conversion() { let rounded: ButtonRounded = px(5.0).into(); assert!(matches!(rounded, ButtonRounded::Size(_))); } // ✅ Use gpui::test for component behavior #[gpui::test] fn test_button_builder(_cx: &mut gpui::TestAppContext) { let button = Button::new("test").large(); assert_eq!(button.size, Size::Large); } ``` ## Summary ✅ **DO**: - Test complete builder patterns - Test complex business logic - Test conditional branches and state transitions - Combine related tests - Use regular `#[test]` when GPUI context is not needed ❌ **DON'T**: - Test simple property setters - Write separate tests for each property/size/variant - Test obvious functionality - Over-fragment tests - Use `#[gpui::test]` unnecessarily **Goal**: Cover the most critical functionality with minimal tests while keeping code clean and maintainable. ================================================ FILE: .claude/skills/generate-component-documentation/SKILL.md ================================================ --- name: generate-component-documentation description: Generate documentation for new components. Use when writing docs, documenting components, or creating component documentation. --- ## Instructions When generating documentation for a new component: 1. **Follow existing patterns**: Use the documentation styles found in the `docs` folder (examples: `button.md`, `accordion.md`, etc.) 2. **Reference implementations**: Base the documentation on the same-named story implementation in `crates/story/src/stories` 3. **API references**: Use markdown `code` blocks with links to docs.rs for component API references when applicable ## Examples The generated documentation should include: - Component description and purpose - Props/API documentation - Usage examples - Visual examples (if applicable) ================================================ FILE: .claude/skills/generate-component-story/SKILL.md ================================================ --- name: generate-component-story description: Create story examples for components. Use when writing stories, creating examples, or demonstrating component usage. --- ## Instructions When creating component stories: 1. **Follow existing patterns**: Base stories on the styles found in `crates/story/src/stories` (examples: `tabs_story.rs`, `group_box_story.rs`, etc.) 2. **Use sections**: Organize the story with `section!` calls for each major part 3. **Comprehensive coverage**: Include all options, variants, and usage examples of the component ## Examples A typical story structure includes: - Basic usage examples - Different variants and states - Interactive examples - Edge cases and error states ================================================ FILE: .claude/skills/github-pull-request-description/SKILL.md ================================================ --- name: github-pull-request-description description: Write a description to description GitHub Pull Request. --- ## Description We less than 150 words description for a PR changes, including new features, bug fixes, and improvements. And if there have APIs break changes (Only `crates/ui` changes) we should have a section called `## Breaking Changes` to list them clearly. ## Breaking changes description When a pull request introduces breaking changes to a codebase, it's important to clearly communicate these changes to users and developers who rely on the code. A well-written breaking changes description helps ensure that everyone understands what has changed, why it has changed, and how to adapt to the new version. We can get the changes from the PR diff and summarize them in a clear and concise manner. Aim to provide a clear APIs changes for users to follow. ### Format We pefer the following format for breaking changes descriptions: 1. Use bullet list for each breaking change item. 2. Each item should have title and a code block showing the old and new usage by use `diff`. 3. Use `## Breaking Changes` as the section title. 4. Use english language. **For example:** ````md ## Breaking Changes - Added `id` parameter to `Sidebar::new`. ```diff - Sidebar::new() + Sidebar::new("sidebar") ``` - Removed the `left` and `right` methods; use `side` instead. > Default is left. ```diff - Sidebar::right() + Sidebar::new("sidebar").side(Side::Right) ``` ```` ================================================ FILE: .claude/skills/gpui-action/SKILL.md ================================================ --- name: gpui-action description: Action definitions and keyboard shortcuts in GPUI. Use when implementing actions, keyboard shortcuts, or key bindings. --- ## Overview Actions provide declarative keyboard-driven UI interactions in GPUI. **Key Concepts:** - Define actions with `actions!` macro or `#[derive(Action)]` - Bind keys with `cx.bind_keys()` - Handle with `.on_action()` on elements - Context-aware via `key_context()` ## Quick Start ### Simple Actions ```rust use gpui::actions; actions!(editor, [MoveUp, MoveDown, Save, Quit]); const CONTEXT: &str = "Editor"; pub fn init(cx: &mut App) { cx.bind_keys([ KeyBinding::new("up", MoveUp, Some(CONTEXT)), KeyBinding::new("down", MoveDown, Some(CONTEXT)), KeyBinding::new("cmd-s", Save, Some(CONTEXT)), KeyBinding::new("cmd-q", Quit, Some(CONTEXT)), ]); } impl Render for Editor { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .key_context(CONTEXT) .on_action(cx.listener(Self::move_up)) .on_action(cx.listener(Self::move_down)) .on_action(cx.listener(Self::save)) } } impl Editor { fn move_up(&mut self, _: &MoveUp, cx: &mut Context) { // Handle move up cx.notify(); } fn move_down(&mut self, _: &MoveDown, cx: &mut Context) { cx.notify(); } fn save(&mut self, _: &Save, cx: &mut Context) { // Save logic cx.notify(); } } ``` ### Actions with Parameters ```rust #[derive(Clone, PartialEq, Action, Deserialize)] #[action(namespace = editor)] pub struct InsertText { pub text: String, } #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = editor, no_json)] pub struct Digit(pub u8); cx.bind_keys([ KeyBinding::new("0", Digit(0), Some(CONTEXT)), KeyBinding::new("1", Digit(1), Some(CONTEXT)), // ... ]); impl Editor { fn on_digit(&mut self, action: &Digit, cx: &mut Context) { self.insert_digit(action.0, cx); } } ``` ## Key Formats ```rust // Modifiers "cmd-s" // Command (macOS) / Ctrl (Windows/Linux) "ctrl-c" // Control "alt-f" // Alt "shift-tab" // Shift "cmd-ctrl-f" // Multiple modifiers // Keys "a-z", "0-9" // Letters and numbers "f1-f12" // Function keys "up", "down", "left", "right" "enter", "escape", "space", "tab" "backspace", "delete" "-", "=", "[", "]", etc. // Special characters ``` ## Action Naming Prefer verb-noun pattern: ```rust actions!([ OpenFile, // ✅ Good CloseWindow, // ✅ Good ToggleSidebar, // ✅ Good Save, // ✅ Good (common exception) ]); ``` ## Context-Aware Bindings ```rust const EDITOR_CONTEXT: &str = "Editor"; const MODAL_CONTEXT: &str = "Modal"; // Same key, different contexts cx.bind_keys([ KeyBinding::new("escape", CloseModal, Some(MODAL_CONTEXT)), KeyBinding::new("escape", ClearSelection, Some(EDITOR_CONTEXT)), ]); // Set context on element div() .key_context(EDITOR_CONTEXT) .child(editor_content) ``` ## Best Practices ### ✅ Use Contexts ```rust // ✅ Good: Context-aware div() .key_context("MyComponent") .on_action(cx.listener(Self::handle)) ``` ### ✅ Name Actions Clearly ```rust // ✅ Good: Clear intent actions!([ SaveDocument, CloseTab, TogglePreview, ]); ``` ### ✅ Handle with Listeners ```rust // ✅ Good: Proper handler naming impl MyComponent { fn on_action_save(&mut self, _: &Save, cx: &mut Context) { // Handle save cx.notify(); } } div().on_action(cx.listener(Self::on_action_save)) ``` ## Reference Documentation - **Complete Guide**: See [reference.md](references/reference.md) - Action definition, keybinding, dispatch - Focus-based routing, best practices - Performance, accessibility ================================================ FILE: .claude/skills/gpui-async/SKILL.md ================================================ --- name: gpui-async description: Async operations and background tasks in GPUI. Use when working with async, spawn, background tasks, or concurrent operations. Essential for handling async I/O, long-running computations, and coordinating between foreground UI updates and background work. --- ## Overview GPUI provides integrated async runtime for foreground UI updates and background computation. **Key Concepts:** - **Foreground tasks**: UI thread, can update entities (`cx.spawn`) - **Background tasks**: Worker threads, CPU-intensive work (`cx.background_spawn`) - All entity updates happen on foreground thread ## Quick Start ### Foreground Tasks (UI Updates) ```rust impl MyComponent { fn fetch_data(&mut self, cx: &mut Context) { let entity = cx.entity().downgrade(); cx.spawn(async move |cx| { // Runs on UI thread, can await and update entities let data = fetch_from_api().await; entity.update(cx, |state, cx| { state.data = Some(data); cx.notify(); }).ok(); }).detach(); } } ``` ### Background Tasks (Heavy Work) ```rust impl MyComponent { fn process_file(&mut self, cx: &mut Context) { let entity = cx.entity().downgrade(); cx.background_spawn(async move { // Runs on background thread, CPU-intensive let result = heavy_computation().await; result }) .then(cx.spawn(move |result, cx| { // Back to foreground to update UI entity.update(cx, |state, cx| { state.result = result; cx.notify(); }).ok(); })) .detach(); } } ``` ### Task Management ```rust struct MyView { _task: Task<()>, // Prefix with _ if stored but not accessed } impl MyView { fn new(cx: &mut Context) -> Self { let entity = cx.entity().downgrade(); let _task = cx.spawn(async move |cx| { // Task automatically cancelled when dropped loop { tokio::time::sleep(Duration::from_secs(1)).await; entity.update(cx, |state, cx| { state.tick(); cx.notify(); }).ok(); } }); Self { _task } } } ``` ## Core Patterns ### 1. Async Data Fetching ```rust cx.spawn(async move |cx| { let data = fetch_data().await?; entity.update(cx, |state, cx| { state.data = Some(data); cx.notify(); })?; Ok::<_, anyhow::Error>(()) }).detach(); ``` ### 2. Background Computation + UI Update ```rust cx.background_spawn(async move { heavy_work() }) .then(cx.spawn(move |result, cx| { entity.update(cx, |state, cx| { state.result = result; cx.notify(); }).ok(); })) .detach(); ``` ### 3. Periodic Tasks ```rust cx.spawn(async move |cx| { loop { tokio::time::sleep(Duration::from_secs(5)).await; // Update every 5 seconds } }).detach(); ``` ### 4. Task Cancellation Tasks are automatically cancelled when dropped. Store in struct to keep alive. ## Common Pitfalls ### ❌ Don't: Update entities from background tasks ```rust // ❌ Wrong: Can't update entities from background thread cx.background_spawn(async move { entity.update(cx, |state, cx| { // Compile error! state.data = data; }); }); ``` ### ✅ Do: Use foreground task or chain ```rust // ✅ Correct: Chain with foreground task cx.background_spawn(async move { data }) .then(cx.spawn(move |data, cx| { entity.update(cx, |state, cx| { state.data = data; cx.notify(); }).ok(); })) .detach(); ``` ## Reference Documentation ### Complete Guides - **API Reference**: See [api-reference.md](references/api-reference.md) - Task types, spawning methods, contexts - Executors, cancellation, error handling - **Patterns**: See [patterns.md](references/patterns.md) - Data fetching, background processing - Polling, debouncing, parallel tasks - Pattern selection guide - **Best Practices**: See [best-practices.md](references/best-practices.md) - Error handling, cancellation - Performance optimization, testing - Common pitfalls and solutions ================================================ FILE: .claude/skills/gpui-context/SKILL.md ================================================ --- name: gpui-context description: Context management in GPUI including App, Window, and AsyncApp. Use when working with contexts, entity updates, or window operations. Different context types provide different capabilities for UI rendering, entity management, and async operations. --- ## Overview GPUI uses different context types for different scenarios: **Context Types:** - **`App`**: Global app state, entity creation - **`Window`**: Window-specific operations, painting, layout - **`Context`**: Entity-specific context for component `T` - **`AsyncApp`**: Async context for foreground tasks - **`AsyncWindowContext`**: Async context with window access ## Quick Start ### Context - Component Context ```rust impl MyComponent { fn update_state(&mut self, cx: &mut Context) { self.value = 42; cx.notify(); // Trigger re-render // Spawn async task cx.spawn(async move |cx| { // Async work }).detach(); // Get current entity let entity = cx.entity(); } } ``` ### App - Global Context ```rust fn main() { let app = Application::new(); app.run(|cx: &mut App| { // Create entities let entity = cx.new(|cx| MyState::default()); // Open windows cx.open_window(WindowOptions::default(), |window, cx| { cx.new(|cx| Root::new(view, window, cx)) }); }); } ``` ### Window - Window Context ```rust impl Render for MyView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { // Window operations let is_focused = window.is_window_focused(); let bounds = window.bounds(); div().child("Content") } } ``` ### AsyncApp - Async Context ```rust cx.spawn(async move |cx: &mut AsyncApp| { let data = fetch_data().await; entity.update(cx, |state, inner_cx| { state.data = data; inner_cx.notify(); }).ok(); }).detach(); ``` ## Common Operations ### Entity Operations ```rust // Create entity let entity = cx.new(|cx| MyState::default()); // Update entity entity.update(cx, |state, cx| { state.value = 42; cx.notify(); }); // Read entity let value = entity.read(cx).value; ``` ### Notifications and Events ```rust // Trigger re-render cx.notify(); // Emit event cx.emit(MyEvent::Updated); // Observe entity cx.observe(&entity, |this, observed, cx| { // React to changes }).detach(); // Subscribe to events cx.subscribe(&entity, |this, source, event, cx| { // Handle event }).detach(); ``` ### Window Operations ```rust // Window state let focused = window.is_window_focused(); let bounds = window.bounds(); let scale = window.scale_factor(); // Close window window.remove_window(); ``` ### Async Operations ```rust // Spawn foreground task cx.spawn(async move |cx| { // Async work with entity access }).detach(); // Spawn background task cx.background_spawn(async move { // Heavy computation }).detach(); ``` ## Context Hierarchy ``` App (Global) └─ Window (Per-window) └─ Context (Per-component) └─ AsyncApp (In async tasks) └─ AsyncWindowContext (Async + Window) ``` ## Reference Documentation - **API Reference**: See [api-reference.md](references/api-reference.md) - Complete context API, methods, conversions - Entity operations, window operations - Async contexts, best practices ================================================ FILE: .claude/skills/gpui-element/SKILL.md ================================================ --- name: gpui-element description: Implementing custom elements using GPUI's low-level Element API (vs. high-level Render/RenderOnce APIs). Use when you need maximum control over layout, prepaint, and paint phases for complex, performance-critical custom UI components that cannot be achieved with Render/RenderOnce traits. --- ## When to Use Use the low-level `Element` trait when: - Need fine-grained control over layout calculation - Building complex, performance-critical components - Implementing custom layout algorithms (masonry, circular, etc.) - High-level `Render`/`RenderOnce` APIs are insufficient **Prefer `Render`/`RenderOnce` for:** Simple components, standard layouts, declarative UI ## Quick Start The `Element` trait provides direct control over three rendering phases: ```rust impl Element for MyElement { type RequestLayoutState = MyLayoutState; // Data passed to later phases type PrepaintState = MyPaintState; // Data for painting fn id(&self) -> Option { Some(self.id.clone()) } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } // Phase 1: Calculate sizes and positions fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, Self::RequestLayoutState) { let layout_id = window.request_layout( Style { size: size(px(200.), px(100.)), ..default() }, vec![], cx ); (layout_id, MyLayoutState { /* ... */ }) } // Phase 2: Create hitboxes, prepare for painting fn prepaint(&mut self, .., bounds: Bounds, layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App) -> Self::PrepaintState { let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); MyPaintState { hitbox } } // Phase 3: Render and handle interactions fn paint(&mut self, .., bounds: Bounds, layout: &mut Self::RequestLayoutState, paint_state: &mut Self::PrepaintState, window: &mut Window, cx: &mut App) { window.paint_quad(paint_quad(bounds, Corners::all(px(4.)), cx.theme().background)); window.on_mouse_event({ let hitbox = paint_state.hitbox.clone(); move |event: &MouseDownEvent, phase, window, cx| { if hitbox.is_hovered(window) && phase.bubble() { // Handle interaction cx.stop_propagation(); } } }); } } // Enable element to be used as child impl IntoElement for MyElement { type Element = Self; fn into_element(self) -> Self::Element { self } } ``` ## Core Concepts ### Three-Phase Rendering 1. **request_layout**: Calculate sizes and positions, return layout ID and state 2. **prepaint**: Create hitboxes, compute final bounds, prepare for painting 3. **paint**: Render element, set up interactions (mouse events, cursor styles) ### State Flow ``` RequestLayoutState → PrepaintState → paint ``` State flows in one direction through associated types, passed as mutable references between phases. ### Key Operations - **Layout**: `window.request_layout(style, children, cx)` - Create layout node - **Hitboxes**: `window.insert_hitbox(bounds, behavior)` - Create interaction area - **Painting**: `window.paint_quad(...)` - Render visual content - **Events**: `window.on_mouse_event(handler)` - Handle user input ## Reference Documentation ### Complete API Documentation - **Element Trait API**: See [api-reference.md](references/api-reference.md) - Associated types, methods, parameters, return values - Hitbox system, event handling, cursor styles ### Implementation Guides - **Examples**: See [examples.md](references/examples.md) - Simple text element with highlighting - Interactive element with selection - Complex element with child management - **Best Practices**: See [best-practices.md](references/best-practices.md) - State management, performance optimization - Interaction handling, layout strategies - Error handling, testing, common pitfalls - **Common Patterns**: See [patterns.md](references/patterns.md) - Text rendering, container, interactive, composite, scrollable patterns - Pattern selection guide - **Advanced Patterns**: See [advanced-patterns.md](references/advanced-patterns.md) - Custom layout algorithms (masonry, circular) - Element composition with traits - Async updates, memoization, virtual lists ================================================ FILE: .claude/skills/gpui-element/references/advanced-patterns.md ================================================ # Advanced Element Patterns Advanced techniques and patterns for implementing sophisticated GPUI elements. ## Custom Layout Algorithms Implementing custom layout algorithms not supported by GPUI's built-in layouts. ### Masonry Layout (Pinterest-Style) ```rust pub struct MasonryLayout { id: ElementId, columns: usize, gap: Pixels, children: Vec, } struct MasonryLayoutState { column_layouts: Vec>, column_heights: Vec, } struct MasonryPaintState { child_bounds: Vec>, } impl Element for MasonryLayout { type RequestLayoutState = MasonryLayoutState; type PrepaintState = MasonryPaintState; fn id(&self) -> Option { Some(self.id.clone()) } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App ) -> (LayoutId, MasonryLayoutState) { // Initialize columns let mut columns: Vec> = vec![Vec::new(); self.columns]; let mut column_heights = vec![px(0.); self.columns]; // Distribute children across columns for child in &mut self.children { let (child_layout_id, _) = child.request_layout( global_id, inspector_id, window, cx ); let child_size = window.layout_bounds(child_layout_id).size; // Find shortest column let min_column_idx = column_heights .iter() .enumerate() .min_by(|a, b| a.1.partial_cmp(b.1).unwrap()) .unwrap() .0; // Add child to shortest column columns[min_column_idx].push(child_layout_id); column_heights[min_column_idx] += child_size.height + self.gap; } // Calculate total layout size let column_width = px(200.); // Fixed column width let total_width = column_width * self.columns as f32 + self.gap * (self.columns - 1) as f32; let total_height = column_heights.iter() .max_by(|a, b| a.partial_cmp(b).unwrap()) .copied() .unwrap_or(px(0.)); let layout_id = window.request_layout( Style { size: size(total_width, total_height), ..default() }, columns.iter().flatten().copied().collect(), cx ); (layout_id, MasonryLayoutState { column_layouts: columns, column_heights, }) } fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, layout_state: &mut MasonryLayoutState, window: &mut Window, cx: &mut App ) -> MasonryPaintState { let column_width = px(200.); let mut child_bounds = Vec::new(); // Position children in columns for (col_idx, column) in layout_state.column_layouts.iter().enumerate() { let x_offset = bounds.left() + (column_width + self.gap) * col_idx as f32; let mut y_offset = bounds.top(); for (child_idx, layout_id) in column.iter().enumerate() { let child_size = window.layout_bounds(*layout_id).size; let child_bound = Bounds::new( point(x_offset, y_offset), size(column_width, child_size.height) ); self.children[child_idx].prepaint( global_id, inspector_id, child_bound, window, cx ); child_bounds.push(child_bound); y_offset += child_size.height + self.gap; } } MasonryPaintState { child_bounds } } fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, _bounds: Bounds, _layout_state: &mut MasonryLayoutState, paint_state: &mut MasonryPaintState, window: &mut Window, cx: &mut App ) { for (child, bounds) in self.children.iter_mut().zip(&paint_state.child_bounds) { child.paint(global_id, inspector_id, *bounds, window, cx); } } } ``` ### Circular Layout ```rust pub struct CircularLayout { id: ElementId, radius: Pixels, children: Vec, } impl Element for CircularLayout { type RequestLayoutState = Vec; type PrepaintState = Vec>; fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App ) -> (LayoutId, Vec) { let child_layouts: Vec<_> = self.children .iter_mut() .map(|child| child.request_layout(global_id, inspector_id, window, cx).0) .collect(); let diameter = self.radius * 2.; let layout_id = window.request_layout( Style { size: size(diameter, diameter), ..default() }, child_layouts.clone(), cx ); (layout_id, child_layouts) } fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, layout_ids: &mut Vec, window: &mut Window, cx: &mut App ) -> Vec> { let center = bounds.center(); let angle_step = 2.0 * std::f32::consts::PI / self.children.len() as f32; let mut child_bounds = Vec::new(); for (i, (child, layout_id)) in self.children.iter_mut() .zip(layout_ids.iter()) .enumerate() { let angle = angle_step * i as f32; let child_size = window.layout_bounds(*layout_id).size; // Position child on circle let x = center.x + self.radius * angle.cos() - child_size.width / 2.; let y = center.y + self.radius * angle.sin() - child_size.height / 2.; let child_bound = Bounds::new(point(x, y), child_size); child.prepaint(global_id, inspector_id, child_bound, window, cx); child_bounds.push(child_bound); } child_bounds } fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, _bounds: Bounds, _layout_ids: &mut Vec, child_bounds: &mut Vec>, window: &mut Window, cx: &mut App ) { for (child, bounds) in self.children.iter_mut().zip(child_bounds) { child.paint(global_id, inspector_id, *bounds, window, cx); } } } ``` ## Element Composition with Traits Create reusable behaviors via traits for element composition. ### Hoverable Trait ```rust pub trait Hoverable: Element { fn on_hover(&mut self, f: F) -> &mut Self where F: Fn(&mut Window, &mut App) + 'static; fn on_hover_end(&mut self, f: F) -> &mut Self where F: Fn(&mut Window, &mut App) + 'static; } // Implementation for custom element pub struct HoverableElement { id: ElementId, content: AnyElement, hover_handlers: Vec>, hover_end_handlers: Vec>, was_hovered: bool, } impl Hoverable for HoverableElement { fn on_hover(&mut self, f: F) -> &mut Self where F: Fn(&mut Window, &mut App) + 'static { self.hover_handlers.push(Box::new(f)); self } fn on_hover_end(&mut self, f: F) -> &mut Self where F: Fn(&mut Window, &mut App) + 'static { self.hover_end_handlers.push(Box::new(f)); self } } impl Element for HoverableElement { type RequestLayoutState = LayoutId; type PrepaintState = Hitbox; fn paint( &mut self, _global_id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, bounds: Bounds, _layout: &mut LayoutId, hitbox: &mut Hitbox, window: &mut Window, cx: &mut App ) { let is_hovered = hitbox.is_hovered(window); // Trigger hover events if is_hovered && !self.was_hovered { for handler in &self.hover_handlers { handler(window, cx); } } else if !is_hovered && self.was_hovered { for handler in &self.hover_end_handlers { handler(window, cx); } } self.was_hovered = is_hovered; // Paint content self.content.paint(bounds, window, cx); } // ... other methods } ``` ### Clickable Trait ```rust pub trait Clickable: Element { fn on_click(&mut self, f: F) -> &mut Self where F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static; fn on_double_click(&mut self, f: F) -> &mut Self where F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static; } pub struct ClickableElement { id: ElementId, content: AnyElement, click_handlers: Vec>, double_click_handlers: Vec>, last_click_time: Option, } impl Clickable for ClickableElement { fn on_click(&mut self, f: F) -> &mut Self where F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static { self.click_handlers.push(Box::new(f)); self } fn on_double_click(&mut self, f: F) -> &mut Self where F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static { self.double_click_handlers.push(Box::new(f)); self } } ``` ## Async Element Updates Elements that update based on async operations. ```rust pub struct AsyncElement { id: ElementId, state: Entity, loading: bool, data: Option, } pub struct AsyncState { loading: bool, data: Option, } impl Element for AsyncElement { type RequestLayoutState = (); type PrepaintState = Hitbox; fn paint( &mut self, _global_id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, bounds: Bounds, _layout: &mut (), hitbox: &mut Hitbox, window: &mut Window, cx: &mut App ) { // Display loading or data if self.loading { // Paint loading indicator self.paint_loading(bounds, window, cx); } else if let Some(data) = &self.data { // Paint data self.paint_data(data, bounds, window, cx); } // Trigger async update on click window.on_mouse_event({ let state = self.state.clone(); let hitbox = hitbox.clone(); move |event: &MouseUpEvent, phase, window, cx| { if hitbox.is_hovered(window) && phase.bubble() { // Spawn async task cx.spawn({ let state = state.clone(); async move { // Perform async operation let result = fetch_data_async().await; // Update state on completion state.update(cx, |state, cx| { state.loading = false; state.data = Some(result); cx.notify(); }); } }).detach(); // Set loading state immediately state.update(cx, |state, cx| { state.loading = true; cx.notify(); }); cx.stop_propagation(); } } }); } // ... other methods } async fn fetch_data_async() -> String { // Simulate async operation tokio::time::sleep(Duration::from_secs(1)).await; "Data loaded!".to_string() } ``` ## Element Memoization Optimize performance by memoizing expensive element computations. ```rust pub struct MemoizedElement { id: ElementId, value: T, render_fn: Box AnyElement>, cached_element: Option, last_value: Option, } impl MemoizedElement { pub fn new(id: ElementId, value: T, render_fn: F) -> Self where F: Fn(&T) -> AnyElement + 'static, { Self { id, value, render_fn: Box::new(render_fn), cached_element: None, last_value: None, } } } impl Element for MemoizedElement { type RequestLayoutState = LayoutId; type PrepaintState = (); fn id(&self) -> Option { Some(self.id.clone()) } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App ) -> (LayoutId, LayoutId) { // Check if value changed if self.last_value.as_ref() != Some(&self.value) || self.cached_element.is_none() { // Recompute element self.cached_element = Some((self.render_fn)(&self.value)); self.last_value = Some(self.value.clone()); } // Request layout for cached element let (layout_id, _) = self.cached_element .as_mut() .unwrap() .request_layout(global_id, inspector_id, window, cx); (layout_id, layout_id) } fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, _layout_id: &mut LayoutId, window: &mut Window, cx: &mut App ) -> () { self.cached_element .as_mut() .unwrap() .prepaint(global_id, inspector_id, bounds, window, cx); } fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, _layout_id: &mut LayoutId, _: &mut (), window: &mut Window, cx: &mut App ) { self.cached_element .as_mut() .unwrap() .paint(global_id, inspector_id, bounds, window, cx); } } // Usage fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { MemoizedElement::new( ElementId::Name("memoized".into()), self.expensive_value.clone(), |value| { // Expensive rendering function only called when value changes div().child(format!("Computed: {}", value)) } ) } ``` ## Virtual List Pattern Efficiently render large lists by only rendering visible items. ```rust pub struct VirtualList { id: ElementId, item_count: usize, item_height: Pixels, viewport_height: Pixels, scroll_offset: Pixels, render_item: Box AnyElement>, } struct VirtualListState { visible_range: Range, visible_item_layouts: Vec, } impl Element for VirtualList { type RequestLayoutState = VirtualListState; type PrepaintState = Hitbox; fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App ) -> (LayoutId, VirtualListState) { // Calculate visible range let start_idx = (self.scroll_offset / self.item_height).floor() as usize; let end_idx = ((self.scroll_offset + self.viewport_height) / self.item_height) .ceil() as usize; let visible_range = start_idx..end_idx.min(self.item_count); // Request layout only for visible items let visible_item_layouts: Vec<_> = visible_range.clone() .map(|i| { let mut item = (self.render_item)(i); item.request_layout(global_id, inspector_id, window, cx).0 }) .collect(); let total_height = self.item_height * self.item_count as f32; let layout_id = window.request_layout( Style { size: size(relative(1.0), self.viewport_height), overflow: Overflow::Hidden, ..default() }, visible_item_layouts.clone(), cx ); (layout_id, VirtualListState { visible_range, visible_item_layouts, }) } fn prepaint( &mut self, _global_id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, bounds: Bounds, state: &mut VirtualListState, window: &mut Window, _cx: &mut App ) -> Hitbox { // Prepaint visible items at correct positions for (i, layout_id) in state.visible_item_layouts.iter().enumerate() { let item_idx = state.visible_range.start + i; let y = item_idx as f32 * self.item_height - self.scroll_offset; let item_bounds = Bounds::new( point(bounds.left(), bounds.top() + y), size(bounds.width(), self.item_height) ); // Prepaint if visible if item_bounds.intersects(&bounds) { // Prepaint item... } } window.insert_hitbox(bounds, HitboxBehavior::Normal) } fn paint( &mut self, _global_id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, bounds: Bounds, state: &mut VirtualListState, hitbox: &mut Hitbox, window: &mut Window, cx: &mut App ) { // Paint visible items for (i, _layout_id) in state.visible_item_layouts.iter().enumerate() { let item_idx = state.visible_range.start + i; let y = item_idx as f32 * self.item_height - self.scroll_offset; let item_bounds = Bounds::new( point(bounds.left(), bounds.top() + y), size(bounds.width(), self.item_height) ); if item_bounds.intersects(&bounds) { let mut item = (self.render_item)(item_idx); item.paint(item_bounds, window, cx); } } // Handle scroll window.on_mouse_event({ let hitbox = hitbox.clone(); let total_height = self.item_height * self.item_count as f32; move |event: &ScrollWheelEvent, phase, window, cx| { if hitbox.is_hovered(window) && phase.bubble() { self.scroll_offset -= event.delta.y; self.scroll_offset = self.scroll_offset .max(px(0.)) .min(total_height - self.viewport_height); cx.notify(); cx.stop_propagation(); } } }); } } // Usage: Efficiently render 10,000 items let virtual_list = VirtualList { id: ElementId::Name("large-list".into()), item_count: 10_000, item_height: px(40.), viewport_height: px(400.), scroll_offset: px(0.), render_item: Box::new(|index| { div().child(format!("Item {}", index)) }), }; ``` These advanced patterns enable sophisticated element implementations while maintaining performance and code quality. ================================================ FILE: .claude/skills/gpui-element/references/api-reference.md ================================================ # Element API Reference Complete API documentation for GPUI's low-level Element trait. ## Element Trait Structure The `Element` trait requires implementing three associated types and five methods: ```rust pub trait Element: 'static + IntoElement { type RequestLayoutState: 'static; type PrepaintState: 'static; fn id(&self) -> Option; fn source_location(&self) -> Option<&'static std::panic::Location<'static>>; fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState); fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState; fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ); } ``` ## Associated Types ### RequestLayoutState Data passed from `request_layout` to `prepaint` and `paint` phases. **Usage:** - Store layout calculations (styled text, child layout IDs) - Cache expensive computations - Pass child state between phases **Examples:** ```rust // Simple: no state needed type RequestLayoutState = (); // Single value type RequestLayoutState = StyledText; // Multiple values type RequestLayoutState = (StyledText, Vec); // Complex struct pub struct MyLayoutState { pub styled_text: StyledText, pub child_layouts: Vec<(LayoutId, ChildState)>, pub computed_bounds: Bounds, } type RequestLayoutState = MyLayoutState; ``` ### PrepaintState Data passed from `prepaint` to `paint` phase. **Usage:** - Store hitboxes for interaction - Cache visual bounds - Store prepaint results **Examples:** ```rust // Simple: just a hitbox type PrepaintState = Hitbox; // Optional hitbox type PrepaintState = Option; // Multiple values type PrepaintState = (Hitbox, Vec>); // Complex struct pub struct MyPaintState { pub hitbox: Hitbox, pub child_bounds: Vec>, pub visible_range: Range, } type PrepaintState = MyPaintState; ``` ## Methods ### id() Returns optional unique identifier for debugging and inspection. ```rust fn id(&self) -> Option { Some(self.id.clone()) } // Or if no ID needed fn id(&self) -> Option { None } ``` ### source_location() Returns source location for debugging. Usually returns `None` unless debugging is needed. ```rust fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } ``` ### request_layout() Calculates sizes and positions for the element tree. **Parameters:** - `global_id`: Global element identifier (optional) - `inspector_id`: Inspector element identifier (optional) - `window`: Mutable window reference - `cx`: Mutable app context **Returns:** - `(LayoutId, Self::RequestLayoutState)`: Layout ID and state for next phases **Responsibilities:** 1. Calculate child layouts by calling `child.request_layout()` 2. Create own layout using `window.request_layout()` 3. Return layout ID and state to pass to next phases **Example:** ```rust fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { // 1. Calculate child layouts let child_layout_id = self.child.request_layout( global_id, inspector_id, window, cx ).0; // 2. Create own layout let layout_id = window.request_layout( Style { size: size(px(200.), px(100.)), ..default() }, vec![child_layout_id], cx ); // 3. Return layout ID and state (layout_id, MyLayoutState { child_layout_id }) } ``` ### prepaint() Prepares for painting by creating hitboxes and computing final bounds. **Parameters:** - `global_id`: Global element identifier (optional) - `inspector_id`: Inspector element identifier (optional) - `bounds`: Final bounds calculated by layout engine - `request_layout`: Mutable reference to layout state - `window`: Mutable window reference - `cx`: Mutable app context **Returns:** - `Self::PrepaintState`: State for paint phase **Responsibilities:** 1. Compute final child bounds based on layout bounds 2. Call `child.prepaint()` for all children 3. Create hitboxes using `window.insert_hitbox()` 4. Return state for paint phase **Example:** ```rust fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { // 1. Compute child bounds let child_bounds = bounds; // or calculated subset // 2. Prepaint children self.child.prepaint( global_id, inspector_id, child_bounds, &mut request_layout.child_state, window, cx ); // 3. Create hitboxes let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); // 4. Return paint state MyPaintState { hitbox } } ``` ### paint() Renders the element and handles interactions. **Parameters:** - `global_id`: Global element identifier (optional) - `inspector_id`: Inspector element identifier (optional) - `bounds`: Final bounds for rendering - `request_layout`: Mutable reference to layout state - `prepaint`: Mutable reference to prepaint state - `window`: Mutable window reference - `cx`: Mutable app context **Responsibilities:** 1. Paint children first (bottom to top) 2. Paint own content (backgrounds, borders, etc.) 3. Set up interactions (mouse events, cursor styles) **Example:** ```rust fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { // 1. Paint children first self.child.paint( global_id, inspector_id, child_bounds, &mut request_layout.child_state, &mut prepaint.child_paint_state, window, cx ); // 2. Paint own content window.paint_quad(paint_quad( bounds, Corners::all(px(4.)), cx.theme().background, )); // 3. Set up interactions window.on_mouse_event({ let hitbox = prepaint.hitbox.clone(); move |event: &MouseDownEvent, phase, window, cx| { if hitbox.is_hovered(window) && phase.bubble() { // Handle click cx.stop_propagation(); } } }); window.set_cursor_style(CursorStyle::PointingHand, &prepaint.hitbox); } ``` ## IntoElement Integration Elements must also implement `IntoElement` to be used as children: ```rust impl IntoElement for MyElement { type Element = Self; fn into_element(self) -> Self::Element { self } } ``` This allows your custom element to be used directly in the element tree: ```rust div() .child(MyElement::new()) // Works because of IntoElement ``` ## Common Parameters ### Global and Inspector IDs Both are optional identifiers used for debugging and inspection: - `global_id`: Unique identifier across entire app - `inspector_id`: Identifier for dev tools/inspector Usually passed through to children without modification. ### Window and Context - `window: &mut Window`: Window-specific operations (painting, hitboxes, events) - `cx: &mut App`: App-wide operations (spawning tasks, accessing globals) ## Layout System Integration ### window.request_layout() Creates a layout node with specified style and children: ```rust let layout_id = window.request_layout( Style { size: size(px(200.), px(100.)), flex: Flex::Column, gap: px(8.), ..default() }, vec![child1_layout_id, child2_layout_id], cx ); ``` ### Bounds Represents rectangular region: ```rust pub struct Bounds { pub origin: Point, pub size: Size, } // Create bounds let bounds = Bounds::new( point(px(10.), px(20.)), size(px(100.), px(50.)) ); // Access properties bounds.left() // origin.x bounds.top() // origin.y bounds.right() // origin.x + size.width bounds.bottom() // origin.y + size.height bounds.center() // center point ``` ## Hitbox System ### Creating Hitboxes ```rust // Normal hitbox (blocks events) let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); // Transparent hitbox (passes events through) let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Transparent); ``` ### Using Hitboxes ```rust // Check if hovered if hitbox.is_hovered(window) { // ... } // Set cursor style window.set_cursor_style(CursorStyle::PointingHand, &hitbox); // Use in event handlers window.on_mouse_event(move |event, phase, window, cx| { if hitbox.is_hovered(window) && phase.bubble() { // Handle event } }); ``` ## Event Handling ### Mouse Events ```rust // Mouse down window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { if phase.bubble() && bounds.contains(&event.position) { // Handle mouse down cx.stop_propagation(); // Prevent bubbling } }); // Mouse up window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| { // Handle mouse up }); // Mouse move window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| { // Handle mouse move }); // Scroll window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { // Handle scroll }); ``` ### Event Phase Events go through two phases: - **Capture**: Top-down (parent → child) - **Bubble**: Bottom-up (child → parent) ```rust move |event, phase, window, cx| { if phase.capture() { // Handle in capture phase } else if phase.bubble() { // Handle in bubble phase } cx.stop_propagation(); // Stop event from continuing } ``` ## Cursor Styles Available cursor styles: ```rust CursorStyle::Arrow CursorStyle::IBeam // Text selection CursorStyle::PointingHand // Clickable CursorStyle::ResizeLeft CursorStyle::ResizeRight CursorStyle::ResizeUp CursorStyle::ResizeDown CursorStyle::ResizeLeftRight CursorStyle::ResizeUpDown CursorStyle::Crosshair CursorStyle::OperationNotAllowed ``` Usage: ```rust window.set_cursor_style(CursorStyle::PointingHand, &hitbox); ``` ================================================ FILE: .claude/skills/gpui-element/references/best-practices.md ================================================ # Element Best Practices Guidelines and best practices for implementing high-quality GPUI elements. ## State Management ### Using Associated Types Effectively **Good:** Use associated types to pass meaningful data between phases ```rust // Good: Structured state with type safety type RequestLayoutState = (StyledText, Vec); type PrepaintState = (Hitbox, Vec); ``` **Bad:** Using empty state when you need data ```rust // Bad: No state when you need to pass data type RequestLayoutState = (); type PrepaintState = (); // Now you can't pass layout info to paint phase! ``` ### Managing Complex State For elements with complex state, create dedicated structs: ```rust // Good: Dedicated struct for complex state pub struct TextElementState { pub styled_text: StyledText, pub text_layout: TextLayout, pub child_states: Vec, } type RequestLayoutState = TextElementState; ``` **Benefits:** - Clear documentation of state structure - Easy to extend - Type-safe access ### State Lifecycle **Golden Rule:** State flows in one direction through the phases ``` request_layout → RequestLayoutState → prepaint → PrepaintState → paint ``` **Don't:** - Store state in the element struct that should be in associated types - Try to mutate element state in paint phase (use `cx.notify()` to schedule re-render) - Pass mutable references across phase boundaries ## Performance Considerations ### Minimize Allocations in Paint Phase **Critical:** Paint phase is called every frame during animations. Minimize allocations. **Good:** Pre-allocate in `request_layout` or `prepaint` ```rust impl Element for MyElement { fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, Vec) { // Allocate once during layout let styled_texts = self.children .iter() .map(|child| StyledText::new(child.text.clone())) .collect(); (layout_id, styled_texts) } fn paint(&mut self, .., styled_texts: &mut Vec, ..) { // Just use pre-allocated styled_texts for text in styled_texts { text.paint(..); } } } ``` **Bad:** Allocate in `paint` phase ```rust fn paint(&mut self, ..) { // Bad: Allocation in paint phase! let styled_texts: Vec<_> = self.children .iter() .map(|child| StyledText::new(child.text.clone())) .collect(); } ``` ### Cache Expensive Computations Use memoization for expensive operations: ```rust pub struct CachedElement { // Cache key last_text: Option, last_width: Option, // Cached result cached_layout: Option, } impl Element for CachedElement { fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, TextLayout) { let current_width = window.bounds().width(); // Check if cache is valid if self.last_text.as_ref() != Some(&self.text) || self.last_width != Some(current_width) || self.cached_layout.is_none() { // Recompute expensive layout self.cached_layout = Some(self.compute_text_layout(current_width)); self.last_text = Some(self.text.clone()); self.last_width = Some(current_width); } // Use cached layout let layout = self.cached_layout.as_ref().unwrap(); (layout_id, layout.clone()) } } ``` ### Lazy Child Rendering Only render visible children in scrollable containers: ```rust fn paint(&mut self, .., bounds: Bounds, paint_state: &mut Self::PrepaintState, ..) { for (i, child) in self.children.iter_mut().enumerate() { let child_bounds = paint_state.child_bounds[i]; // Only paint visible children if self.is_visible(&child_bounds, &bounds) { child.paint(..); } } } fn is_visible(&self, child_bounds: &Bounds, container_bounds: &Bounds) -> bool { child_bounds.bottom() >= container_bounds.top() && child_bounds.top() <= container_bounds.bottom() } ``` ## Interaction Handling ### Proper Event Bubbling Always check phase and bounds before handling events: ```rust fn paint(&mut self, .., window: &mut Window, cx: &mut App) { window.on_mouse_event({ let hitbox = self.hitbox.clone(); move |event: &MouseDownEvent, phase, window, cx| { // Check phase first if !phase.bubble() { return; } // Check if event is within bounds if !hitbox.is_hovered(window) { return; } // Handle event self.handle_click(event); // Stop propagation if handled cx.stop_propagation(); } }); } ``` **Don't forget:** - Check `phase.bubble()` or `phase.capture()` as appropriate - Check hitbox hover state or bounds - Call `cx.stop_propagation()` if you handle the event ### Hitbox Management Create hitboxes in `prepaint` phase, not `paint`: **Good:** ```rust fn prepaint(&mut self, .., bounds: Bounds, window: &mut Window, ..) -> Hitbox { // Create hitbox in prepaint window.insert_hitbox(bounds, HitboxBehavior::Normal) } fn paint(&mut self, .., hitbox: &mut Hitbox, window: &mut Window, ..) { // Use hitbox in paint window.set_cursor_style(CursorStyle::PointingHand, hitbox); } ``` **Hitbox Behaviors:** ```rust // Normal: Blocks events from passing through HitboxBehavior::Normal // Transparent: Allows events to pass through to elements below HitboxBehavior::Transparent ``` ### Cursor Style Guidelines Set appropriate cursor styles for interactivity cues: ```rust // Text selection window.set_cursor_style(CursorStyle::IBeam, &hitbox); // Clickable elements (desktop convention: use default, not pointing hand) window.set_cursor_style(CursorStyle::Arrow, &hitbox); // Links (web convention: use pointing hand) window.set_cursor_style(CursorStyle::PointingHand, &hitbox); // Resizable edges window.set_cursor_style(CursorStyle::ResizeLeftRight, &hitbox); ``` **Desktop vs Web Convention:** - Desktop apps: Use `Arrow` for buttons - Web apps: Use `PointingHand` for links only ## Layout Strategies ### Fixed Size Elements For elements with known, unchanging size: ```rust fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, ()) { let layout_id = window.request_layout( Style { size: size(px(200.), px(100.)), ..default() }, vec![], // No children cx ); (layout_id, ()) } ``` ### Content-Based Sizing For elements sized by their content: ```rust fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, Size) { // Measure content let text_bounds = self.measure_text(window); let padding = px(16.); let layout_id = window.request_layout( Style { size: size( text_bounds.width() + padding * 2., text_bounds.height() + padding * 2., ), ..default() }, vec![], cx ); (layout_id, text_bounds) } ``` ### Flexible Layouts For elements that adapt to available space: ```rust fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, Vec) { let mut child_layout_ids = Vec::new(); for child in &mut self.children { let (layout_id, _) = child.request_layout(window, cx); child_layout_ids.push(layout_id); } let layout_id = window.request_layout( Style { flex_direction: FlexDirection::Row, gap: px(8.), size: Size { width: relative(1.0), // Fill parent width height: auto(), // Auto height }, ..default() }, child_layout_ids.clone(), cx ); (layout_id, child_layout_ids) } ``` ## Error Handling ### Graceful Degradation Handle errors gracefully, don't panic: ```rust fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, Option) { // Try to create styled text match StyledText::new(self.text.clone()).request_layout(None, None, window, cx) { Ok((layout_id, text_layout)) => { (layout_id, Some(text_layout)) } Err(e) => { // Log error eprintln!("Failed to layout text: {}", e); // Fallback to simple text let fallback_text = StyledText::new("(Error loading text)".into()); let (layout_id, _) = fallback_text.request_layout(None, None, window, cx); (layout_id, None) } } } ``` ### Defensive Bounds Checking Always validate bounds and indices: ```rust fn paint_selection(&self, selection: &Selection, text_layout: &TextLayout, ..) { // Validate selection bounds let start = selection.start.min(self.text.len()); let end = selection.end.min(self.text.len()); if start >= end { return; // Invalid selection } let rects = text_layout.rects_for_range(start..end); // Paint selection... } ``` ## Testing Element Implementations ### Layout Tests Test that layout calculations are correct: ```rust #[cfg(test)] mod tests { use super::*; use gpui::TestAppContext; #[gpui::test] fn test_element_layout(cx: &mut TestAppContext) { cx.update(|cx| { let mut window = cx.open_window(Default::default(), |_, _| ()).unwrap(); window.update(cx, |window, cx| { let mut element = MyElement::new(); let (layout_id, layout_state) = element.request_layout( None, None, window, cx ); // Assert layout properties let bounds = window.layout_bounds(layout_id); assert_eq!(bounds.size.width, px(200.)); assert_eq!(bounds.size.height, px(100.)); }); }); } } ``` ### Interaction Tests Test that interactions work correctly: ```rust #[gpui::test] fn test_element_click(cx: &mut TestAppContext) { cx.update(|cx| { let mut window = cx.open_window(Default::default(), |_, cx| { cx.new(|_| MyElement::new()) }).unwrap(); window.update(cx, |window, cx| { let view = window.root_view().unwrap(); // Simulate click let position = point(px(10.), px(10.)); window.dispatch_event(MouseDownEvent { position, button: MouseButton::Left, modifiers: Modifiers::default(), }); // Assert element responded view.read(cx).assert_clicked(); }); }); } ``` ## Common Pitfalls ### ❌ Storing Layout State in Element Struct **Bad:** ```rust pub struct MyElement { id: ElementId, // Bad: This should be in RequestLayoutState cached_layout: Option, } ``` **Good:** ```rust pub struct MyElement { id: ElementId, text: SharedString, } type RequestLayoutState = TextLayout; // Good: State in associated type ``` ### ❌ Mutating Element in Paint Phase **Bad:** ```rust fn paint(&mut self, ..) { self.counter += 1; // Bad: Mutating element in paint } ``` **Good:** ```rust fn paint(&mut self, .., window: &mut Window, cx: &mut App) { window.on_mouse_event(move |event, phase, window, cx| { if phase.bubble() { self.counter += 1; cx.notify(); // Schedule re-render } }); } ``` ### ❌ Creating Hitboxes in Paint Phase **Bad:** ```rust fn paint(&mut self, .., bounds: Bounds, window: &mut Window, ..) { // Bad: Creating hitbox in paint let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); } ``` **Good:** ```rust fn prepaint(&mut self, .., bounds: Bounds, window: &mut Window, ..) -> Hitbox { // Good: Creating hitbox in prepaint window.insert_hitbox(bounds, HitboxBehavior::Normal) } ``` ### ❌ Ignoring Event Phase **Bad:** ```rust window.on_mouse_event(move |event, phase, window, cx| { // Bad: Not checking phase self.handle_click(event); }); ``` **Good:** ```rust window.on_mouse_event(move |event, phase, window, cx| { // Good: Checking phase if !phase.bubble() { return; } self.handle_click(event); }); ``` ## Performance Checklist Before shipping an element implementation, verify: - [ ] No allocations in `paint` phase (except event handlers) - [ ] Expensive computations are cached/memoized - [ ] Only visible children are rendered in scrollable containers - [ ] Hitboxes created in `prepaint`, not `paint` - [ ] Event handlers check phase and bounds - [ ] Layout state is passed through associated types, not stored in element - [ ] Element implements proper error handling with fallbacks - [ ] Tests cover layout calculations and interactions ================================================ FILE: .claude/skills/gpui-element/references/examples.md ================================================ # Element Implementation Examples Complete examples of implementing custom elements for various scenarios. ## Table of Contents 1. [Simple Text Element](#simple-text-element) 2. [Interactive Element with Selection](#interactive-element-with-selection) 3. [Complex Element with Child Management](#complex-element-with-child-management) ## Simple Text Element A basic text element with syntax highlighting support. ```rust pub struct SimpleText { id: ElementId, text: SharedString, highlights: Vec<(Range, HighlightStyle)>, } impl IntoElement for SimpleText { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for SimpleText { type RequestLayoutState = StyledText; type PrepaintState = Hitbox; fn id(&self) -> Option { Some(self.id.clone()) } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App ) -> (LayoutId, Self::RequestLayoutState) { // Create styled text with highlights let mut runs = Vec::new(); let mut ix = 0; for (range, highlight) in &self.highlights { // Add unstyled text before highlight if ix < range.start { runs.push(window.text_style().to_run(range.start - ix)); } // Add highlighted text runs.push( window.text_style() .highlight(*highlight) .to_run(range.len()) ); ix = range.end; } // Add remaining unstyled text if ix < self.text.len() { runs.push(window.text_style().to_run(self.text.len() - ix)); } let styled_text = StyledText::new(self.text.clone()).with_runs(runs); let (layout_id, _) = styled_text.request_layout( global_id, inspector_id, window, cx ); (layout_id, styled_text) } fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, styled_text: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App ) -> Self::PrepaintState { // Prepaint the styled text styled_text.prepaint( global_id, inspector_id, bounds, &mut (), window, cx ); // Create hitbox for interaction let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); hitbox } fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, styled_text: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, window: &mut Window, cx: &mut App ) { // Paint the styled text styled_text.paint( global_id, inspector_id, bounds, &mut (), &mut (), window, cx ); // Set cursor style for text window.set_cursor_style(CursorStyle::IBeam, hitbox); } } ``` ## Interactive Element with Selection A text element that supports text selection via mouse interaction. ```rust #[derive(Clone)] pub struct Selection { pub start: usize, pub end: usize, } pub struct SelectableText { id: ElementId, text: SharedString, selectable: bool, selection: Option, } impl IntoElement for SelectableText { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for SelectableText { type RequestLayoutState = TextLayout; type PrepaintState = Option; fn id(&self) -> Option { Some(self.id.clone()) } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App ) -> (LayoutId, Self::RequestLayoutState) { let styled_text = StyledText::new(self.text.clone()); let (layout_id, _) = styled_text.request_layout( global_id, inspector_id, window, cx ); // Extract text layout for selection painting let text_layout = styled_text.layout().clone(); (layout_id, text_layout) } fn prepaint( &mut self, _global_id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, bounds: Bounds, _text_layout: &mut Self::RequestLayoutState, window: &mut Window, _cx: &mut App ) -> Self::PrepaintState { // Only create hitbox if selectable if self.selectable { Some(window.insert_hitbox(bounds, HitboxBehavior::Normal)) } else { None } } fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, text_layout: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, window: &mut Window, cx: &mut App ) { // Paint text let styled_text = StyledText::new(self.text.clone()); styled_text.paint( global_id, inspector_id, bounds, &mut (), &mut (), window, cx ); // Paint selection if any if let Some(selection) = &self.selection { Self::paint_selection(selection, text_layout, &bounds, window, cx); } // Handle mouse events for selection if let Some(hitbox) = hitbox { window.set_cursor_style(CursorStyle::IBeam, hitbox); // Mouse down to start selection window.on_mouse_event({ let bounds = bounds.clone(); move |event: &MouseDownEvent, phase, window, cx| { if bounds.contains(&event.position) && phase.bubble() { // Start selection at mouse position let char_index = Self::position_to_index( event.position, &bounds, text_layout ); self.selection = Some(Selection { start: char_index, end: char_index, }); cx.notify(); cx.stop_propagation(); } } }); // Mouse drag to extend selection window.on_mouse_event({ let bounds = bounds.clone(); move |event: &MouseMoveEvent, phase, window, cx| { if let Some(selection) = &mut self.selection { if phase.bubble() { let char_index = Self::position_to_index( event.position, &bounds, text_layout ); selection.end = char_index; cx.notify(); } } } }); } } } impl SelectableText { fn paint_selection( selection: &Selection, text_layout: &TextLayout, bounds: &Bounds, window: &mut Window, cx: &mut App ) { // Calculate selection bounds from text layout let selection_rects = text_layout.rects_for_range( selection.start..selection.end ); // Paint selection background for rect in selection_rects { window.paint_quad(paint_quad( Bounds::new( point(bounds.left() + rect.origin.x, bounds.top() + rect.origin.y), rect.size ), Corners::default(), cx.theme().selection_background, )); } } fn position_to_index( position: Point, bounds: &Bounds, text_layout: &TextLayout ) -> usize { // Convert screen position to character index let relative_pos = point( position.x - bounds.left(), position.y - bounds.top() ); text_layout.index_for_position(relative_pos) } } ``` ## Complex Element with Child Management A container element that manages multiple children with scrolling support. ```rust pub struct ComplexElement { id: ElementId, children: Vec>>, scrollable: bool, scroll_offset: Point, } struct ComplexLayoutState { child_layouts: Vec, total_height: Pixels, } struct ComplexPaintState { child_bounds: Vec>, hitbox: Hitbox, } impl IntoElement for ComplexElement { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for ComplexElement { type RequestLayoutState = ComplexLayoutState; type PrepaintState = ComplexPaintState; fn id(&self) -> Option { Some(self.id.clone()) } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App ) -> (LayoutId, Self::RequestLayoutState) { let mut child_layouts = Vec::new(); let mut total_height = px(0.); // Request layout for all children for child in &mut self.children { let (child_layout_id, _) = child.request_layout( global_id, inspector_id, window, cx ); child_layouts.push(child_layout_id); // Get child size from layout let child_size = window.layout_bounds(child_layout_id).size(); total_height += child_size.height; } // Create container layout let layout_id = window.request_layout( Style { flex_direction: FlexDirection::Column, gap: px(8.), size: Size { width: relative(1.0), height: if self.scrollable { // Fixed height for scrollable px(400.) } else { // Auto height for non-scrollable total_height }, }, ..default() }, child_layouts.clone(), cx ); (layout_id, ComplexLayoutState { child_layouts, total_height, }) } fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, layout_state: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App ) -> Self::PrepaintState { let mut child_bounds = Vec::new(); let mut y_offset = self.scroll_offset.y; // Calculate child bounds and prepaint children for (child, layout_id) in self.children.iter_mut() .zip(&layout_state.child_layouts) { let child_size = window.layout_bounds(*layout_id).size(); let child_bound = Bounds::new( point(bounds.left(), bounds.top() + y_offset), child_size ); // Only prepaint visible children if self.is_visible(&child_bound, &bounds) { child.prepaint( global_id, inspector_id, child_bound, &mut (), window, cx ); } child_bounds.push(child_bound); y_offset += child_size.height + px(8.); // gap } let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); ComplexPaintState { child_bounds, hitbox, } } fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, layout_state: &mut Self::RequestLayoutState, paint_state: &mut Self::PrepaintState, window: &mut Window, cx: &mut App ) { // Paint background window.paint_quad(paint_quad( bounds, Corners::all(px(4.)), cx.theme().background, )); // Paint visible children only for (i, child) in self.children.iter_mut().enumerate() { let child_bounds = paint_state.child_bounds[i]; if self.is_visible(&child_bounds, &bounds) { child.paint( global_id, inspector_id, child_bounds, &mut (), &mut (), window, cx ); } } // Paint scrollbar if scrollable if self.scrollable { self.paint_scrollbar(bounds, layout_state, window, cx); } // Handle scroll events if self.scrollable { window.on_mouse_event({ let hitbox = paint_state.hitbox.clone(); let total_height = layout_state.total_height; let visible_height = bounds.size.height; move |event: &ScrollWheelEvent, phase, window, cx| { if hitbox.is_hovered(window) && phase.bubble() { // Update scroll offset self.scroll_offset.y -= event.delta.y; // Clamp scroll offset let max_scroll = (total_height - visible_height).max(px(0.)); self.scroll_offset.y = self.scroll_offset.y .max(px(0.)) .min(max_scroll); cx.notify(); cx.stop_propagation(); } } }); } } } impl ComplexElement { fn is_visible(&self, child_bounds: &Bounds, container_bounds: &Bounds) -> bool { // Check if child is within visible area child_bounds.bottom() >= container_bounds.top() && child_bounds.top() <= container_bounds.bottom() } fn paint_scrollbar( &self, bounds: Bounds, layout_state: &ComplexLayoutState, window: &mut Window, cx: &mut App ) { let scrollbar_width = px(8.); let visible_height = bounds.size.height; let total_height = layout_state.total_height; if total_height <= visible_height { return; // No need for scrollbar } // Calculate scrollbar position and size let scroll_ratio = self.scroll_offset.y / (total_height - visible_height); let thumb_height = (visible_height / total_height) * visible_height; let thumb_y = scroll_ratio * (visible_height - thumb_height); // Paint scrollbar track let track_bounds = Bounds::new( point(bounds.right() - scrollbar_width, bounds.top()), size(scrollbar_width, visible_height) ); window.paint_quad(paint_quad( track_bounds, Corners::default(), cx.theme().scrollbar_track, )); // Paint scrollbar thumb let thumb_bounds = Bounds::new( point(bounds.right() - scrollbar_width, bounds.top() + thumb_y), size(scrollbar_width, thumb_height) ); window.paint_quad(paint_quad( thumb_bounds, Corners::all(px(4.)), cx.theme().scrollbar_thumb, )); } } ``` ## Usage Examples ### Using SimpleText ```rust fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .child(SimpleText { id: ElementId::Name("code-text".into()), text: "fn main() { println!(\"Hello\"); }".into(), highlights: vec![ (0..2, HighlightStyle::keyword()), (3..7, HighlightStyle::function()), ], }) } ``` ### Using SelectableText ```rust fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .child(SelectableText { id: ElementId::Name("selectable-text".into()), text: "Select this text with your mouse".into(), selectable: true, selection: self.current_selection.clone(), }) } ``` ### Using ComplexElement ```rust fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let children: Vec>> = self.items .iter() .map(|item| Box::new(div().child(item.name.clone())) as Box<_>) .collect(); div() .child(ComplexElement { id: ElementId::Name("scrollable-list".into()), children, scrollable: true, scroll_offset: self.scroll_offset, }) } ``` ================================================ FILE: .claude/skills/gpui-element/references/patterns.md ================================================ # Common Element Patterns Reusable patterns for implementing common element types in GPUI. ## Text Rendering Elements Elements that display and manipulate text content. ### Pattern Characteristics - Use `StyledText` for text layout and rendering - Handle text selection in `paint` phase with hitbox interaction - Create hitboxes for text interaction in `prepaint` - Support text highlighting and custom styling via runs ### Implementation Template ```rust pub struct TextElement { id: ElementId, text: SharedString, style: TextStyle, } impl Element for TextElement { type RequestLayoutState = StyledText; type PrepaintState = Hitbox; fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, StyledText) { let styled_text = StyledText::new(self.text.clone()) .with_style(self.style); let (layout_id, _) = styled_text.request_layout(None, None, window, cx); (layout_id, styled_text) } fn prepaint(&mut self, .., bounds: Bounds, styled_text: &mut StyledText, window: &mut Window, cx: &mut App) -> Hitbox { styled_text.prepaint(None, None, bounds, &mut (), window, cx); window.insert_hitbox(bounds, HitboxBehavior::Normal) } fn paint(&mut self, .., bounds: Bounds, styled_text: &mut StyledText, hitbox: &mut Hitbox, window: &mut Window, cx: &mut App) { styled_text.paint(None, None, bounds, &mut (), &mut (), window, cx); window.set_cursor_style(CursorStyle::IBeam, hitbox); } } ``` ### Use Cases - Code editors with syntax highlighting - Rich text displays - Labels with custom formatting - Selectable text areas ## Container Elements Elements that manage and layout child elements. ### Pattern Characteristics - Manage child element layouts and positions - Handle scrolling and clipping when needed - Implement flex/grid-like layouts - Coordinate child interactions and event delegation ### Implementation Template ```rust pub struct ContainerElement { id: ElementId, children: Vec, direction: FlexDirection, gap: Pixels, } impl Element for ContainerElement { type RequestLayoutState = Vec; type PrepaintState = Vec>; fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, Vec) { let child_layout_ids: Vec<_> = self.children .iter_mut() .map(|child| child.request_layout(window, cx).0) .collect(); let layout_id = window.request_layout( Style { flex_direction: self.direction, gap: self.gap, ..default() }, child_layout_ids.clone(), cx ); (layout_id, child_layout_ids) } fn prepaint(&mut self, .., bounds: Bounds, layout_ids: &mut Vec, window: &mut Window, cx: &mut App) -> Vec> { let mut child_bounds = Vec::new(); for (child, layout_id) in self.children.iter_mut().zip(layout_ids.iter()) { let child_bound = window.layout_bounds(*layout_id); child.prepaint(child_bound, window, cx); child_bounds.push(child_bound); } child_bounds } fn paint(&mut self, .., child_bounds: &mut Vec>, window: &mut Window, cx: &mut App) { for (child, bounds) in self.children.iter_mut().zip(child_bounds.iter()) { child.paint(*bounds, window, cx); } } } ``` ### Use Cases - Panels and split views - List containers - Grid layouts - Tab containers ## Interactive Elements Elements that respond to user input (mouse, keyboard, touch). ### Pattern Characteristics - Create appropriate hitboxes for interaction areas - Handle mouse/keyboard/touch events properly - Manage focus and cursor styles - Support hover, active, and disabled states ### Implementation Template ```rust pub struct InteractiveElement { id: ElementId, content: AnyElement, on_click: Option>, hover_style: Option

{formatGreeting(title)}

Current year: {currentYear}

Environment: {isProduction ? 'Production' : 'Development'}

{ count > 0 && (

You have {count} {count === 1 ? 'item' : 'items'}

) }
    { items.map((item, index) => (
  • {item}
  • )) }

{items.length > 0 ? `Found ${items.length} technologies` : 'No technologies found'}

5 }]} >

Interactive card with dynamic attributes

================================================ FILE: crates/story/examples/fixtures/test.c ================================================ #include #include #include #include #define MAX_NAME_LENGTH 100 #define BUFFER_SIZE 1024 /* Constants for configuration limits */ #define MIN_TIMEOUT 1000 #define MAX_TIMEOUT 10000 #define MAX_RETRIES 5 /** * HelloWorld structure represents a greeter object with configuration * Contains: * - name: String identifier for the greeter (max 100 chars) * - created_at: Timestamp when instance was created * - timeout: Milliseconds to wait between greetings (1000-10000) * - retries: Number of retry attempts (0-5) */ typedef struct { char name[MAX_NAME_LENGTH]; time_t created_at; int timeout; int retries; } HelloWorld; HelloWorld* hello_world_create(const char* name) { HelloWorld* hw = (HelloWorld*)malloc(sizeof(HelloWorld)); if (!hw) return NULL; strncpy(hw->name, name, MAX_NAME_LENGTH - 1); hw->name[MAX_NAME_LENGTH - 1] = '\0'; hw->created_at = time(NULL); hw->timeout = 5000; hw->retries = 3; return hw; } void hello_world_destroy(HelloWorld* hw) { if (hw) { free(hw); } } void hello_world_greet(HelloWorld* hw, const char** names, int count) { for (int i = 0; i < count; i++) { printf("Hello, %s from %s!\n", names[i], hw->name); } } void hello_world_configure(HelloWorld* hw, int timeout, int retries) { hw->timeout = timeout; hw->retries = retries; } char* hello_world_generate_report(const HelloWorld* hw) { char* report = (char*)malloc(BUFFER_SIZE); char time_str[26]; ctime_r(&hw->created_at, time_str); time_str[24] = '\0'; snprintf(report, BUFFER_SIZE, "HelloWorld Report\n" "================\n" "Name: %s\n" "Created: %s\n" "Timeout: %d\n" "Retries: %d\n", hw->name, time_str, hw->timeout, hw->retries); return report; } int main() { HelloWorld* greeter = hello_world_create("C Example"); const char* names[] = {"Alice", "Bob"}; int names_count = sizeof(names) / sizeof(names[0]); hello_world_configure(greeter, 1000, 5); hello_world_greet(greeter, names, names_count); char* report = hello_world_generate_report(greeter); printf("%s\n", report); free(report); hello_world_destroy(greeter); return 0; } ================================================ FILE: crates/story/examples/fixtures/test.go ================================================ package main import ( "context" "encoding/json" "fmt" "sync" "time" ) // Default timeout duration for operations const timeout = 5 * time.Second var ( instanceCount int mu sync.RWMutex ) /** * HelloWorld represents a greeter with configuration options * Contains: * - name: String identifier for the greeter * - createdAt: Timestamp when instance was created * - options: Map of configuration options */ type HelloWorld struct { name string createdAt time.Time options map[string]interface{} } type Config struct { Timeout time.Duration `json:"timeout"` Retries int `json:"retries"` Debug bool `json:"debug"` } func NewHelloWorld(name string) *HelloWorld { mu.Lock() instanceCount++ mu.Unlock() return &HelloWorld{ name: name, createdAt: time.Now(), options: make(map[string]interface{}), } } func (h *HelloWorld) Greet(ctx context.Context, names ...string) error { for _, name := range names { select { case <-ctx.Done(): return ctx.Err() default: fmt.Printf("Hello, %s!\n", name) } } return nil } func (h *HelloWorld) Configure(cfg Config) { h.options["timeout"] = cfg.Timeout h.options["retries"] = cfg.Retries h.options["debug"] = cfg.Debug } func (h *HelloWorld) generateReport() string { data, _ := json.MarshalIndent(h.options, "", " ") return fmt.Sprintf(` HelloWorld Report ================ Name: %s Created: %s Options: %s `, h.name, h.createdAt.Format(time.RFC3339), string(data)) } func main() { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() greeter := NewHelloWorld("Go") greeter.Configure(Config{ Timeout: timeout, Retries: 3, Debug: true, }) if err := greeter.Greet(ctx, "Alice", "Bob"); err != nil { fmt.Printf("Error greeting: %v\n", err) } fmt.Println(greeter.generateReport()) } ================================================ FILE: crates/story/examples/fixtures/test.html ================================================

A simple HTML document

Here is a test in div.

This is a paragraph inside a div element, have @Mention Tag Link with: Bold italic, bold, italic, and code text.

This is a text before blockquote.

This is before paragraph in blockquote.

This is first blockquote paragraph.

This is second level

This is third level

  • List item in a blockquote.
  • Second list item
This is after paragraph in blockquote.

This is second paragraph.

A text after div.

这是一个中文演示段落,用于展示更多的 Markdown GFM 内容。これは日本語のデモ段落です。の多言語サポートを示すためのテキストが含まれています。

List

Example for Bulleted and Numbered List:

Numbered List

Text before the Numbered List.
  1. Numbered item 1
    1. Sub item 1
    2. Sub item 2
  2. Numbered item 2
  3. Numbered item 3
Text after the Numbered List.

Bulleted List

Text before the Bulleted List.
  • Bullet 1
    1. Sub Numbered 1
    2. Sub Numbered 2
  • Bullet 2
Text after the Bulleted List.
Text before the section.

Table

Text before the table.
Head 1 Head 2
This Cell have 2 span
Cell 3 Cell 4
Text after the table.
Text after the section.

Images

(A Tesla Model X on display at the June 2024 Shanghai new energy vehicle show. Image credit: CnEVPost)

Text before the image. Text after the image.
================================================ FILE: crates/story/examples/fixtures/test.js ================================================ const fs = require("fs"); const getName = function () { return "John Doe"; }; /** * A class representing a HelloWorld greeter with various utility methods * @class HelloWorld * @param {string} name - The name to use for greetings */ class HelloWorld { // Version number of the HelloWorld class static VERSION = "1.0.0"; // Counter to track number of class instances static #instanceCount = 0; // Private instance fields #name; #options; #createdAt; /** * Creates a new HelloWorld instance * @param {string} name - The name to use for greetings * @param {Object} options - Configuration options */ constructor(name = "World", options = {}) { this.#name = name; this.#options = options; this.#createdAt = new Date(); HelloWorld.#instanceCount++; } static getInstanceCount() { return HelloWorld.#instanceCount; } get name() { return this.#name; } set name(value) { this.#name = value; } async greet(...names) { try { for (const name of names) { await new Promise((resolve) => setTimeout(resolve, 100)); console.log(`Hello, ${name}!`); } } catch (error) { console.error(`Error: ${error.message}`); } } configure(options = {}) { Object.assign(this.#options, options); } *generateSequence(start = 0, end = 10) { for (let i = start; i <= end; i++) yield i; } processNames(names) { return names .filter((name) => name.length > 0) .map((name) => name.toUpperCase()) .sort(); } } const greeter = new HelloWorld("JavaScript"); (async () => { const uniqueNames = new Set(["Alice", "Bob"]); await greeter.greet(...uniqueNames); for (const num of greeter.generateSequence(0, 5)) { console.log(num); } })(); ================================================ FILE: crates/story/examples/fixtures/test.json ================================================ [ { "name": "GPUI Component", "description": "UI components for building fantastic desktop application by using GPUI.", "license": "Apache-2.0", "keywords": ["UI", "desktop", "application"], "stars": 3000, "rgb": "rgb(100, 200, 100)", "hsla": "hsla(20, 100%, 50%, .5)", "hsl": "hsl(225, 100%, 70%)", "中文": "#EEAAFF", "public": true, "repository": "https://github.com/longbridge/gpui-component" } ] ================================================ FILE: crates/story/examples/fixtures/test.kt ================================================ package hello import kotlin.math.roundToInt const val VERSION = "1.0.0" data class Config( val timeout: Int = 5000, val retries: Int = 3, val debug: Boolean = false, ) enum class LogLevel(val label: String) { INFO("INFO"), WARN("WARN"), ERROR("ERROR"); companion object { fun fromString(s: String): LogLevel? = entries.find { it.label == s } } } sealed class Result { data class Success(val data: T) : Result() data class Error(val message: String) : Result() } interface Greetable { fun greet(vararg names: String): List } class HelloWorld(private val name: String = "World") : Greetable { private var config = Config() private val createdAt = System.currentTimeMillis() override fun greet(vararg names: String): List { return names.map { "Hello, $it!" }.also { lines -> if (config.debug) lines.forEach { println(" [debug] $it") } } } fun configure(block: Config.() -> Config) { config = config.block() } fun processNames(names: List?): List = names?.filter { it.isNotBlank() } ?.map { it.uppercase() } ?.sorted() ?: emptyList() fun generateReport(): String { val elapsed = ((System.currentTimeMillis() - createdAt) / 1000.0).roundToInt() return """ |HelloWorld Report |================= |Name: $name |Elapsed: ${elapsed}s |Config: timeout=${config.timeout}, retries=${config.retries} """.trimMargin() } override fun toString(): String = "HelloWorld(name=$name)" } fun safely(block: () -> T): Result = try { Result.Success(block()) } catch (e: Exception) { Result.Error(e.message ?: "unknown error") } fun describe(obj: Any?): String = when (obj) { null -> "null" is String -> "String(${obj.length}): \"${obj.take(20)}\"" is Result.Success<*> -> "ok: ${obj.data}" is Result.Error -> "err: ${obj.message}" else -> obj::class.simpleName ?: "unknown" } fun main() { val greeter = HelloWorld("Kotlin") greeter.configure { copy(debug = true, retries = 5) } greeter.greet("Alice", "Bob", "Charlie") val processed = greeter.processNames(listOf("alice", "", "bob")) println("Processed: $processed") val result = safely { greeter.generateReport() } when (result) { is Result.Success -> println(result.data) is Result.Error -> println("Failed: ${result.message}") } // Null safety & describe val items: List = listOf("hello", 42, null, result) items.forEach { println(" ${describe(it)}") } println("Instances: $greeter (v$VERSION)") } ================================================ FILE: crates/story/examples/fixtures/test.lua ================================================ --[[ HelloWorld module demonstrating Lua syntax highlighting Version: 1.0.0 --]] local VERSION = "1.0.0" -- Module configuration with default values local Config = { timeout = 5000, retries = 3, debug = false } -- Enum-like table for log levels local LogLevel = { INFO = "INFO", WARN = "WARN", ERROR = "ERROR" } -- Create a class using metatables local HelloWorld = {} HelloWorld.__index = HelloWorld function HelloWorld.new(name) local self = setmetatable({}, HelloWorld) self.name = name or "World" self.config = {timeout = 5000, retries = 3, debug = false} self.createdAt = os.time() self._greetCount = 0 -- private-like field return self end -- Method using colon syntax (implicit self) function HelloWorld:greet(...) local names = {...} local results = {} for i, name in ipairs(names) do local greeting = string.format("Hello, %s!", name) table.insert(results, greeting) self._greetCount = self._greetCount + 1 if self.config.debug then print(string.format(" [debug] %s", greeting)) end end return results end function HelloWorld:configure(newConfig) for k, v in pairs(newConfig) do self.config[k] = v end end function HelloWorld:processNames(names) local processed = {} for _, name in ipairs(names or {}) do if name ~= "" and name:match("%S") then table.insert(processed, name:upper()) end end table.sort(processed) return processed end function HelloWorld:generateReport() local elapsed = os.time() - self.createdAt local lines = { "HelloWorld Report", "=================", string.format("Name: %s", self.name), string.format("Elapsed: %ds", elapsed), string.format("Greetings: %d", self._greetCount), string.format("Config: timeout=%d, retries=%d", self.config.timeout, self.config.retries) } return table.concat(lines, "\n") end -- Metatable for Result type (Success/Error) local function Success(data) return {success = true, data = data} end local function Error(message) return {success = false, message = message} end -- Protected call wrapper local function safely(fn) local status, result = pcall(fn) return status and Success(result) or Error(tostring(result)) end -- Pattern matching-like function local function describe(obj) if obj == nil then return "nil" elseif type(obj) == "string" then return string.format('String(%d): "%s"', #obj, obj:sub(1, 20)) elseif type(obj) == "table" and obj.success ~= nil then return obj.success and ("ok: " .. tostring(obj.data)) or ("err: " .. obj.message) else return type(obj) end end -- Main execution local greeter = HelloWorld.new("Lua") greeter:configure({debug = true, retries = 5}) greeter:greet("Alice", "Bob", "Charlie") local processed = greeter:processNames({"alice", "", "bob", " charlie "}) print("Processed: " .. table.concat(processed, ", ")) local result = safely(function() return greeter:generateReport() end) if result.success then print(result.data) else print("Failed: " .. result.message) end print(string.format("\nVersion: %s", VERSION)) ================================================ FILE: crates/story/examples/fixtures/test.md ================================================ # Hello, **World**! Build Status [![Build Status](https://github.com/longbridge/gpui-component/actions/workflows/ci.yml/badge.svg)](https://github.com/longbridge/gpui-component/actions/workflows/ci.yml) of [GPUI Component](https://github.com/longbridge/gpui-component). This is first paragraph, there have **BOLD**, _italic_, and ~strikethrough~, `code` text [^1] [^2]. This is an additional demonstration paragraph in English demonstrating more content for [Markdown GFM]. It includes various stylistic elements and plain text. ![Img](https://miro.medium.com/v2/resize:fit:1400/format:webp/1*WgEz5f3n3lD7MfC7NeQGOA.jpeg) 这是一个中文演示段落,用于展示更多的 [Markdown GFM] 内容。您可以在此尝试使用使用**粗体**、*斜体*和`代码`等样式。これは日本語のデモ段落です。Markdown の多言語サポートを示すためのテキストが含まれています。例えば、、**ボールド**、_イタリック_、および`コード`のスタイルなどを試すことができます。 [Markdown GFM]: https://github.github.com/gfm/ [^1]: This is a footnote example. [^2]: Here is another footnote. ## Basic formatting ### **Bold** text You can mark some text as bold with **two asterisks** or **two underscores**. ### **Italic** text You can mark some text as italic with _asterisks_ or _underscores_. ### **_Bold and italic_** Three stars gives **_bold and italic_** ### ~~Strikethrough~~ Using `~~two tildes~~` will strikethrough: ~~two tildes~~ ## Blockquotes > Blockquote: More complex nested inline style like **bold: _italic_**. > This is second paragraph, it includes a block quote. And this is next blockquote > Hello, world! ### Nested blockquotes > First level > > > Second level > > Third level > > ```rs > const FOO: &str = "bar"; > ``` ## Code block #### Rust ```rust struct Repository { /// Name of the repository. name: String, } fn main() { let _ = Repository { name: "GPUI Component".to_string(), }; println!("Hello, World!"); } ``` #### Python ```python class Repository: """A repository.""" def __init__(self, name: str): """Initialize the repository. Args: name: Name of the repository. """ self.name = name ``` --- ## Heading for [Links](https://www.google.com) Here is a link to [Google](https://www.google.com), and another to [Rust](https://www.rust-lang.org). ## Image ![](https://miro.medium.com/v2/resize:fit:1400/format:webp/1*sOTh1aAl32jxKNuGO0TOcA.png) ### SVG ![Rust](https://rust-lang.org/static/images/rust-logo-blk.svg) ## Table | Header 1 | Centered | Header 3 | Align Right | | -------- | :------: | ------------------------------------ | ----------: | | Cell 0 | Cell 1 | This is a long cell with line break. | Cell 3 | | Row 2 | Row 2 | Row 2
[Link](https://github.com) | Row 2 | | Row 3 | **Bold** | Row 3 | Row 3 | See the way the text is aligned, depending on the position of `':'` | Syntax | Description | Test Text | | :-------- | :---------: | ----------: | | Header | Title | Here's this | | Paragraph | Text | And more | ## Lists ### Bulleted List - Bullet 1, this is very long and needs to be wrapped to the next line, display should be wrapped to the next line as well. - Bullet 2, the second bullet item is also long and needs to be wrapped to the next line. - Bullet 2.1 - Bullet 2.1.1 - Bullet 2.1.1.1 - Bullet 2.1.2 - Bullet 2.2 - Bullet 3 ### Numbered List 1. Numbered item 1 1. Numbered item 1.1 1. Numbered item 1.1.1 1. Numbered item 1.2 2. Numbered item 2 3. Numbered item 3 ### To-Do List - [x] Task 1, a long long text task, this line is very long and needs to be wrapped to the next line, display should be wrapped to the next line as well. - [ ] Task 2, going to do something if there is a long text that needs to be wrapped to the next line. - [ ] Task 3 ## Heading Add `##` at the beginning of a line to set as Heading. You can use up to 6 `#` symbols for the corresponding Heading levels ## Heading 2 This is paragraph of the heading 2. ### Heading 3 This is paragraph of the heading 3. #### Heading 4 This is paragraph of the heading 4. ##### Heading 5 This is paragraph of the heading 5. ###### Heading 6 This is paragraph of the heading 6. ## HTML ### Paragraph and Text
Here is a test in div.

This is a paragraph inside a div element, have link, bold, italic, and code text.

This is second paragraph.

A text after div.
### List
  1. Numbered item 1
  2. Numbered item 2
  • Bullet 1
  • Bullet 2
### Table
Head 1 Head 2
Cell 1 Cell 2
Cell 3 Cell 4
### Image The Best Programming Languages to Learn in 2025 ## Unsupported ### HTML
Click to expand

This is a paragraph inside a details element.

This is second paragraph.

### Math This is an inline math $x^2 + y^2 = z^2$. This is a block math: $$ \begin{aligned} x^2 + y^2 &= z^2 \\ x^3 + y^3 &= z^3 \end{aligned} $$ This is final paragraph, it includes a code block and a list of items. ================================================ FILE: crates/story/examples/fixtures/test.nv ================================================ use std.net.http.client.{HttpClient, Request}; use std.net.http.OK; fn main() throws { let client = HttpClient.new( max_redirect_count: 5, user_agent: "navi-client", ); let req = try Request.get("https://httpbin.org/get"); let res = try client.request(req); if (res.status() != OK) { try println("Failed to fetch repo", res.text()); return; } try println(res.text()); } ================================================ FILE: crates/story/examples/fixtures/test.php ================================================ "Hello, {$n}!", $names); } public function report(): string { return "HelloWorld({$this->name})"; } } function is_valid_email(string $email): bool { return preg_match('/^[\w+\-.]+@[\w\-]+\.[a-z]{2,}$/i', $email) === 1; } $name = $_GET['name'] ?? 'PHP'; $email = $_POST['email'] ?? 'user@example.com'; $greeter = new HelloWorld((string) $name); $lines = $greeter->greet('Alice', 'Bob', 'Charlie'); ?> <?php echo htmlspecialchars($greeter->report(), ENT_QUOTES, 'UTF-8'); ?>

report(), ENT_QUOTES, 'UTF-8'); ?>

Status:

================================================ FILE: crates/story/examples/fixtures/test.py ================================================ from __future__ import annotations from typing import Optional, List, Dict, Any from dataclasses import dataclass from datetime import datetime import json import asyncio # Data class for configuration settings @dataclass class Config: timeout: int = 5000 # Default timeout in milliseconds retries: int = 3 # Number of retry attempts debug: bool = False # Debug mode flag """ HelloWorld class provides greeting functionality with configuration options. Features: - Async greetings with customizable names - Configuration management - Instance tracking - Report generation Example: greeter = HelloWorld("Python") await greeter.greet("Alice", "Bob") """ class HelloWorld: VERSION: str = "1.0.0" _instance_count: int = 0 def __init__(self, name: str = "World", options: Optional[Dict[str, Any]] = None): self._name = name self._options = options or {} self._created_at = datetime.now() self._config = Config() HelloWorld._instance_count += 1 @property def name(self) -> str: return self._name @name.setter def name(self, value: str) -> None: if not value: raise ValueError("Name cannot be empty") self._name = value @classmethod def get_instance_count(cls) -> int: return cls._instance_count async def greet(self, *names: str) -> None: try: for name in names: await asyncio.sleep(0.1) print(f"Hello, {name}!") except Exception as e: print(f"Error: {str(e)}") def process_names(self, names: List[str] = None) -> List[str]: if names is None: names = [] return sorted([name.upper() for name in names if name]) def _generate_report(self) -> str: return f""" HelloWorld Report ================ Name: {self._name} Created: {self._created_at.isoformat()} Options: {json.dumps(self._options, indent=2)} """ def __str__(self) -> str: return f"HelloWorld(name={self._name})" async def main(): greeter = HelloWorld("Python") await greeter.greet("Alice", "Bob", "Charlie") print(greeter._generate_report()) if __name__ == "__main__": asyncio.run(main()) ================================================ FILE: crates/story/examples/fixtures/test.rb ================================================ require 'json' require 'date' # Module for logging functionality module Logging LOG_LEVELS = %i[debug info warn error].freeze end # HelloWorld class provides greeting functionality with configuration options # @author Example Author # @version 1.0.0 # Features: # - Configurable greetings with multiple names # - Instance tracking # - Report generation # - Logging capabilities class HelloWorld < Object include Logging @@instances = 0 VERSION = '1.0.0' attr_accessor :name attr_reader :created_at def initialize(name: 'World', options: {}) @name = name @created_at = Time.now @options = options @@instances += 1 yield self if block_given? end def self.instance_count(format: :short) case format when :short then @@instances.to_s when :long then "Total instances: #{@@instances}" end end def greet(*names) names.each { |n| puts "Hello, #{n}!" } rescue => e puts "Error: #{e.message}" end def configure(timeout: 5000, retries: 3) @options.merge!(timeout: timeout, retries: retries) end def configured? !@options.empty? end def process_names(names) names.map(&:upcase).select(&:present?) end private def generate_report <<~REPORT HelloWorld Report ================ Name: #{@name} Created: #{@created_at} Options: #{@options.to_json} REPORT end end # Create new greeter instance with configuration block greeter = HelloWorld.new(name: 'Ruby') { |g| g.configure(timeout: 1000) } # Process array and handle errors numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |n| n * 2 } # Email validation EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i validator = ->(email) { email.match?(EMAIL_REGEX) } begin greeter.greet('Alice', 'Bob') rescue StandardError => e puts "Error occurred: #{e.message}" ensure puts "Execution completed at #{Time.now}" end ================================================ FILE: crates/story/examples/fixtures/test.rs ================================================ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::time; // Version number of the HelloWorld struct const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options /// This is CJK 中文🎊 for test line, column. /// /// # Features /// - Async greetings with customizable names /// - Configuration management via HashMap /// - Report generation /// - Error handling with custom error types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HelloWorld { name: String, #[serde(skip)] options: HashMap, created_at: chrono::DateTime, } #[derive(Debug, thiserror::Error)] pub enum HelloError { #[error("Invalid name: {0}")] InvalidName(String), #[error("Operation timeout")] Timeout, } // Document colors: #FF0033, #00AA33, #0033FF, #FFAA33 type Result = std::result::Result; impl HelloWorld { pub fn new(name: impl Into) -> Self { Self { name: name.into(), options: HashMap::new(), created_at: chrono::Utc::now(), } } // Greets multiple people asynchronously with configurable delay // // Use `Command-click` on `Duration` will jump to its definition. // Use `Control-click` on `String`, `HashMap` or `Result` will open its documentation page. pub async fn greet>(&self, names: &[T]) -> Result<()> { for name in names { time::sleep(Duration::from_millis(100)).await; println!("Hello, {}!", name.as_ref()); } Ok(()) } fn generate_report(&self) -> String { format!( "HelloWorld Report\n================\nName: {}\nCreated: {}\nOptions: {:?}", self.name, self.created_at, self.options ) } } trait Configurable { fn configure(&mut self, options: HashMap); fn is_configured(&self) -> bool; } impl Configurable for HelloWorld { fn configure(&mut self, options: HashMap) { self.options.extend(options); } fn is_configured(&self) -> bool { !self.options.is_empty() } } #[tokio::main] async fn main() -> Result<()> { let mut greeter = HelloWorld::new("Rust"); let mut config = HashMap::new(); config.insert("timeout".to_string(), serde_json::json!(5000)); config.insert("retries".to_string(), serde_json::json!(3)); greeter.configure(config); match greeter.greet(&["Alice", "Bob"]).await { Ok(_) => println!("Greetings sent successfully"), Err(e) => eprintln!("Error: {}", e), } Ok(()) } use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::time; // Version number of the HelloWorld struct const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options /// /// # Features /// - Async greetings with customizable names /// - Configuration management via HashMap /// - Report generation /// - Error handling with custom error types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HelloWorld { name: String, #[serde(skip)] options: HashMap, created_at: chrono::DateTime, } #[derive(Debug, thiserror::Error)] pub enum HelloError { #[error("Invalid name: {0}")] InvalidName(String), #[error("Operation timeout")] Timeout, } type Result = std::result::Result; impl HelloWorld { pub fn new(name: impl Into) -> Self { Self { name: name.into(), options: HashMap::new(), created_at: chrono::Utc::now(), } } // Greets multiple people asynchronously with configurable delay pub async fn greet>(&self, names: &[T]) -> Result<()> { for name in names { time::sleep(Duration::from_millis(100)).await; println!("Hello, {}!", name.as_ref()); } Ok(()) } fn generate_report(&self) -> String { format!( "HelloWorld Report\n================\nName: {}\nCreated: {}\nOptions: {:?}", self.name, self.created_at, self.options ) } } trait Configurable { fn configure(&mut self, options: HashMap); fn is_configured(&self) -> bool; } impl Configurable for HelloWorld { fn configure(&mut self, options: HashMap) { self.options.extend(options); } fn is_configured(&self) -> bool { !self.options.is_empty() } } #[tokio::main] async fn main() -> Result<()> { let mut greeter = HelloWorld::new("Rust"); let mut config = HashMap::new(); config.insert("timeout".to_string(), serde_json::json!(5000)); config.insert("retries".to_string(), serde_json::json!(3)); greeter.configure(config); match greeter.greet(&["Alice", "Bob"]).await { Ok(_) => println!("Greetings sent successfully"), Err(e) => eprintln!("Error: {}", e), } Ok(()) } use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::time; // Version number of the HelloWorld struct const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options /// /// # Features /// - Async greetings with customizable names /// - Configuration management via HashMap /// - Report generation /// - Error handling with custom error types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HelloWorld { name: String, #[serde(skip)] options: HashMap, created_at: chrono::DateTime, } #[derive(Debug, thiserror::Error)] pub enum HelloError { #[error("Invalid name: {0}")] InvalidName(String), #[error("Operation timeout")] Timeout, } type Result = std::result::Result; impl HelloWorld { pub fn new(name: impl Into) -> Self { Self { name: name.into(), options: HashMap::new(), created_at: chrono::Utc::now(), } } // Greets multiple people asynchronously with configurable delay pub async fn greet>(&self, names: &[T]) -> Result<()> { for name in names { time::sleep(Duration::from_millis(100)).await; println!("Hello, {}!", name.as_ref()); } Ok(()) } fn generate_report(&self) -> String { format!( "HelloWorld Report\n================\nName: {}\nCreated: {}\nOptions: {:?}", self.name, self.created_at, self.options ) } } trait Configurable { fn configure(&mut self, options: HashMap); fn is_configured(&self) -> bool; } impl Configurable for HelloWorld { fn configure(&mut self, options: HashMap) { self.options.extend(options); } fn is_configured(&self) -> bool { !self.options.is_empty() } } #[tokio::main] async fn main() -> Result<()> { let mut greeter = HelloWorld::new("Rust"); let mut config = HashMap::new(); config.insert("timeout".to_string(), serde_json::json!(5000)); config.insert("retries".to_string(), serde_json::json!(3)); greeter.configure(config); match greeter.greet(&["Alice", "Bob"]).await { Ok(_) => println!("Greetings sent successfully"), Err(e) => eprintln!("Error: {}", e), } Ok(()) } use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::time; // Version number of the HelloWorld struct const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options /// /// # Features /// - Async greetings with customizable names /// - Configuration management via HashMap /// - Report generation /// - Error handling with custom error types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HelloWorld { name: String, #[serde(skip)] options: HashMap, created_at: chrono::DateTime, } #[derive(Debug, thiserror::Error)] pub enum HelloError { #[error("Invalid name: {0}")] InvalidName(String), #[error("Operation timeout")] Timeout, } type Result = std::result::Result; impl HelloWorld { pub fn new(name: impl Into) -> Self { Self { name: name.into(), options: HashMap::new(), created_at: chrono::Utc::now(), } } // Greets multiple people asynchronously with configurable delay pub async fn greet>(&self, names: &[T]) -> Result<()> { for name in names { time::sleep(Duration::from_millis(100)).await; println!("Hello, {}!", name.as_ref()); } Ok(()) } fn generate_report(&self) -> String { format!( "HelloWorld Report\n================\nName: {}\nCreated: {}\nOptions: {:?}", self.name, self.created_at, self.options ) } } trait Configurable { fn configure(&mut self, options: HashMap); fn is_configured(&self) -> bool; } impl Configurable for HelloWorld { fn configure(&mut self, options: HashMap) { self.options.extend(options); } fn is_configured(&self) -> bool { !self.options.is_empty() } } #[tokio::main] async fn main() -> Result<()> { let mut greeter = HelloWorld::new("Rust"); let mut config = HashMap::new(); config.insert("timeout".to_string(), serde_json::json!(5000)); config.insert("retries".to_string(), serde_json::json!(3)); greeter.configure(config); match greeter.greet(&["Alice", "Bob"]).await { Ok(_) => println!("Greetings sent successfully"), Err(e) => eprintln!("Error: {}", e), } Ok(()) } use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::time; // Version number of the HelloWorld struct const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options /// /// # Features /// - Async greetings with customizable names /// - Configuration management via HashMap /// - Report generation /// - Error handling with custom error types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HelloWorld { name: String, #[serde(skip)] options: HashMap, created_at: chrono::DateTime, } #[derive(Debug, thiserror::Error)] pub enum HelloError { #[error("Invalid name: {0}")] InvalidName(String), #[error("Operation timeout")] Timeout, } type Result = std::result::Result; impl HelloWorld { pub fn new(name: impl Into) -> Self { Self { name: name.into(), options: HashMap::new(), created_at: chrono::Utc::now(), } } // Greets multiple people asynchronously with configurable delay pub async fn greet>(&self, names: &[T]) -> Result<()> { for name in names { time::sleep(Duration::from_millis(100)).await; println!("Hello, {}!", name.as_ref()); } Ok(()) } fn generate_report(&self) -> String { format!( "HelloWorld Report\n================\nName: {}\nCreated: {}\nOptions: {:?}", self.name, self.created_at, self.options ) } } trait Configurable { fn configure(&mut self, options: HashMap); fn is_configured(&self) -> bool; } impl Configurable for HelloWorld { fn configure(&mut self, options: HashMap) { self.options.extend(options); } fn is_configured(&self) -> bool { !self.options.is_empty() } } #[tokio::main] async fn main() -> Result<()> { let mut greeter = HelloWorld::new("Rust"); let mut config = HashMap::new(); config.insert("timeout".to_string(), serde_json::json!(5000)); config.insert("retries".to_string(), serde_json::json!(3)); greeter.configure(config); match greeter.greet(&["Alice", "Bob"]).await { Ok(_) => println!("Greetings sent successfully"), Err(e) => eprintln!("Error: {}", e), } Ok(()) } use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::time; // Version number of the HelloWorld struct const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options /// /// # Features /// - Async greetings with customizable names /// - Configuration management via HashMap /// - Report generation /// - Error handling with custom error types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HelloWorld { name: String, #[serde(skip)] options: HashMap, created_at: chrono::DateTime, } #[derive(Debug, thiserror::Error)] pub enum HelloError { #[error("Invalid name: {0}")] InvalidName(String), #[error("Operation timeout")] Timeout, } type Result = std::result::Result; impl HelloWorld { pub fn new(name: impl Into) -> Self { Self { name: name.into(), options: HashMap::new(), created_at: chrono::Utc::now(), } } // Greets multiple people asynchronously with configurable delay pub async fn greet>(&self, names: &[T]) -> Result<()> { for name in names { time::sleep(Duration::from_millis(100)).await; println!("Hello, {}!", name.as_ref()); } Ok(()) } fn generate_report(&self) -> String { format!( "HelloWorld Report\n================\nName: {}\nCreated: {}\nOptions: {:?}", self.name, self.created_at, self.options ) } } trait Configurable { fn configure(&mut self, options: HashMap); fn is_configured(&self) -> bool; } impl Configurable for HelloWorld { fn configure(&mut self, options: HashMap) { self.options.extend(options); } fn is_configured(&self) -> bool { !self.options.is_empty() } } #[tokio::main] async fn main() -> Result<()> { let mut greeter = HelloWorld::new("Rust"); let mut config = HashMap::new(); config.insert("timeout".to_string(), serde_json::json!(5000)); config.insert("retries".to_string(), serde_json::json!(3)); greeter.configure(config); match greeter.greet(&["Alice", "Bob"]).await { Ok(_) => println!("Greetings sent successfully"), Err(e) => eprintln!("Error: {}", e), } Ok(()) } use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::time; // Version number of the HelloWorld struct const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options /// /// # Features /// - Async greetings with customizable names /// - Configuration management via HashMap /// - Report generation /// - Error handling with custom error types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HelloWorld { name: String, #[serde(skip)] options: HashMap, created_at: chrono::DateTime, } #[derive(Debug, thiserror::Error)] pub enum HelloError { #[error("Invalid name: {0}")] InvalidName(String), #[error("Operation timeout")] Timeout, } type Result = std::result::Result; impl HelloWorld { pub fn new(name: impl Into) -> Self { Self { name: name.into(), options: HashMap::new(), created_at: chrono::Utc::now(), } } // Greets multiple people asynchronously with configurable delay pub async fn greet>(&self, names: &[T]) -> Result<()> { for name in names { time::sleep(Duration::from_millis(100)).await; println!("Hello, {}!", name.as_ref()); } Ok(()) } fn generate_report(&self) -> String { format!( "HelloWorld Report\n================\nName: {}\nCreated: {}\nOptions: {:?}", self.name, self.created_at, self.options ) } } trait Configurable { fn configure(&mut self, options: HashMap); fn is_configured(&self) -> bool; } impl Configurable for HelloWorld { fn configure(&mut self, options: HashMap) { self.options.extend(options); } fn is_configured(&self) -> bool { !self.options.is_empty() } } #[tokio::main] async fn main() -> Result<()> { let mut greeter = HelloWorld::new("Rust"); let mut config = HashMap::new(); config.insert("timeout".to_string(), serde_json::json!(5000)); config.insert("retries".to_string(), serde_json::json!(3)); greeter.configure(config); match greeter.greet(&["Alice", "Bob"]).await { Ok(_) => println!("Greetings sent successfully"), Err(e) => eprintln!("Error: {}", e), } Ok(()) } use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::time; // Version number of the HelloWorld struct const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options /// /// # Features /// - Async greetings with customizable names /// - Configuration management via HashMap /// - Report generation /// - Error handling with custom error types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HelloWorld { name: String, #[serde(skip)] options: HashMap, created_at: chrono::DateTime, } #[derive(Debug, thiserror::Error)] pub enum HelloError { #[error("Invalid name: {0}")] InvalidName(String), #[error("Operation timeout")] Timeout, } type Result = std::result::Result; impl HelloWorld { pub fn new(name: impl Into) -> Self { Self { name: name.into(), options: HashMap::new(), created_at: chrono::Utc::now(), } } // Greets multiple people asynchronously with configurable delay pub async fn greet>(&self, names: &[T]) -> Result<()> { for name in names { time::sleep(Duration::from_millis(100)).await; println!("Hello, {}!", name.as_ref()); } Ok(()) } fn generate_report(&self) -> String { format!( "HelloWorld Report\n================\nName: {}\nCreated: {}\nOptions: {:?}", self.name, self.created_at, self.options ) } } trait Configurable { fn configure(&mut self, options: HashMap); fn is_configured(&self) -> bool; } impl Configurable for HelloWorld { fn configure(&mut self, options: HashMap) { self.options.extend(options); } fn is_configured(&self) -> bool { !self.options.is_empty() } } #[tokio::main] async fn main() -> Result<()> { let mut greeter = HelloWorld::new("Rust"); let mut config = HashMap::new(); config.insert("timeout".to_string(), serde_json::json!(5000)); config.insert("retries".to_string(), serde_json::json!(3)); greeter.configure(config); match greeter.greet(&["Alice", "Bob"]).await { Ok(_) => println!("Greetings sent successfully"), Err(e) => eprintln!("Error: {}", e), } Ok(()) } use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::time; // Version number of the HelloWorld struct const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options /// /// # Features /// - Async greetings with customizable names /// - Configuration management via HashMap /// - Report generation /// - Error handling with custom error types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HelloWorld { name: String, #[serde(skip)] options: HashMap, created_at: chrono::DateTime, } #[derive(Debug, thiserror::Error)] pub enum HelloError { #[error("Invalid name: {0}")] InvalidName(String), #[error("Operation timeout")] Timeout, } type Result = std::result::Result; impl HelloWorld { pub fn new(name: impl Into) -> Self { Self { name: name.into(), options: HashMap::new(), created_at: chrono::Utc::now(), } } // Greets multiple people asynchronously with configurable delay pub async fn greet>(&self, names: &[T]) -> Result<()> { for name in names { time::sleep(Duration::from_millis(100)).await; println!("Hello, {}!", name.as_ref()); } Ok(()) } fn generate_report(&self) -> String { format!( "HelloWorld Report\n================\nName: {}\nCreated: {}\nOptions: {:?}", self.name, self.created_at, self.options ) } } trait Configurable { fn configure(&mut self, options: HashMap); fn is_configured(&self) -> bool; } impl Configurable for HelloWorld { fn configure(&mut self, options: HashMap) { self.options.extend(options); } fn is_configured(&self) -> bool { !self.options.is_empty() } } #[tokio::main] async fn main() -> Result<()> { let mut greeter = HelloWorld::new("Rust"); let mut config = HashMap::new(); config.insert("timeout".to_string(), serde_json::json!(5000)); config.insert("retries".to_string(), serde_json::json!(3)); greeter.configure(config); match greeter.greet(&["Alice", "Bob"]).await { Ok(_) => println!("Greetings sent successfully"), Err(e) => eprintln!("Error: {}", e), } Ok(()) } use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::time; // Version number of the HelloWorld struct const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options /// /// # Features /// - Async greetings with customizable names /// - Configuration management via HashMap /// - Report generation /// - Error handling with custom error types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HelloWorld { name: String, #[serde(skip)] options: HashMap, created_at: chrono::DateTime, } #[derive(Debug, thiserror::Error)] pub enum HelloError { #[error("Invalid name: {0}")] InvalidName(String), #[error("Operation timeout")] Timeout, } type Result = std::result::Result; impl HelloWorld { pub fn new(name: impl Into) -> Self { Self { name: name.into(), options: HashMap::new(), created_at: chrono::Utc::now(), } } // Greets multiple people asynchronously with configurable delay pub async fn greet>(&self, names: &[T]) -> Result<()> { for name in names { time::sleep(Duration::from_millis(100)).await; println!("Hello, {}!", name.as_ref()); } Ok(()) } fn generate_report(&self) -> String { format!( "HelloWorld Report\n================\nName: {}\nCreated: {}\nOptions: {:?}", self.name, self.created_at, self.options ) } } trait Configurable { fn configure(&mut self, options: HashMap); fn is_configured(&self) -> bool; } impl Configurable for HelloWorld { fn configure(&mut self, options: HashMap) { self.options.extend(options); } fn is_configured(&self) -> bool { !self.options.is_empty() } } #[tokio::main] async fn main() -> Result<()> { let mut greeter = HelloWorld::new("Rust"); let mut config = HashMap::new(); config.insert("timeout".to_string(), serde_json::json!(5000)); config.insert("retries".to_string(), serde_json::json!(3)); greeter.configure(config); match greeter.greet(&["Alice", "Bob"]).await { Ok(_) => println!("Greetings sent successfully"), Err(e) => eprintln!("Error: {}", e), } Ok(()) } use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; use tokio::time; // Version number of the HelloWorld struct const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options /// /// # Features /// - Async greetings with customizable names /// - Configuration management via HashMap /// - Report generation /// - Error handling with custom error types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HelloWorld { name: String, #[serde(skip)] options: HashMap, created_at: chrono::DateTime, } #[derive(Debug, thiserror::Error)] pub enum HelloError { #[error("Invalid name: {0}")] InvalidName(String), #[error("Operation timeout")] Timeout, } type Result = std::result::Result; impl HelloWorld { pub fn new(name: impl Into) -> Self { Self { name: name.into(), options: HashMap::new(), created_at: chrono::Utc::now(), } } // Greets multiple people asynchronously with configurable delay pub async fn greet>(&self, names: &[T]) -> Result<()> { for name in names { time::sleep(Duration::from_millis(100)).await; println!("Hello, {}!", name.as_ref()); } Ok(()) } fn generate_report(&self) -> String { format!( "HelloWorld Report\n================\nName: {}\nCreated: {}\nOptions: {:?}", self.name, self.created_at, self.options ) } } trait Configurable { fn configure(&mut self, options: HashMap); fn is_configured(&self) -> bool; } impl Configurable for HelloWorld { fn configure(&mut self, options: HashMap) { self.options.extend(options); } fn is_configured(&self) -> bool { !self.options.is_empty() } } #[tokio::main] async fn main() -> Result<()> { let mut greeter = HelloWorld::new("Rust"); let mut config = HashMap::new(); config.insert("timeout".to_string(), serde_json::json!(5000)); config.insert("retries".to_string(), serde_json::json!(3)); greeter.configure(config); match greeter.greet(&["Alice", "Bob"]).await { Ok(_) => println!("Greetings sent successfully"), Err(e) => eprintln!("Error: {}", e), } Ok(()) } ================================================ FILE: crates/story/examples/fixtures/test.sql ================================================ SELECT * FROM users WHERE email ilike '%test%' AND deleted_at IS NOT NULL LIMIT 1; ================================================ FILE: crates/story/examples/fixtures/test.svelte ================================================

Svelte 5 Features

Counter

Count: {state.count}

Input Binding

List Management

e.key === 'Enter' && addItem()} />
{#if state.items.length === 0}

No items yet. Add some!

{:else}
    {#each state.items as item, i (item)}
  • {item}
  • {/each}
{/if}
================================================ FILE: crates/story/examples/fixtures/test.ts ================================================ import fs, { readFile } from "fs"; const VERSION = "1.0.0"; class HelloWorld { private name: string; private createdAt: Date; private options: Record; constructor(name: string) { this.name = name; this.createdAt = new Date(); this.options = {}; } greet(names: string[]): void { for (const name of names) { console.log(`Hello, ${name}!`); } } configure(cfg: { timeout: number; retries: number; debug: boolean }): void { this.options["timeout"] = cfg.timeout; this.options["retries"] = cfg.retries; this.options["debug"] = cfg.debug; } generateReport(): string { return ` HelloWorld Report ================= Name: ${this.name} Created: ${this.createdAt.toISOString()} Options: ${JSON.stringify(this.options, null, 2)} `; } } const timeout = 5000; function main() { const greeter = new HelloWorld("TypeScript"); greeter.configure({ timeout: timeout, retries: 3, debug: true, }); greeter.greet(["Alice", "Bob"]); console.log(greeter.generateReport()); } ================================================ FILE: crates/story/examples/fixtures/test.zig ================================================ const std = @import("std"); const json = std.json; const time = std.time; const HashMap = std.HashMap; pub const VERSION = "1.0.0"; pub const HelloError = error{ InvalidName, Timeout, }; pub const HelloWorld = struct { name: []const u8, options: HashMap([]const u8, json.Value), created_at: i64, pub fn init(allocator: *std.mem.Allocator, name: []const u8) !HelloWorld { return HelloWorld{ .name = name, .options = HashMap([]const u8, json.Value).init(allocator), .created_at = time.timestamp(), }; } pub fn deinit(self: *HelloWorld) void { self.options.deinit(); } pub fn greet(self: *const HelloWorld, names: []const []const u8) !void { for (names) |name| { time.sleep(100 * time.millisecond); std.debug.print("Hello, {s}!\n", .{name}); } } pub fn configure(self: *HelloWorld, options: HashMap([]const u8, json.Value)) void { var it = options.iterator(); while (it.next()) |entry| { self.options.put(entry.key, entry.value) catch {}; } } pub fn generateReport(self: *const HelloWorld) ![]const u8 { var report = std.ArrayList(u8).init(std.heap.page_allocator); defer report.deinit(); try report.writer().print( \\HelloWorld Report \\================ \\Name: {s} \\Created: {} \\Options: {} \\ , .{ self.name, self.created_at, self.options, }); return report.toOwnedSlice(); } }; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = &gpa.allocator; var greeter = try HelloWorld.init(allocator, "Zig"); defer greeter.deinit(); var config = HashMap([]const u8, json.Value).init(allocator); try config.put("timeout", json.Value{ .Integer = 5000 }); try config.put("retries", json.Value{ .Integer = 3 }); greeter.configure(config); const names = [_][]const u8{ "Alice", "Bob" }; try greeter.greet(&names); const report = try greeter.generateReport(); std.debug.print("{s}\n", .{report}); } ================================================ FILE: crates/story/examples/html.rs ================================================ use gpui::*; use gpui_component::{ ActiveTheme as _, highlighter::Language, input::{Input, InputState, TabSize}, resizable::h_resizable, text::html, }; use gpui_component_assets::Assets; pub struct Example { input_state: Entity, _subscribe: Subscription, } const EXAMPLE: &str = include_str!("./fixtures/test.html"); impl Example { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let input_state = cx.new(|cx| { InputState::new(window, cx) .code_editor(Language::Html) .tab_size(TabSize { tab_size: 4, hard_tabs: false, }) .default_value(EXAMPLE) .placeholder("Enter your HTML here...") }); let _subscribe = cx.subscribe( &input_state, |_, _, _: &gpui_component::input::InputEvent, cx| { cx.notify(); }, ); Self { input_state, _subscribe, } } fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl Render for Example { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { h_resizable("container") .child( div() .id("source") .size_full() .font_family(cx.theme().mono_font_family.clone()) .text_size(cx.theme().mono_font_size) .child( Input::new(&self.input_state) .h_full() .appearance(false) .focus_bordered(false), ) .into_any(), ) .child( html(self.input_state.read(cx).value().clone()) .p_5() .scrollable(true) .selectable(true) .into_any(), ) } } fn main() { let app = gpui_platform::application().with_assets(Assets); app.run(move |cx| { gpui_component_story::init(cx); cx.activate(true); gpui_component_story::create_new_window("HTML Render (native)", Example::view, cx); }); } ================================================ FILE: crates/story/examples/large-text.rs ================================================ use gpui::*; use gpui_component::{ ActiveTheme, Selectable, Sizable, WindowExt, button::{Button, ButtonVariants as _}, h_flex, input::{self, Input, InputEvent, InputState, TabSize}, v_flex, }; use gpui_component_assets::Assets; pub struct Example { editor: Entity, go_to_line_state: Entity, soft_wrap: bool, _subscribes: Vec, } impl Example { pub fn new(window: &mut Window, cx: &mut Context) -> Self { // 10K lines let text = "这是一个中文演示段落,用于展示更多的 [Markdown GFM] 内容。您可以在此尝试使用使用**粗体**、*斜体*和`代码`等样式。これは日本語のデモ段落です。Markdown の多言語サポートを示すためのテキストが含まれています。例えば、、**ボールド**、_イタリック_、および`コード`のスタイルなどを試すことができます。\n".repeat(10000); let editor = cx.new(|cx| { InputState::new(window, cx) .multi_line(true) .tab_size(TabSize { tab_size: 4, hard_tabs: false }) .soft_wrap(true) .placeholder("Enter your code here...") .default_value(text) }); let go_to_line_state = cx.new(|cx| InputState::new(window, cx)); let _subscribes = vec![cx.subscribe(&editor, |_, _, _: &InputEvent, cx| { cx.notify(); })]; Self { editor, go_to_line_state, soft_wrap: false, _subscribes } } fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn go_to_line(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { let editor = self.editor.clone(); let input_state = self.go_to_line_state.clone(); window.open_dialog(cx, move |dialog, window, cx| { input_state.update(cx, |state, cx| { let position = editor.read(cx).cursor_position(); state.set_placeholder( format!("{}:{}", position.line, position.character), window, cx, ); state.focus(window, cx); }); dialog.title("Go to line").child(Input::new(&input_state)).on_ok({ let editor = editor.clone(); let input_state = input_state.clone(); move |_, window, cx| { let query = input_state.read(cx).value(); let mut parts = query .split(':') .map(|s| s.trim().parse::().ok()) .collect::>() .into_iter(); let Some(line) = parts.next().and_then(|l| l) else { return false; }; let line = line.saturating_sub(1); let column = parts.next().and_then(|c| c).unwrap_or(1).saturating_sub(1); editor.update(cx, |state, cx| { state.set_cursor_position( input::Position::new(line as u32, column as u32), window, cx, ); }); true } }) }); } fn toggle_soft_wrap(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { self.soft_wrap = !self.soft_wrap; self.editor.update(cx, |state, cx| { state.set_soft_wrap(self.soft_wrap, window, cx); }); cx.notify(); } } impl Render for Example { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex().size_full().child( v_flex() .id("source") .w_full() .flex_1() .child(Input::new(&self.editor).bordered(false).h_full().focus_bordered(false)) .child( h_flex() .justify_between() .text_sm() .bg(cx.theme().secondary) .py_1p5() .px_4() .border_t_1() .border_color(cx.theme().border) .text_color(cx.theme().muted_foreground) .child(h_flex().gap_3().child({ Button::new("soft-wrap") .ghost() .xsmall() .label("Soft Wrap") .selected(self.soft_wrap) .on_click(cx.listener(Self::toggle_soft_wrap)) })) .child({ let loc = self.editor.read(cx).cursor_position(); let cursor = self.editor.read(cx).cursor(); Button::new("line-column") .ghost() .xsmall() .label(format!("{}:{} ({} c)", loc.line, loc.character, cursor)) .on_click(cx.listener(Self::go_to_line)) }), ), ) } } fn main() { let app = gpui_platform::application().with_assets(Assets); app.run(move |cx| { gpui_component_story::init(cx); cx.activate(true); gpui_component_story::create_new_window("Large Text Editor", Example::view, cx); }); } ================================================ FILE: crates/story/examples/markdown.rs ================================================ use gpui::{prelude::FluentBuilder as _, *}; use gpui_component::{ ActiveTheme as _, IconName, Sizable as _, button::{Button, ButtonVariants as _}, clipboard::Clipboard, h_flex, highlighter::Language, input::{Input, InputEvent, InputState, TabSize}, resizable::{h_resizable, resizable_panel}, text::markdown, }; use gpui_component_assets::Assets; use gpui_component_story::Open; pub struct Example { input_state: Entity, _subscriptions: Vec, } const EXAMPLE: &str = include_str!("./fixtures/test.md"); impl Example { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let input_state = cx.new(|cx| { InputState::new(window, cx) .code_editor(Language::Markdown) .line_number(true) .tab_size(TabSize { tab_size: 2, ..Default::default() }) .searchable(true) .placeholder("Enter your Markdown here...") .default_value(EXAMPLE) }); let _subscriptions = vec![cx.subscribe(&input_state, |_, _, _: &InputEvent, _| {})]; Self { input_state, _subscriptions, } } fn on_action_open(&mut self, _: &Open, window: &mut Window, cx: &mut Context) { let path = cx.prompt_for_paths(PathPromptOptions { files: true, directories: true, multiple: false, prompt: Some("Select a Markdown file".into()), }); let input_state = self.input_state.clone(); cx.spawn_in(window, async move |_, window| { let path = path.await.ok()?.ok()??.iter().next()?.clone(); let content = std::fs::read_to_string(&path).ok()?; window .update(|window, cx| { _ = input_state.update(cx, |this, cx| { this.set_value(content, window, cx); }); }) .ok(); Some(()) }) .detach(); } fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl Render for Example { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .id("editor") .size_full() .on_action(cx.listener(Self::on_action_open)) .child( h_resizable("container") .child( resizable_panel().child( div() .id("source") .size_full() .font_family(cx.theme().mono_font_family.clone()) .text_size(cx.theme().mono_font_size) .child( Input::new(&self.input_state) .h_full() .p_0() .border_0() .focus_bordered(false), ), ), ) .child( resizable_panel().child( markdown(self.input_state.read(cx).value().clone()) .code_block_actions(|code_block, _window, _cx| { let code = code_block.code(); let lang = code_block.lang(); h_flex() .gap_1() .child(Clipboard::new("copy").value(code.clone())) .when_some(lang, |this, lang| { // Only show run terminal button for certain languages if lang.as_ref() == "rust" || lang.as_ref() == "python" { this.child( Button::new("run-terminal") .icon(IconName::SquareTerminal) .ghost() .xsmall() .on_click(move |_, _, _cx| { println!( "Running {} code: {}", lang, code ); }), ) } else { this } }) }) .flex_none() .p_5() .scrollable(true) .selectable(true), ), ), ) } } fn main() { let app = gpui_platform::application().with_assets(Assets); app.run(move |cx| { gpui_component_story::init(cx); cx.activate(true); gpui_component_story::create_new_window("Markdown Editor", Example::view, cx); }); } ================================================ FILE: crates/story/examples/stream_markdown.rs ================================================ use gpui::*; use gpui_component::{ button::Button, h_flex, text::{TextView, TextViewState}, v_flex, }; use gpui_component_assets::Assets; pub struct Example { markdown_state: Entity, tx: smol::channel::Sender, scroll_handle: ScrollHandle, _task: Task<()>, _update_task: Task<()>, } const EXAMPLE: &str = include_str!("./fixtures/test.md"); impl Example { pub fn new(_: &mut Window, cx: &mut Context) -> Self { let markdown_state = cx.new(|cx| TextViewState::markdown("# Streaming Markdown Parse\n\n", cx)); let scroll_handle = ScrollHandle::new(); let (tx, rx) = smol::channel::unbounded::(); let _task = cx.spawn({ let scroll_handle = scroll_handle.clone(); let weak_state = markdown_state.downgrade(); async move |_, cx| { while let Ok(chunk) = rx.recv().await { _ = weak_state.update(cx, |state, cx| { // Push the new chunk to the markdown state, // it will reparse and re-render automatically. state.push_str(&chunk, cx); scroll_handle.scroll_to_bottom(); }); } } }); Self { markdown_state, scroll_handle, tx, _task, _update_task: Task::ready(()), } } fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } /// Simulate streaming by updating markdown state in chunks /// 50ms for a iteration, every time adding about 5 - 20 characters /// This is just for demonstration; in a real app, you'd stream from a source. fn replay(&mut self, _window: &mut Window, cx: &mut Context) { let tx = self.tx.clone(); let mut current = 0; self.markdown_state.update(cx, |state, cx| { state.set_text("", cx); }); self._update_task = cx.background_executor().spawn(async move { let chars: Vec = EXAMPLE.chars().collect(); while current < chars.len() { let chunk_size = (5 + rand::random::() % 15).min(chars.len() - current); let chunk: String = chars[current..current + chunk_size].iter().collect(); _ = tx.try_send(chunk); current += chunk_size; std::thread::sleep(std::time::Duration::from_millis(50)); } }); } } impl Render for Example { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .id("example") .size_full() .p_4() .gap_4() .child( h_flex() .w_full() .child( Button::new("replay") .outline() .label("Replay") .on_click(cx.listener(move |this, _, window, cx| { this.replay(window, cx); })), ), ) .child( div() .id("contents") .flex_1() .w_full() .track_scroll(&self.scroll_handle) .overflow_y_scroll() .size_full() .child(TextView::new(&self.markdown_state).selectable(true)), ) } } fn main() { let app = gpui_platform::application().with_assets(Assets); app.run(move |cx| { gpui_component_story::init(cx); cx.activate(true); gpui_component_story::create_new_window_with_size( "Stream Markdown", Some(size(px(600.), px(800.))), Example::view, cx, ); }); } ================================================ FILE: crates/story/examples/tiles.rs ================================================ use anyhow::{Context as _, Result}; use gpui::*; use gpui_component::{ ActiveTheme, Root, Sizable, TitleBar, dock::{ DockArea, DockAreaState, DockEvent, DockItem, Panel, PanelEvent, PanelInfo, PanelRegistry, PanelState, PanelView, register_panel, }, input::{Input, InputState}, scroll::ScrollbarShow, }; use gpui_component_assets::Assets; use gpui_component_story::{ButtonStory, IconStory, StoryContainer}; use serde::{Deserialize, Serialize}; use std::{sync::Arc, time::Duration}; actions!(tiles_story, [Quit]); const TILES_DOCK_AREA: DockAreaTab = DockAreaTab { id: "story-tiles", version: 1, }; /// A specification for a container panel for wrapping other panels to add some common functionality. /// /// For example: /// /// - Add a search bar to all panels. struct ContainerPanel { panel: Arc, search_state: Entity, } #[derive(Clone, Serialize, Deserialize)] struct ContainerPanelState { /// The state of the child panel. child: PanelState, } impl ContainerPanelState { fn new(child: PanelState) -> Self { Self { child } } fn to_value(&self) -> serde_json::Value { serde_json::to_value(self).unwrap() } fn from_value(value: serde_json::Value) -> Result { serde_json::from_value(value).context("failed to deserialize ContainerPanelState") } } impl ContainerPanel { fn init(cx: &mut App) { register_panel( cx, "ContainerPanel", |dock_area, _, info, window, cx| match info { PanelInfo::Panel(panel_info) => { let container_state = ContainerPanelState::from_value(panel_info.clone()).unwrap(); let child_state = container_state.child; let view = PanelRegistry::build_panel( &child_state.panel_name, dock_area, &child_state, &child_state.info, window, cx, ); Box::new(ContainerPanel::new(view.into(), window, cx)) } _ => unreachable!(), }, ); } fn new(panel: Arc, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| { let search_state = cx.new(|cx| InputState::new(window, cx).placeholder("Search...")); Self { panel, search_state, } }) } } impl Panel for ContainerPanel { fn panel_name(&self) -> &'static str { "ContainerPanel" } fn title(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { self.panel.title(window, cx) } fn title_suffix(&mut self, _: &mut Window, cx: &mut Context) -> Option { Some( div() .w_24() .h_6() .px_0p5() .rounded(cx.theme().radius_lg) .border_1() .border_color(cx.theme().input) .child(Input::new(&self.search_state).xsmall().appearance(false)) .into_any_element(), ) } fn dump(&self, cx: &App) -> PanelState { let mut state = PanelState::new(self); let panel_state = self.panel.dump(cx); let json_value = ContainerPanelState::new(panel_state).to_value(); state.info = PanelInfo::panel(json_value); state } } impl EventEmitter for ContainerPanel {} impl Focusable for ContainerPanel { fn focus_handle(&self, cx: &App) -> FocusHandle { self.panel.focus_handle(cx) } } impl Render for ContainerPanel { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { self.panel.view().clone() } } actions!(workspace, [Open, CloseWindow]); pub fn init(cx: &mut App) { cx.on_action(|_action: &Open, _cx: &mut App| {}); gpui_component::init(cx); gpui_component_story::init(cx); } pub struct StoryTiles { dock_area: Entity, last_layout_state: Option, _save_layout_task: Option>, } struct DockAreaTab { id: &'static str, version: usize, } impl StoryTiles { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let dock_area = cx.new(|cx| { DockArea::new( TILES_DOCK_AREA.id, Some(TILES_DOCK_AREA.version), window, cx, ) }); let weak_dock_area = dock_area.downgrade(); match Self::load_tiles(dock_area.clone(), window, cx) { Ok(_) => { println!("load tiles success"); } Err(err) => { eprintln!("load tiles error: {:?}", err); Self::reset_default_layout(weak_dock_area, window, cx); } }; cx.subscribe_in( &dock_area, window, |this, dock_area, ev: &DockEvent, window, cx| match ev { DockEvent::LayoutChanged => this.save_layout(dock_area, window, cx), DockEvent::DragDrop(item) => { println!("drag drop: {:?}", item); } }, ) .detach(); cx.on_app_quit({ let dock_area = dock_area.clone(); move |_, cx| { let state = dock_area.read(cx).dump(cx); cx.background_executor().spawn(async move { // Save layout before quitting Self::save_tiles(&state).unwrap(); }) } }) .detach(); Self { dock_area, last_layout_state: None, _save_layout_task: None, } } fn save_layout( &mut self, dock_area: &Entity, _: &mut Window, cx: &mut Context, ) { let dock_area = dock_area.clone(); self._save_layout_task = Some(cx.spawn(async move |this, cx| { cx.background_executor() .timer(Duration::from_secs(10)) .await; let _ = cx.update(|cx| { let dock_area = dock_area.read(cx); let state = dock_area.dump(cx); let last_layout_state = this.upgrade().unwrap().read(cx).last_layout_state.clone(); if Some(&state) == last_layout_state.as_ref() { return; } Self::save_tiles(&state).unwrap(); let _ = this.update(cx, |this, _| { this.last_layout_state = Some(state); }); }); })); } fn save_tiles(state: &DockAreaState) -> Result<()> { println!("Save tiles..."); let json = serde_json::to_string_pretty(state)?; std::fs::write("target/tiles.json", json)?; Ok(()) } fn set_scrollbar_show(dock_area: &mut DockArea, cx: &mut App) { match dock_area.center() { DockItem::Tiles { view, .. } => { view.update(cx, |this, cx| { this.set_scrollbar_show(Some(ScrollbarShow::Always), cx); }); } _ => {} } } fn load_tiles( dock_area: Entity, window: &mut Window, cx: &mut Context, ) -> Result<()> { let fname = "target/tiles.json"; let json = std::fs::read_to_string(fname)?; let state = serde_json::from_str::(&json)?; // Check if the saved layout version is different from the current version // Notify the user and ask if they want to reset the layout to default. if state.version != Some(TILES_DOCK_AREA.version) { let answer = window.prompt( PromptLevel::Info, "The default tiles layout has been updated.\n\ Do you want to reset the layout to default?", None, &["Yes", "No"], cx, ); let weak_dock_area = dock_area.downgrade(); cx.spawn_in(window, async move |this, window| { if answer.await == Ok(0) { _ = this.update_in(window, |_, window, cx| { Self::reset_default_layout(weak_dock_area, window, cx); }); } }) .detach(); } dock_area.update(cx, |dock_area, cx| { dock_area.load(state, window, cx).context("load layout")?; Self::set_scrollbar_show(dock_area, cx); Ok::<(), anyhow::Error>(()) }) } fn reset_default_layout( dock_area: WeakEntity, window: &mut Window, cx: &mut Context, ) { let dock_item = Self::init_default_layout(&dock_area, window, cx); _ = dock_area.update(cx, |dock_area, cx| { dock_area.set_version(TILES_DOCK_AREA.version, window, cx); dock_area.set_center(dock_item, window, cx); Self::set_scrollbar_show(dock_area, cx); Self::save_tiles(&dock_area.dump(cx)).unwrap(); }); } fn init_default_layout( dock_area: &WeakEntity, window: &mut Window, cx: &mut App, ) -> DockItem { const PANELS: usize = 4; let panels = (0..PANELS) .map(|i| { let story = if i % 2 == 0 { Arc::new(StoryContainer::panel::(window, cx)) } else { Arc::new(StoryContainer::panel::(window, cx)) }; DockItem::tab( ContainerPanel::new(story, window, cx), dock_area, window, cx, ) }) .collect::>(); // Panel size: 380x280, Gap: 20px, Starting position: (20, 20) let panel_width = px(380.); let panel_height = px(280.); let gap = px(20.); let start_x = px(20.); let start_y = px(20.); let cols = 4; let bounds = (0..PANELS) .map(|i| { let row = i / cols; let col = i % cols; let x = start_x + (panel_width + gap) * col as f32; let y = start_y + (panel_height + gap) * row as f32; Bounds::new(point(x, y), size(panel_width, panel_height)) }) .collect::>(); DockItem::tiles(panels, bounds, dock_area, window, cx) } pub fn new_local(cx: &mut App) -> Task>> { let mut window_size = size(px(1600.0), px(1200.0)); if let Some(display) = cx.primary_display() { let display_size = display.bounds().size; window_size.width = window_size.width.min(display_size.width * 0.85); window_size.height = window_size.height.min(display_size.height * 0.85); } let window_bounds = Bounds::centered(None, window_size, cx); cx.spawn(async move |cx| { let options = WindowOptions { window_bounds: Some(WindowBounds::Windowed(window_bounds)), titlebar: Some(TitlebarOptions { title: None, appears_transparent: true, traffic_light_position: Some(point(px(9.0), px(9.0))), }), window_min_size: Some(gpui::Size { width: px(640.), height: px(480.), }), kind: WindowKind::Normal, #[cfg(target_os = "linux")] window_background: gpui::WindowBackgroundAppearance::Transparent, #[cfg(target_os = "linux")] window_decorations: Some(gpui::WindowDecorations::Client), ..Default::default() }; let window = cx.open_window(options, |window, cx| { let tiles_view = cx.new(|cx| Self::new(window, cx)); cx.new(|cx| Root::new(tiles_view, window, cx)) })?; window .update(cx, |_, window, _| { window.activate_window(); window.set_window_title("Story Tiles"); }) .expect("failed to update window"); Ok(window) }) } } pub fn open_new( cx: &mut App, init: impl FnOnce(&mut Root, &mut Window, &mut Context) + 'static + Send, ) -> Task<()> { let task: Task, anyhow::Error>> = StoryTiles::new_local(cx); cx.spawn(async move |cx| { if let Some(root) = task.await.ok() { root.update(cx, |workspace, window, cx| init(workspace, window, cx)) .expect("failed to init workspace"); } }) } impl Render for StoryTiles { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let sheet_layer = Root::render_sheet_layer(window, cx); let dialog_layer = Root::render_dialog_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); div() .font_family(cx.theme().font_family.clone()) .relative() .size_full() .flex() .flex_col() .bg(cx.theme().background) .text_color(cx.theme().foreground) .child(TitleBar::new().child(div().flex().items_center().child("Story Tiles"))) .child(self.dock_area.clone()) .children(sheet_layer) .children(dialog_layer) .children(notification_layer) } } fn main() { let app = gpui_platform::application().with_assets(Assets); app.run(move |cx| { gpui_component::init(cx); gpui_component_story::init(cx); ContainerPanel::init(cx); cx.on_action(quit); cx.set_menus(vec![Menu { name: "GPUI App".into(), items: vec![MenuItem::action("Quit", Quit)], }]); cx.activate(true); open_new(cx, |_, _, _| { // do something }) .detach(); }); } fn quit(_: &Quit, cx: &mut App) { cx.quit(); } ================================================ FILE: crates/story/src/app_menus.rs ================================================ use gpui::{App, Entity, Menu, MenuItem, SharedString}; use gpui_component::{ ActiveTheme as _, GlobalState, Theme, ThemeMode, ThemeRegistry, menu::AppMenuBar, }; use crate::{ About, Open, Quit, SelectLocale, ToggleSearch, themes::{SwitchTheme, SwitchThemeMode}, }; pub fn init(title: impl Into, cx: &mut App) -> Entity { let app_menu_bar = AppMenuBar::new(cx); let title: SharedString = title.into(); update_app_menu(title.clone(), app_menu_bar.clone(), cx); cx.on_action({ let title = title.clone(); let app_menu_bar = app_menu_bar.clone(); move |s: &SelectLocale, cx: &mut App| { rust_i18n::set_locale(&s.0.as_str()); update_app_menu(title.clone(), app_menu_bar.clone(), cx); } }); // Observe theme changes to update the menu to refresh the checked state cx.observe_global::({ let title = title.clone(); let app_menu_bar = app_menu_bar.clone(); move |cx| { update_app_menu(title.clone(), app_menu_bar.clone(), cx); } }) .detach(); app_menu_bar } fn update_app_menu(title: impl Into, app_menu_bar: Entity, cx: &mut App) { let title: SharedString = title.into(); cx.set_menus(build_menus(title.clone(), cx)); let menus = build_menus(title, cx) .into_iter() .map(|menu| menu.owned()) .collect(); GlobalState::global_mut(cx).set_app_menus(menus); app_menu_bar.update(cx, |menu_bar, cx| { menu_bar.reload(cx); }) } fn build_menus(title: impl Into, cx: &App) -> Vec { vec![ Menu { name: title.into(), items: vec![ MenuItem::action("About", About), MenuItem::Separator, MenuItem::action("Open...", Open), MenuItem::Separator, MenuItem::Submenu(Menu { name: "Appearance".into(), items: vec![ MenuItem::action("Light", SwitchThemeMode(ThemeMode::Light)) .checked(!cx.theme().mode.is_dark()), MenuItem::action("Dark", SwitchThemeMode(ThemeMode::Dark)) .checked(cx.theme().mode.is_dark()), ], }), theme_menu(cx), language_menu(cx), MenuItem::Separator, MenuItem::action("Quit", Quit), ], }, Menu { name: "Edit".into(), items: vec![ MenuItem::action("Undo", gpui_component::input::Undo), MenuItem::action("Redo", gpui_component::input::Redo), MenuItem::separator(), MenuItem::action("Cut", gpui_component::input::Cut), MenuItem::action("Copy", gpui_component::input::Copy), MenuItem::action("Paste", gpui_component::input::Paste), MenuItem::separator(), MenuItem::action("Delete", gpui_component::input::Delete), MenuItem::action( "Delete Previous Word", gpui_component::input::DeleteToPreviousWordStart, ), MenuItem::action( "Delete Next Word", gpui_component::input::DeleteToNextWordEnd, ), MenuItem::separator(), MenuItem::action("Find", gpui_component::input::Search), MenuItem::separator(), MenuItem::action("Select All", gpui_component::input::SelectAll), ], }, Menu { name: "Window".into(), items: vec![MenuItem::action("Toggle Search", ToggleSearch)], }, Menu { name: "Help".into(), items: vec![MenuItem::action("Open Website", Open)], }, ] } fn language_menu(_: &App) -> MenuItem { let locale = rust_i18n::locale().to_string(); MenuItem::Submenu(Menu { name: "Language".into(), items: vec![ MenuItem::action("English", SelectLocale("en".into())).checked(locale == "en"), MenuItem::action("简体中文", SelectLocale("zh-CN".into())).checked(locale == "zh-CN"), ], }) } fn theme_menu(cx: &App) -> MenuItem { let themes = ThemeRegistry::global(cx).sorted_themes(); let current_name = cx.theme().theme_name(); MenuItem::Submenu(Menu { name: "Theme".into(), items: themes .iter() .map(|theme| { let checked = current_name == &theme.name; MenuItem::action(theme.name.clone(), SwitchTheme(theme.name.clone())) .checked(checked) }) .collect(), }) } ================================================ FILE: crates/story/src/embedded_themes.rs ================================================ #[cfg(target_family = "wasm")] use std::collections::HashMap; #[cfg(target_family = "wasm")] pub fn embedded_themes() -> HashMap<&'static str, &'static str> { let mut themes = HashMap::new(); themes.insert("adventure", include_str!("../../../themes/adventure.json")); themes.insert("alduin", include_str!("../../../themes/alduin.json")); themes.insert("asciinema", include_str!("../../../themes/asciinema.json")); themes.insert("ayu", include_str!("../../../themes/ayu.json")); themes.insert( "catppuccin", include_str!("../../../themes/catppuccin.json"), ); themes.insert( "everforest", include_str!("../../../themes/everforest.json"), ); themes.insert( "fahrenheit", include_str!("../../../themes/fahrenheit.json"), ); themes.insert("flexoki", include_str!("../../../themes/flexoki.json")); themes.insert("gruvbox", include_str!("../../../themes/gruvbox.json")); themes.insert("harper", include_str!("../../../themes/harper.json")); themes.insert("hybrid", include_str!("../../../themes/hybrid.json")); themes.insert( "jellybeans", include_str!("../../../themes/jellybeans.json"), ); themes.insert("kibble", include_str!("../../../themes/kibble.json")); themes.insert( "macos-classic", include_str!("../../../themes/macos-classic.json"), ); themes.insert("matrix", include_str!("../../../themes/matrix.json")); themes.insert( "mellifluous", include_str!("../../../themes/mellifluous.json"), ); themes.insert("molokai", include_str!("../../../themes/molokai.json")); themes.insert("solarized", include_str!("../../../themes/solarized.json")); themes.insert("spaceduck", include_str!("../../../themes/spaceduck.json")); themes.insert( "tokyonight", include_str!("../../../themes/tokyonight.json"), ); themes.insert("twilight", include_str!("../../../themes/twilight.json")); themes } ================================================ FILE: crates/story/src/fixtures/counters.json ================================================ [ { "symbol": "AAPL", "name": "Apple Inc.", "market": "US" }, { "symbol": "MSFT", "name": "Microsoft Corp.", "market": "US" }, { "symbol": "GOOGL", "name": "Alphabet Inc. Class A", "market": "US" }, { "symbol": "AMZN", "name": "Amazon.com Inc.", "market": "US" }, { "symbol": "META", "name": "Meta Platforms Inc.", "market": "US" }, { "symbol": "TSLA", "name": "Tesla Inc.", "market": "US" }, { "symbol": "BRK.B", "name": "Berkshire Hathaway Inc. Class B", "market": "US" }, { "symbol": "NVDA", "name": "NVIDIA Corp.", "market": "US" }, { "symbol": "JPM", "name": "JPMorgan Chase & Co.", "market": "US" }, { "symbol": "V", "name": "Visa Inc.", "market": "US" }, { "symbol": "UNH", "name": "UnitedHealth Group Inc.", "market": "US" }, { "symbol": "MA", "name": "Mastercard Inc.", "market": "US" }, { "symbol": "HD", "name": "Home Depot Inc.", "market": "US" }, { "symbol": "PG", "name": "Procter & Gamble Co.", "market": "US" }, { "symbol": "LLY", "name": "Eli Lilly and Co.", "market": "US" }, { "symbol": "BAC", "name": "Bank of America Corp.", "market": "US" }, { "symbol": "XOM", "name": "Exxon Mobil Corp.", "market": "US" }, { "symbol": "KO", "name": "Coca-Cola Co.", "market": "US" }, { "symbol": "MRK", "name": "Merck & Co. Inc.", "market": "US" }, { "symbol": "PEP", "name": "PepsiCo Inc.", "market": "US" }, { "symbol": "ABBV", "name": "AbbVie Inc.", "market": "US" }, { "symbol": "AVGO", "name": "Broadcom Inc.", "market": "US" }, { "symbol": "COST", "name": "Costco Wholesale Corp.", "market": "US" }, { "symbol": "WMT", "name": "Walmart Inc.", "market": "US" }, { "symbol": "MCD", "name": "McDonald's Corp.", "market": "US" }, { "symbol": "ADBE", "name": "Adobe Inc.", "market": "US" }, { "symbol": "CRM", "name": "Salesforce Inc.", "market": "US" }, { "symbol": "NFLX", "name": "Netflix Inc.", "market": "US" }, { "symbol": "TMO", "name": "Thermo Fisher Scientific Inc.", "market": "US" }, { "symbol": "ACN", "name": "Accenture plc", "market": "US" }, { "symbol": "LIN", "name": "Linde plc", "market": "US" }, { "symbol": "TXN", "name": "Texas Instruments Inc.", "market": "US" }, { "symbol": "QCOM", "name": "Qualcomm Inc.", "market": "US" }, { "symbol": "NEE", "name": "NextEra Energy Inc.", "market": "US" }, { "symbol": "PM", "name": "Philip Morris International Inc.", "market": "US" }, { "symbol": "HON", "name": "Honeywell International Inc.", "market": "US" }, { "symbol": "ORCL", "name": "Oracle Corp.", "market": "US" }, { "symbol": "AMGN", "name": "Amgen Inc.", "market": "US" }, { "symbol": "INTC", "name": "Intel Corp.", "market": "US" }, { "symbol": "DHR", "name": "Danaher Corp.", "market": "US" }, { "symbol": "LOW", "name": "Lowe's Companies Inc.", "market": "US" }, { "symbol": "UPS", "name": "United Parcel Service Inc.", "market": "US" }, { "symbol": "SBUX", "name": "Starbucks Corp.", "market": "US" }, { "symbol": "CVX", "name": "Chevron Corp.", "market": "US" }, { "symbol": "GS", "name": "Goldman Sachs Group Inc.", "market": "US" }, { "symbol": "MDT", "name": "Medtronic plc", "market": "US" }, { "symbol": "AXP", "name": "American Express Co.", "market": "US" }, { "symbol": "SPGI", "name": "S&P Global Inc.", "market": "US" }, { "symbol": "BLK", "name": "BlackRock Inc.", "market": "US" }, { "symbol": "SYK", "name": "Stryker Corp.", "market": "US" }, { "symbol": "ISRG", "name": "Intuitive Surgical Inc.", "market": "US" }, { "symbol": "DE", "name": "Deere & Co.", "market": "US" }, { "symbol": "PLD", "name": "Prologis Inc.", "market": "US" }, { "symbol": "SCHW", "name": "Charles Schwab Corp.", "market": "US" }, { "symbol": "BKNG", "name": "Booking Holdings Inc.", "market": "US" }, { "symbol": "CI", "name": "Cigna Group", "market": "US" }, { "symbol": "CB", "name": "Chubb Ltd.", "market": "US" }, { "symbol": "MMC", "name": "Marsh & McLennan Companies Inc.", "market": "US" }, { "symbol": "ADP", "name": "Automatic Data Processing Inc.", "market": "US" }, { "symbol": "LRCX", "name": "Lam Research Corp.", "market": "US" }, { "symbol": "CME", "name": "CME Group Inc.", "market": "US" }, { "symbol": "TGT", "name": "Target Corp.", "market": "US" }, { "symbol": "DUK", "name": "Duke Energy Corp.", "market": "US" }, { "symbol": "GILD", "name": "Gilead Sciences Inc.", "market": "US" }, { "symbol": "AMAT", "name": "Applied Materials Inc.", "market": "US" }, { "symbol": "FISV", "name": "Fiserv Inc.", "market": "US" }, { "symbol": "MO", "name": "Altria Group Inc.", "market": "US" }, { "symbol": "VRTX", "name": "Vertex Pharmaceuticals Inc.", "market": "US" }, { "symbol": "REGN", "name": "Regeneron Pharmaceuticals Inc.", "market": "US" }, { "symbol": "PGR", "name": "Progressive Corp.", "market": "US" }, { "symbol": "ELV", "name": "Elevance Health Inc.", "market": "US" }, { "symbol": "EOG", "name": "EOG Resources Inc.", "market": "US" }, { "symbol": "FDX", "name": "FedEx Corp.", "market": "US" }, { "symbol": "APD", "name": "Air Products and Chemicals Inc.", "market": "US" }, { "symbol": "AON", "name": "Aon plc", "market": "US" }, { "symbol": "ITW", "name": "Illinois Tool Works Inc.", "market": "US" }, { "symbol": "BSX", "name": "Boston Scientific Corp.", "market": "US" }, { "symbol": "GM", "name": "General Motors Co.", "market": "US" }, { "symbol": "HCA", "name": "HCA Healthcare Inc.", "market": "US" }, { "symbol": "GD", "name": "General Dynamics Corp.", "market": "US" }, { "symbol": "ZTS", "name": "Zoetis Inc.", "market": "US" }, { "symbol": "SYY", "name": "Sysco Corp.", "market": "US" }, { "symbol": "AIG", "name": "American International Group Inc.", "market": "US" }, { "symbol": "MCO", "name": "Moody's Corp.", "market": "US" }, { "symbol": "MS", "name": "Morgan Stanley", "market": "US" }, { "symbol": "SLB", "name": "Schlumberger Ltd.", "market": "US" }, { "symbol": "SO", "name": "Southern Co.", "market": "US" }, { "symbol": "WM", "name": "Waste Management Inc.", "market": "US" }, { "symbol": "D", "name": "Dominion Energy Inc.", "market": "US" }, { "symbol": "PSA", "name": "Public Storage", "market": "US" }, { "symbol": "CNC", "name": "Centene Corp.", "market": "US" }, { "symbol": "TRV", "name": "Travelers Companies Inc.", "market": "US" }, { "symbol": "AFL", "name": "Aflac Inc.", "market": "US" }, { "symbol": "VLO", "name": "Valero Energy Corp.", "market": "US" }, { "symbol": "WELL", "name": "Welltower Inc.", "market": "US" }, { "symbol": "F", "name": "Ford Motor Co.", "market": "US" }, { "symbol": "STZ", "name": "Constellation Brands Inc.", "market": "US" }, { "symbol": "NOC", "name": "Northrop Grumman Corp.", "market": "US" }, { "symbol": "KHC", "name": "Kraft Heinz Co.", "market": "US" }, { "symbol": "EXC", "name": "Exelon Corp.", "market": "US" }, { "symbol": "CARR", "name": "Carrier Global Corp.", "market": "US" }, { "symbol": "HES", "name": "Hess Corp.", "market": "US" }, { "symbol": "WMB", "name": "Williams Companies Inc.", "market": "US" }, { "symbol": "VTR", "name": "Ventas Inc.", "market": "US" }, { "symbol": "DOW", "name": "Dow Inc.", "market": "US" }, { "symbol": "BKR", "name": "Baker Hughes Co.", "market": "US" }, { "symbol": "OKE", "name": "ONEOK Inc.", "market": "US" }, { "symbol": "NUE", "name": "Nucor Corp.", "market": "US" }, { "symbol": "CPB", "name": "Campbell Soup Co.", "market": "US" }, { "symbol": "CL", "name": "Colgate-Palmolive Co.", "market": "US" }, { "symbol": "EMR", "name": "Emerson Electric Co.", "market": "US" }, { "symbol": "ETN", "name": "Eaton Corp. plc", "market": "US" }, { "symbol": "ROK", "name": "Rockwell Automation Inc.", "market": "US" }, { "symbol": "IFF", "name": "International Flavors & Fragrances Inc.", "market": "US" }, { "symbol": "XYL", "name": "Xylem Inc.", "market": "US" }, { "symbol": "WAB", "name": "Westinghouse Air Brake Technologies Corp.", "market": "US" }, { "symbol": "TDG", "name": "TransDigm Group Inc.", "market": "US" }, { "symbol": "MAS", "name": "Masco Corp.", "market": "US" }, { "symbol": "AWK", "name": "American Water Works Co. Inc.", "market": "US" }, { "symbol": "PNR", "name": "Pentair plc", "market": "US" }, { "symbol": "AOS", "name": "A. O. Smith Corp.", "market": "US" }, { "symbol": "NDSN", "name": "Nordson Corp.", "market": "US" }, { "symbol": "ALB", "name": "Albemarle Corp.", "market": "US" }, { "symbol": "APTV", "name": "Aptiv PLC", "market": "US" }, { "symbol": "BWA", "name": "BorgWarner Inc.", "market": "US" }, { "symbol": "CUM", "name": "Cummins Inc.", "market": "US" }, { "symbol": "DOV", "name": "Dover Corp.", "market": "US" }, { "symbol": "FLS", "name": "Flowserve Corp.", "market": "US" }, { "symbol": "GWW", "name": "W.W. Grainger Inc.", "market": "US" }, { "symbol": "IR", "name": "Ingersoll Rand Inc.", "market": "US" }, { "symbol": "IT", "name": "Gartner Inc.", "market": "US" }, { "symbol": "J", "name": "Jacobs Solutions Inc.", "market": "US" }, { "symbol": "LECO", "name": "Lincoln Electric Holdings Inc.", "market": "US" }, { "symbol": "MIDD", "name": "Middleby Corp.", "market": "US" }, { "symbol": "MLI", "name": "Mueller Industries Inc.", "market": "US" }, { "symbol": "NVT", "name": "nVent Electric plc", "market": "US" }, { "symbol": "OSK", "name": "Oshkosh Corp.", "market": "US" }, { "symbol": "PWR", "name": "Quanta Services Inc.", "market": "US" }, { "symbol": "RBC", "name": "Regal Rexnord Corp.", "market": "US" }, { "symbol": "SNA", "name": "Snap-on Inc.", "market": "US" }, { "symbol": "SWK", "name": "Stanley Black & Decker Inc.", "market": "US" }, { "symbol": "TT", "name": "Trane Technologies plc", "market": "US" }, { "symbol": "VMI", "name": "Valmont Industries Inc.", "market": "US" }, { "symbol": "WMS", "name": "Advanced Drainage Systems Inc.", "market": "US" }, { "symbol": "XYL", "name": "Xylem Inc.", "market": "US" }, { "symbol": "0700.HK", "name": "Tencent Holdings Ltd.", "market": "HK" }, { "symbol": "9988.HK", "name": "Alibaba Group Holding Ltd.", "market": "HK" }, { "symbol": "3690.HK", "name": "Meituan", "market": "HK" }, { "symbol": "1299.HK", "name": "AIA Group Ltd.", "market": "HK" }, { "symbol": "0005.HK", "name": "HSBC Holdings plc", "market": "HK" }, { "symbol": "2318.HK", "name": "Ping An Insurance Group Co.", "market": "HK" }, { "symbol": "2382.HK", "name": "Sunny Optical Technology Group Co.", "market": "HK" }, { "symbol": "1810.HK", "name": "Xiaomi Corp.", "market": "HK" }, { "symbol": "0823.HK", "name": "Link Real Estate Investment Trust", "market": "HK" }, { "symbol": "0001.HK", "name": "CK Hutchison Holdings Ltd.", "market": "HK" }, { "symbol": "0027.HK", "name": "Galaxy Entertainment Group Ltd.", "market": "HK" }, { "symbol": "0011.HK", "name": "Hang Seng Bank Ltd.", "market": "HK" }, { "symbol": "0002.HK", "name": "CLP Holdings Ltd.", "market": "HK" }, { "symbol": "0003.HK", "name": "Hong Kong & China Gas Co. Ltd.", "market": "HK" }, { "symbol": "0006.HK", "name": "Power Assets Holdings Ltd.", "market": "HK" }, { "symbol": "0016.HK", "name": "Sun Hung Kai Properties Ltd.", "market": "HK" }, { "symbol": "0017.HK", "name": "New World Development Co. Ltd.", "market": "HK" }, { "symbol": "0023.HK", "name": "Bank of East Asia Ltd.", "market": "HK" }, { "symbol": "0066.HK", "name": "MTR Corp. Ltd.", "market": "HK" }, { "symbol": "0083.HK", "name": "Sino Land Co. Ltd.", "market": "HK" }, { "symbol": "0101.HK", "name": "Hang Lung Properties Ltd.", "market": "HK" }, { "symbol": "0144.HK", "name": "China Merchants Port Holdings Co. Ltd.", "market": "HK" }, { "symbol": "0151.HK", "name": "Want Want China Holdings Ltd.", "market": "HK" }, { "symbol": "0175.HK", "name": "Geely Automobile Holdings Ltd.", "market": "HK" }, { "symbol": "0207.HK", "name": "Joy City Property Ltd.", "market": "HK" }, { "symbol": "0267.HK", "name": "CITIC Ltd.", "market": "HK" }, { "symbol": "0288.HK", "name": "WH Group Ltd.", "market": "HK" }, { "symbol": "0322.HK", "name": "China Oilfield Services Ltd.", "market": "HK" }, { "symbol": "0386.HK", "name": "China Petroleum & Chemical Corp.", "market": "HK" }, { "symbol": "0393.HK", "name": "China Merchants China Direct Investments Ltd.", "market": "HK" }, { "symbol": "0416.HK", "name": "Kwoon Chung Bus Holdings Ltd.", "market": "HK" }, { "symbol": "0451.HK", "name": "Xinyi Glass Holdings Ltd.", "market": "HK" }, { "symbol": "0522.HK", "name": "ASM Pacific Technology Ltd.", "market": "HK" }, { "symbol": "0575.HK", "name": "Pacific Textiles Holdings Ltd.", "market": "HK" }, { "symbol": "0606.HK", "name": "China Agri-Industries Holdings Ltd.", "market": "HK" }, { "symbol": "0669.HK", "name": "Techtronic Industries Co. Ltd.", "market": "HK" }, { "symbol": "0688.HK", "name": "China Overseas Land & Investment Ltd.", "market": "HK" }, { "symbol": "0708.HK", "name": "China Everbright International Ltd.", "market": "HK" }, { "symbol": "0762.HK", "name": "China Unicom (Hong Kong) Ltd.", "market": "HK" }, { "symbol": "0808.HK", "name": "China Oilfield Services Ltd.", "market": "HK" }, { "symbol": "0836.HK", "name": "China Resources Power Holdings Co. Ltd.", "market": "HK" }, { "symbol": "0857.HK", "name": "PetroChina Co. Ltd.", "market": "HK" }, { "symbol": "0883.HK", "name": "CNOOC Ltd.", "market": "HK" }, { "symbol": "0939.HK", "name": "China Construction Bank Corp.", "market": "HK" }, { "symbol": "0941.HK", "name": "China Mobile Ltd.", "market": "HK" }, { "symbol": "0968.HK", "name": "Xinyi Solar Holdings Ltd.", "market": "HK" }, { "symbol": "0992.HK", "name": "Lenovo Group Ltd.", "market": "HK" }, { "symbol": "1038.HK", "name": "CK Infrastructure Holdings Ltd.", "market": "HK" }, { "symbol": "1044.HK", "name": "Hengan International Group Co. Ltd.", "market": "HK" }, { "symbol": "1093.HK", "name": "CSPC Pharmaceutical Group Ltd.", "market": "HK" }, { "symbol": "1109.HK", "name": "China Resources Land Ltd.", "market": "HK" }, { "symbol": "1113.HK", "name": "CK Asset Holdings Ltd.", "market": "HK" }, { "symbol": "1137.HK", "name": "Hong Kong Technology Venture Co. Ltd.", "market": "HK" }, { "symbol": "1177.HK", "name": "Sino Biopharmaceutical Ltd.", "market": "HK" }, { "symbol": "1208.HK", "name": "MMG Ltd.", "market": "HK" }, { "symbol": "1211.HK", "name": "BYD Co. Ltd.", "market": "HK" }, { "symbol": "1288.HK", "name": "Agricultural Bank of China Ltd.", "market": "HK" }, { "symbol": "1336.HK", "name": "New China Life Insurance Co. Ltd.", "market": "HK" }, { "symbol": "1359.HK", "name": "China Cinda Asset Management Co. Ltd.", "market": "HK" }, { "symbol": "1398.HK", "name": "Industrial and Commercial Bank of China Ltd.", "market": "HK" }, { "symbol": "1515.HK", "name": "China Resources Medical Holdings Co. Ltd.", "market": "HK" }, { "symbol": "1585.HK", "name": "Yihai International Holding Ltd.", "market": "HK" }, { "symbol": "1658.HK", "name": "Post Holdings Ltd.", "market": "HK" }, { "symbol": "1772.HK", "name": "Ganfeng Lithium Co. Ltd.", "market": "HK" }, { "symbol": "1816.HK", "name": "CGN Power Co. Ltd.", "market": "HK" }, { "symbol": "1876.HK", "name": "Budweiser Brewing Company APAC Ltd.", "market": "HK" }, { "symbol": "1928.HK", "name": "Sands China Ltd.", "market": "HK" }, { "symbol": "1997.HK", "name": "Wharf Real Estate Investment Co. Ltd.", "market": "HK" }, { "symbol": "2007.HK", "name": "Country Garden Holdings Co. Ltd.", "market": "HK" }, { "symbol": "2018.HK", "name": "AAC Technologies Holdings Inc.", "market": "HK" }, { "symbol": "2020.HK", "name": "ANTA Sports Products Ltd.", "market": "HK" }, { "symbol": "2196.HK", "name": "Fuyao Glass Industry Group Co. Ltd.", "market": "HK" }, { "symbol": "2233.HK", "name": "West China Cement Ltd.", "market": "HK" }, { "symbol": "2313.HK", "name": "Shenzhou International Group Holdings Ltd.", "market": "HK" }, { "symbol": "2328.HK", "name": "PICC Property and Casualty Co. Ltd.", "market": "HK" }, { "symbol": "2331.HK", "name": "Li Ning Co. Ltd.", "market": "HK" }, { "symbol": "2388.HK", "name": "BOC Hong Kong Holdings Ltd.", "market": "HK" }, { "symbol": "2412.HK", "name": "China Merchants Bank Co. Ltd.", "market": "HK" }, { "symbol": "2428.HK", "name": "YTO Express Group Co. Ltd.", "market": "HK" }, { "symbol": "2600.HK", "name": "China Aluminum International Engineering Corp. Ltd.", "market": "HK" }, { "symbol": "2628.HK", "name": "China Life Insurance Co. Ltd.", "market": "HK" }, { "symbol": "2688.HK", "name": "ENN Energy Holdings Ltd.", "market": "HK" }, { "symbol": "2800.HK", "name": "Tracker Fund of Hong Kong", "market": "HK" }, { "symbol": "2822.HK", "name": "CSOP FTSE China A50 ETF", "market": "HK" }, { "symbol": "2883.HK", "name": "China Oilfield Services Ltd.", "market": "HK" }, { "symbol": "2899.HK", "name": "Zijin Mining Group Co. Ltd.", "market": "HK" }, { "symbol": "3328.HK", "name": "Bank of Communications Co. Ltd.", "market": "HK" }, { "symbol": "3988.HK", "name": "Bank of China Ltd.", "market": "HK" }, { "symbol": "BABA", "name": "Alibaba Group Holding Ltd. ADR", "market": "US" }, { "symbol": "JD", "name": "JD.com Inc. ADR", "market": "US" }, { "symbol": "PDD", "name": "PDD Holdings Inc. ADR", "market": "US" }, { "symbol": "NTES", "name": "NetEase Inc. ADR", "market": "US" }, { "symbol": "TAL", "name": "TAL Education Group ADR", "market": "US" }, { "symbol": "YUMC", "name": "Yum China Holdings Inc.", "market": "US" }, { "symbol": "ZTO", "name": "ZTO Express (Cayman) Inc. ADR", "market": "US" }, { "symbol": "BGNE", "name": "BeiGene Ltd. ADR", "market": "US" }, { "symbol": "GDS", "name": "GDS Holdings Ltd. ADR", "market": "US" }, { "symbol": "HUYA", "name": "HUYA Inc. ADR", "market": "US" }, { "symbol": "WB", "name": "Weibo Corp. ADR", "market": "US" }, { "symbol": "IQ", "name": "iQIYI Inc. ADR", "market": "US" }, { "symbol": "MOMO", "name": "Hello Group Inc. ADR", "market": "US" }, { "symbol": "SINA", "name": "Sina Corp.", "market": "US" }, { "symbol": "BIDU", "name": "Baidu Inc. ADR", "market": "US" }, { "symbol": "XPEV", "name": "XPeng Inc. ADR", "market": "US" }, { "symbol": "LI", "name": "Li Auto Inc. ADR", "market": "US" }, { "symbol": "NIO", "name": "NIO Inc. ADR", "market": "US" }, { "symbol": "EH", "name": "EHang Holdings Ltd. ADR", "market": "US" }, { "symbol": "TME", "name": "Tencent Music Entertainment Group ADR", "market": "US" }, { "symbol": "DIDI", "name": "DiDi Global Inc. ADR", "market": "US" }, { "symbol": "QFIN", "name": "360 DigiTech Inc. ADR", "market": "US" }, { "symbol": "KC", "name": "Kingsoft Cloud Holdings Ltd. ADR", "market": "US" }, { "symbol": "JKS", "name": "JinkoSolar Holding Co. Ltd. ADR", "market": "US" }, { "symbol": "SOL", "name": "Emeren Group Ltd. ADR", "market": "US" }, { "symbol": "DQ", "name": "Daqo New Energy Corp. ADR", "market": "US" }, { "symbol": "CAN", "name": "Canaan Inc. ADR", "market": "US" }, { "symbol": "EDU", "name": "New Oriental Education & Technology Group Inc. ADR", "market": "US" }, { "symbol": "ATHM", "name": "Autohome Inc. ADR", "market": "US" }, { "symbol": "BZUN", "name": "Baozun Inc. ADR", "market": "US" }, { "symbol": "LK", "name": "Luckin Coffee Inc. ADR", "market": "US" }, { "symbol": "YJ", "name": "Yunji Inc. ADR", "market": "US" }, { "symbol": "VIOT", "name": "Viomi Technology Co. Ltd. ADR", "market": "US" }, { "symbol": "WBAI", "name": "500.com Ltd. ADR", "market": "US" }, { "symbol": "SFUN", "name": "Fang Holdings Ltd. ADR", "market": "US" }, { "symbol": "CTRP", "name": "Trip.com Group Ltd. ADR", "market": "US" }, { "symbol": "HTHT", "name": "H World Group Ltd. ADR", "market": "US" }, { "symbol": "VIPS", "name": "Vipshop Holdings Ltd. ADR", "market": "US" }, { "symbol": "YY", "name": "JOYY Inc. ADR", "market": "US" }, { "symbol": "ZNH", "name": "China Southern Airlines Co. Ltd. ADR", "market": "US" }, { "symbol": "CEA", "name": "China Eastern Airlines Corp. Ltd. ADR", "market": "US" }, { "symbol": "SNP", "name": "China Petroleum & Chemical Corp. ADR", "market": "US" }, { "symbol": "PTR", "name": "PetroChina Co. Ltd. ADR", "market": "US" }, { "symbol": "ACH", "name": "Aluminum Corp. of China Ltd. ADR", "market": "US" }, { "symbol": "SHI", "name": "Sinopec Shanghai Petrochemical Co. Ltd. ADR", "market": "US" }, { "symbol": "HNP", "name": "Huaneng Power International Inc. ADR", "market": "US" }, { "symbol": "CHA", "name": "China Telecom Corp. Ltd. ADR", "market": "US" }, { "symbol": "CHL", "name": "China Mobile Ltd. ADR", "market": "US" }, { "symbol": "CMCM", "name": "Cheetah Mobile Inc. ADR", "market": "US" }, { "symbol": "CJJD", "name": "China Jo-Jo Drugstores Inc.", "market": "US" }, { "symbol": "CNTF", "name": "China TechFaith Wireless Communication Technology Ltd.", "market": "US" }, { "symbol": "CNET", "name": "ZW Data Action Technologies Inc.", "market": "US" }, { "symbol": "CTK", "name": "CooTek (Cayman) Inc. ADR", "market": "US" }, { "symbol": "DOGZ", "name": "Dogness (International) Corp.", "market": "US" }, { "symbol": "FANH", "name": "Fanhua Inc. ADR", "market": "US" }, { "symbol": "GSMG", "name": "Glory Star New Media Group Holdings Ltd.", "market": "US" }, { "symbol": "JFIN", "name": "Jiayin Group Inc. ADR", "market": "US" }, { "symbol": "KZIA", "name": "Kazia Therapeutics Ltd. ADR", "market": "US" }, { "symbol": "LIZI", "name": "Lizhi Inc. ADR", "market": "US" }, { "symbol": "MDJH", "name": "MDJM Ltd.", "market": "US" }, { "symbol": "MTC", "name": "MMTec Inc.", "market": "US" }, { "symbol": "MY", "name": "MYR Group Inc.", "market": "US" }, { "symbol": "NCTY", "name": "The9 Ltd. ADR", "market": "US" }, { "symbol": "PUYI", "name": "Puyi Inc. ADR", "market": "US" }, { "symbol": "RAAS", "name": "Cloopen Group Holding Ltd. ADR", "market": "US" }, { "symbol": "RENN", "name": "Renren Inc. ADR", "market": "US" }, { "symbol": "SECO", "name": "Secoo Holding Ltd. ADR", "market": "US" }, { "symbol": "SISI", "name": "Shineco Inc.", "market": "US" }, { "symbol": "SOS", "name": "SOS Ltd. ADR", "market": "US" }, { "symbol": "TC", "name": "TuanChe Ltd. ADR", "market": "US" }, { "symbol": "TEDU", "name": "Tarena International Inc. ADR", "market": "US" }, { "symbol": "TIGR", "name": "UP Fintech Holding Ltd. ADR", "market": "US" }, { "symbol": "TOUR", "name": "Tuniu Corp. ADR", "market": "US" }, { "symbol": "WAFU", "name": "Wah Fu Education Group Ltd.", "market": "US" }, { "symbol": "XNET", "name": "Xunlei Ltd. ADR", "market": "US" }, { "symbol": "YRD", "name": "Yiren Digital Ltd. ADR", "market": "US" }, { "symbol": "ZEPP", "name": "Zepp Health Corp. ADR", "market": "US" }, { "symbol": "ZKIN", "name": "ZK International Group Co. Ltd.", "market": "US" }, { "symbol": "ZLAB", "name": "Zai Lab Ltd. ADR", "market": "US" }, { "symbol": "ZM", "name": "Zoom Video Communications Inc.", "market": "US" }, { "symbol": "SNAP", "name": "Snap Inc.", "market": "US" }, { "symbol": "SQ", "name": "Block Inc.", "market": "US" }, { "symbol": "ROKU", "name": "Roku Inc.", "market": "US" }, { "symbol": "DOCU", "name": "DocuSign Inc.", "market": "US" }, { "symbol": "SHOP", "name": "Shopify Inc.", "market": "US" }, { "symbol": "TWLO", "name": "Twilio Inc.", "market": "US" }, { "symbol": "CRWD", "name": "CrowdStrike Holdings Inc.", "market": "US" }, { "symbol": "OKTA", "name": "Okta Inc.", "market": "US" }, { "symbol": "ZS", "name": "Zscaler Inc.", "market": "US" }, { "symbol": "DDOG", "name": "Datadog Inc.", "market": "US" }, { "symbol": "MDB", "name": "MongoDB Inc.", "market": "US" }, { "symbol": "TEAM", "name": "Atlassian Corp.", "market": "US" }, { "symbol": "FSLY", "name": "Fastly Inc.", "market": "US" }, { "symbol": "NET", "name": "Cloudflare Inc.", "market": "US" }, { "symbol": "PLAN", "name": "Anaplan Inc.", "market": "US" }, { "symbol": "ESTC", "name": "Elastic N.V.", "market": "US" }, { "symbol": "SMAR", "name": "Smartsheet Inc.", "market": "US" }, { "symbol": "WORK", "name": "Slack Technologies Inc.", "market": "US" }, { "symbol": "DOCS", "name": "Doximity Inc.", "market": "US" }, { "symbol": "BILL", "name": "Bill Holdings Inc.", "market": "US" }, { "symbol": "U", "name": "Unity Software Inc.", "market": "US" }, { "symbol": "RBLX", "name": "Roblox Corp.", "market": "US" }, { "symbol": "COIN", "name": "Coinbase Global Inc.", "market": "US" }, { "symbol": "HOOD", "name": "Robinhood Markets Inc.", "market": "US" }, { "symbol": "ABNB", "name": "Airbnb Inc.", "market": "US" }, { "symbol": "LYFT", "name": "Lyft Inc.", "market": "US" }, { "symbol": "UBER", "name": "Uber Technologies Inc.", "market": "US" }, { "symbol": "DD", "name": "DuPont de Nemours Inc.", "market": "US" }, { "symbol": "VEEV", "name": "Veeva Systems Inc.", "market": "US" }, { "symbol": "FTNT", "name": "Fortinet Inc.", "market": "US" }, { "symbol": "PINS", "name": "Pinterest Inc.", "market": "US" }, { "symbol": "ETSY", "name": "Etsy Inc.", "market": "US" }, { "symbol": "ALGN", "name": "Align Technology Inc.", "market": "US" }, { "symbol": "TDOC", "name": "Teladoc Health Inc.", "market": "US" }, { "symbol": "DOCS", "name": "Doximity Inc.", "market": "US" }, { "symbol": "MTCH", "name": "Match Group Inc.", "market": "US" }, { "symbol": "SPLK", "name": "Splunk Inc.", "market": "US" }, { "symbol": "WDAY", "name": "Workday Inc.", "market": "US" }, { "symbol": "HUBS", "name": "HubSpot Inc.", "market": "US" }, { "symbol": "TTD", "name": "The Trade Desk Inc.", "market": "US" }, { "symbol": "PAYC", "name": "Paycom Software Inc.", "market": "US" }, { "symbol": "NOW", "name": "ServiceNow Inc.", "market": "US" }, { "symbol": "SNOW", "name": "Snowflake Inc.", "market": "US" }, { "symbol": "ZS", "name": "Zscaler Inc.", "market": "US" }, { "symbol": "DDOG", "name": "Datadog Inc.", "market": "US" }, { "symbol": "MDB", "name": "MongoDB Inc.", "market": "US" }, { "symbol": "TEAM", "name": "Atlassian Corp.", "market": "US" }, { "symbol": "FSLY", "name": "Fastly Inc.", "market": "US" }, { "symbol": "NET", "name": "Cloudflare Inc.", "market": "US" }, { "symbol": "PLAN", "name": "Anaplan Inc.", "market": "US" }, { "symbol": "ESTC", "name": "Elastic N.V.", "market": "US" }, { "symbol": "SMAR", "name": "Smartsheet Inc.", "market": "US" }, { "symbol": "WORK", "name": "Slack Technologies Inc.", "market": "US" }, { "symbol": "DOCS", "name": "Doximity Inc.", "market": "US" }, { "symbol": "BILL", "name": "Bill Holdings Inc.", "market": "US" }, { "symbol": "U", "name": "Unity Software Inc.", "market": "US" }, { "symbol": "RBLX", "name": "Roblox Corp.", "market": "US" }, { "symbol": "COIN", "name": "Coinbase Global Inc.", "market": "US" }, { "symbol": "HOOD", "name": "Robinhood Markets Inc.", "market": "US" }, { "symbol": "ABNB", "name": "Airbnb Inc.", "market": "US" }, { "symbol": "LYFT", "name": "Lyft Inc.", "market": "US" }, { "symbol": "UBER", "name": "Uber Technologies Inc.", "market": "US" }, { "symbol": "DD", "name": "DuPont de Nemours Inc.", "market": "US" }, { "symbol": "VEEV", "name": "Veeva Systems Inc.", "market": "US" }, { "symbol": "FTNT", "name": "Fortinet Inc.", "market": "US" }, { "symbol": "PINS", "name": "Pinterest Inc.", "market": "US" }, { "symbol": "ETSY", "name": "Etsy Inc.", "market": "US" }, { "symbol": "ALGN", "name": "Align Technology Inc.", "market": "US" }, { "symbol": "TDOC", "name": "Teladoc Health Inc.", "market": "US" }, { "symbol": "MTCH", "name": "Match Group Inc.", "market": "US" }, { "symbol": "SPLK", "name": "Splunk Inc.", "market": "US" }, { "symbol": "WDAY", "name": "Workday Inc.", "market": "US" }, { "symbol": "HUBS", "name": "HubSpot Inc.", "market": "US" }, { "symbol": "TTD", "name": "The Trade Desk Inc.", "market": "US" }, { "symbol": "PAYC", "name": "Paycom Software Inc.", "market": "US" }, { "symbol": "NOW", "name": "ServiceNow Inc.", "market": "US" }, { "symbol": "SNOW", "name": "Snowflake Inc.", "market": "US" }, { "symbol": "DOCU", "name": "DocuSign Inc.", "market": "US" }, { "symbol": "SHOP", "name": "Shopify Inc.", "market": "US" }, { "symbol": "TWLO", "name": "Twilio Inc.", "market": "US" }, { "symbol": "CRWD", "name": "CrowdStrike Holdings Inc.", "market": "US" }, { "symbol": "OKTA", "name": "Okta Inc.", "market": "US" }, { "symbol": "ZS", "name": "Zscaler Inc.", "market": "US" }, { "symbol": "DDOG", "name": "Datadog Inc.", "market": "US" }, { "symbol": "MDB", "name": "MongoDB Inc.", "market": "US" }, { "symbol": "TEAM", "name": "Atlassian Corp.", "market": "US" }, { "symbol": "FSLY", "name": "Fastly Inc.", "market": "US" }, { "symbol": "NET", "name": "Cloudflare Inc.", "market": "US" }, { "symbol": "PLAN", "name": "Anaplan Inc.", "market": "US" }, { "symbol": "ESTC", "name": "Elastic N.V.", "market": "US" }, { "symbol": "SMAR", "name": "Smartsheet Inc.", "market": "US" }, { "symbol": "WORK", "name": "Slack Technologies Inc.", "market": "US" }, { "symbol": "DOCS", "name": "Doximity Inc.", "market": "US" }, { "symbol": "BILL", "name": "Bill Holdings Inc.", "market": "US" }, { "symbol": "U", "name": "Unity Software Inc.", "market": "US" }, { "symbol": "RBLX", "name": "Roblox Corp.", "market": "US" }, { "symbol": "COIN", "name": "Coinbase Global Inc.", "market": "US" }, { "symbol": "HOOD", "name": "Robinhood Markets Inc.", "market": "US" }, { "symbol": "ABNB", "name": "Airbnb Inc.", "market": "US" }, { "symbol": "LYFT", "name": "Lyft Inc.", "market": "US" }, { "symbol": "UBER", "name": "Uber Technologies Inc.", "market": "US" } ] ================================================ FILE: crates/story/src/fixtures/countries.json ================================================ [ { "name": "Afghanistan", "code": "AF" }, { "name": "Åland Islands", "code": "AX" }, { "name": "Albania", "code": "AL" }, { "name": "Algeria", "code": "DZ" }, { "name": "American Samoa", "code": "AS" }, { "name": "AndorrA", "code": "AD" }, { "name": "Angola", "code": "AO" }, { "name": "Anguilla", "code": "AI" }, { "name": "Antarctica", "code": "AQ" }, { "name": "Antigua and Barbuda", "code": "AG" }, { "name": "Argentina", "code": "AR" }, { "name": "Armenia", "code": "AM" }, { "name": "Aruba", "code": "AW" }, { "name": "Australia", "code": "AU" }, { "name": "Austria", "code": "AT" }, { "name": "Azerbaijan", "code": "AZ" }, { "name": "Bahamas", "code": "BS" }, { "name": "Bahrain", "code": "BH" }, { "name": "Bangladesh", "code": "BD" }, { "name": "Barbados", "code": "BB" }, { "name": "Belarus", "code": "BY" }, { "name": "Belgium", "code": "BE" }, { "name": "Belize", "code": "BZ" }, { "name": "Benin", "code": "BJ" }, { "name": "Bermuda", "code": "BM" }, { "name": "Bhutan", "code": "BT" }, { "name": "Bolivia", "code": "BO" }, { "name": "Bosnia and Herzegovina", "code": "BA" }, { "name": "Botswana", "code": "BW" }, { "name": "Bouvet Island", "code": "BV" }, { "name": "Brazil", "code": "BR" }, { "name": "British Indian Ocean Territory", "code": "IO" }, { "name": "Brunei Darussalam", "code": "BN" }, { "name": "Bulgaria", "code": "BG" }, { "name": "Burkina Faso", "code": "BF" }, { "name": "Burundi", "code": "BI" }, { "name": "Cambodia", "code": "KH" }, { "name": "Cameroon", "code": "CM" }, { "name": "Canada", "code": "CA" }, { "name": "Cape Verde", "code": "CV" }, { "name": "Cayman Islands", "code": "KY" }, { "name": "Central African Republic", "code": "CF" }, { "name": "Chad", "code": "TD" }, { "name": "Chile", "code": "CL" }, { "name": "China", "code": "CN" }, { "name": "Christmas Island", "code": "CX" }, { "name": "Cocos (Keeling) Islands", "code": "CC" }, { "name": "Colombia", "code": "CO" }, { "name": "Comoros", "code": "KM" }, { "name": "Congo", "code": "CG" }, { "name": "Congo, The Democratic Republic of the", "code": "CD" }, { "name": "Cook Islands", "code": "CK" }, { "name": "Costa Rica", "code": "CR" }, { "name": "Cote D'Ivoire", "code": "CI" }, { "name": "Croatia", "code": "HR" }, { "name": "Cuba", "code": "CU" }, { "name": "Cyprus", "code": "CY" }, { "name": "Czech Republic", "code": "CZ" }, { "name": "Denmark", "code": "DK" }, { "name": "Djibouti", "code": "DJ" }, { "name": "Dominica", "code": "DM" }, { "name": "Dominican Republic", "code": "DO" }, { "name": "Ecuador", "code": "EC" }, { "name": "Egypt", "code": "EG" }, { "name": "El Salvador", "code": "SV" }, { "name": "Equatorial Guinea", "code": "GQ" }, { "name": "Eritrea", "code": "ER" }, { "name": "Estonia", "code": "EE" }, { "name": "Ethiopia", "code": "ET" }, { "name": "Falkland Islands (Malvinas)", "code": "FK" }, { "name": "Faroe Islands", "code": "FO" }, { "name": "Fiji", "code": "FJ" }, { "name": "Finland", "code": "FI" }, { "name": "France", "code": "FR" }, { "name": "French Guiana", "code": "GF" }, { "name": "French Polynesia", "code": "PF" }, { "name": "French Southern Territories", "code": "TF" }, { "name": "Gabon", "code": "GA" }, { "name": "Gambia", "code": "GM" }, { "name": "Georgia", "code": "GE" }, { "name": "Germany", "code": "DE" }, { "name": "Ghana", "code": "GH" }, { "name": "Gibraltar", "code": "GI" }, { "name": "Greece", "code": "GR" }, { "name": "Greenland", "code": "GL" }, { "name": "Grenada", "code": "GD" }, { "name": "Guadeloupe", "code": "GP" }, { "name": "Guam", "code": "GU" }, { "name": "Guatemala", "code": "GT" }, { "name": "Guernsey", "code": "GG" }, { "name": "Guinea", "code": "GN" }, { "name": "Guinea-Bissau", "code": "GW" }, { "name": "Guyana", "code": "GY" }, { "name": "Haiti", "code": "HT" }, { "name": "Heard Island and Mcdonald Islands", "code": "HM" }, { "name": "Holy See (Vatican City State)", "code": "VA" }, { "name": "Honduras", "code": "HN" }, { "name": "Hong Kong", "code": "HK" }, { "name": "Hungary", "code": "HU" }, { "name": "Iceland", "code": "IS" }, { "name": "India", "code": "IN" }, { "name": "Indonesia", "code": "ID" }, { "name": "Iran, Islamic Republic Of", "code": "IR" }, { "name": "Iraq", "code": "IQ" }, { "name": "Ireland", "code": "IE" }, { "name": "Isle of Man", "code": "IM" }, { "name": "Israel", "code": "IL" }, { "name": "Italy", "code": "IT" }, { "name": "Jamaica", "code": "JM" }, { "name": "Japan", "code": "JP" }, { "name": "Jersey", "code": "JE" }, { "name": "Jordan", "code": "JO" }, { "name": "Kazakhstan", "code": "KZ" }, { "name": "Kenya", "code": "KE" }, { "name": "Kiribati", "code": "KI" }, { "name": "Korea, Democratic People'S Republic of", "code": "KP" }, { "name": "Korea, Republic of", "code": "KR" }, { "name": "Kuwait", "code": "KW" }, { "name": "Kyrgyzstan", "code": "KG" }, { "name": "Lao People'S Democratic Republic", "code": "LA" }, { "name": "Latvia", "code": "LV" }, { "name": "Lebanon", "code": "LB" }, { "name": "Lesotho", "code": "LS" }, { "name": "Liberia", "code": "LR" }, { "name": "Libyan Arab Jamahiriya", "code": "LY" }, { "name": "Liechtenstein", "code": "LI" }, { "name": "Lithuania", "code": "LT" }, { "name": "Luxembourg", "code": "LU" }, { "name": "Macao", "code": "MO" }, { "name": "Macedonia, The Former Yugoslav Republic of", "code": "MK" }, { "name": "Madagascar", "code": "MG" }, { "name": "Malawi", "code": "MW" }, { "name": "Malaysia", "code": "MY" }, { "name": "Maldives", "code": "MV" }, { "name": "Mali", "code": "ML" }, { "name": "Malta", "code": "MT" }, { "name": "Marshall Islands", "code": "MH" }, { "name": "Martinique", "code": "MQ" }, { "name": "Mauritania", "code": "MR" }, { "name": "Mauritius", "code": "MU" }, { "name": "Mayotte", "code": "YT" }, { "name": "Mexico", "code": "MX" }, { "name": "Micronesia, Federated States of", "code": "FM" }, { "name": "Moldova, Republic of", "code": "MD" }, { "name": "Monaco", "code": "MC" }, { "name": "Mongolia", "code": "MN" }, { "name": "Montserrat", "code": "MS" }, { "name": "Morocco", "code": "MA" }, { "name": "Mozambique", "code": "MZ" }, { "name": "Myanmar", "code": "MM" }, { "name": "Namibia", "code": "NA" }, { "name": "Nauru", "code": "NR" }, { "name": "Nepal", "code": "NP" }, { "name": "Netherlands", "code": "NL" }, { "name": "Netherlands Antilles", "code": "AN" }, { "name": "New Caledonia", "code": "NC" }, { "name": "New Zealand", "code": "NZ" }, { "name": "Nicaragua", "code": "NI" }, { "name": "Niger", "code": "NE" }, { "name": "Nigeria", "code": "NG" }, { "name": "Niue", "code": "NU" }, { "name": "Norfolk Island", "code": "NF" }, { "name": "Northern Mariana Islands", "code": "MP" }, { "name": "Norway", "code": "NO" }, { "name": "Oman", "code": "OM" }, { "name": "Pakistan", "code": "PK" }, { "name": "Palau", "code": "PW" }, { "name": "Palestinian Territory, Occupied", "code": "PS" }, { "name": "Panama", "code": "PA" }, { "name": "Papua New Guinea", "code": "PG" }, { "name": "Paraguay", "code": "PY" }, { "name": "Peru", "code": "PE" }, { "name": "Philippines", "code": "PH" }, { "name": "Pitcairn", "code": "PN" }, { "name": "Poland", "code": "PL" }, { "name": "Portugal", "code": "PT" }, { "name": "Puerto Rico", "code": "PR" }, { "name": "Qatar", "code": "QA" }, { "name": "Reunion", "code": "RE" }, { "name": "Romania", "code": "RO" }, { "name": "Russian Federation", "code": "RU" }, { "name": "RWANDA", "code": "RW" }, { "name": "Saint Helena", "code": "SH" }, { "name": "Saint Kitts and Nevis", "code": "KN" }, { "name": "Saint Lucia", "code": "LC" }, { "name": "Saint Pierre and Miquelon", "code": "PM" }, { "name": "Saint Vincent and the Grenadines", "code": "VC" }, { "name": "Samoa", "code": "WS" }, { "name": "San Marino", "code": "SM" }, { "name": "Sao Tome and Principe", "code": "ST" }, { "name": "Saudi Arabia", "code": "SA" }, { "name": "Senegal", "code": "SN" }, { "name": "Serbia and Montenegro", "code": "CS" }, { "name": "Seychelles", "code": "SC" }, { "name": "Sierra Leone", "code": "SL" }, { "name": "Singapore", "code": "SG" }, { "name": "Slovakia", "code": "SK" }, { "name": "Slovenia", "code": "SI" }, { "name": "Solomon Islands", "code": "SB" }, { "name": "Somalia", "code": "SO" }, { "name": "South Africa", "code": "ZA" }, { "name": "South Georgia and the South Sandwich Islands", "code": "GS" }, { "name": "Spain", "code": "ES" }, { "name": "Sri Lanka", "code": "LK" }, { "name": "Sudan", "code": "SD" }, { "name": "Suriname", "code": "SR" }, { "name": "Svalbard and Jan Mayen", "code": "SJ" }, { "name": "Swaziland", "code": "SZ" }, { "name": "Sweden", "code": "SE" }, { "name": "Switzerland", "code": "CH" }, { "name": "Syrian Arab Republic", "code": "SY" }, { "name": "Tajikistan", "code": "TJ" }, { "name": "Tanzania, United Republic of", "code": "TZ" }, { "name": "Thailand", "code": "TH" }, { "name": "Timor-Leste", "code": "TL" }, { "name": "Togo", "code": "TG" }, { "name": "Tokelau", "code": "TK" }, { "name": "Tonga", "code": "TO" }, { "name": "Trinidad and Tobago", "code": "TT" }, { "name": "Tunisia", "code": "TN" }, { "name": "Turkey", "code": "TR" }, { "name": "Turkmenistan", "code": "TM" }, { "name": "Turks and Caicos Islands", "code": "TC" }, { "name": "Tuvalu", "code": "TV" }, { "name": "Uganda", "code": "UG" }, { "name": "Ukraine", "code": "UA" }, { "name": "United Arab Emirates", "code": "AE" }, { "name": "United Kingdom", "code": "GB" }, { "name": "United States", "code": "US" }, { "name": "United States Minor Outlying Islands", "code": "UM" }, { "name": "Uruguay", "code": "UY" }, { "name": "Uzbekistan", "code": "UZ" }, { "name": "Vanuatu", "code": "VU" }, { "name": "Venezuela", "code": "VE" }, { "name": "Viet Nam", "code": "VN" }, { "name": "Virgin Islands, British", "code": "VG" }, { "name": "Virgin Islands, U.S.", "code": "VI" }, { "name": "Wallis and Futuna", "code": "WF" }, { "name": "Western Sahara", "code": "EH" }, { "name": "Yemen", "code": "YE" }, { "name": "Zambia", "code": "ZM" }, { "name": "Zimbabwe", "code": "ZW" } ] ================================================ FILE: crates/story/src/fixtures/daily-devices.json ================================================ [ { "date": "Apr 1", "desktop": 222, "mobile": 111, "tablet": 67, "watch": 28 }, { "date": "Apr 2", "desktop": 97, "mobile": 48, "tablet": 29, "watch": 12 }, { "date": "Apr 3", "desktop": 167, "mobile": 84, "tablet": 50, "watch": 21 }, { "date": "Apr 4", "desktop": 242, "mobile": 121, "tablet": 73, "watch": 30 }, { "date": "Apr 5", "desktop": 373, "mobile": 187, "tablet": 112, "watch": 47 }, { "date": "Apr 6", "desktop": 301, "mobile": 151, "tablet": 91, "watch": 38 }, { "date": "Apr 7", "desktop": 245, "mobile": 123, "tablet": 74, "watch": 31 }, { "date": "Apr 8", "desktop": 409, "mobile": 205, "tablet": 123, "watch": 51 }, { "date": "Apr 9", "desktop": 59, "mobile": 30, "tablet": 18, "watch": 8 }, { "date": "Apr 10", "desktop": 261, "mobile": 131, "tablet": 79, "watch": 33 }, { "date": "Apr 11", "desktop": 327, "mobile": 164, "tablet": 98, "watch": 41 }, { "date": "Apr 12", "desktop": 292, "mobile": 146, "tablet": 88, "watch": 36 }, { "date": "Apr 13", "desktop": 342, "mobile": 171, "tablet": 103, "watch": 43 }, { "date": "Apr 14", "desktop": 137, "mobile": 69, "tablet": 41, "watch": 17 }, { "date": "Apr 15", "desktop": 120, "mobile": 60, "tablet": 36, "watch": 15 }, { "date": "Apr 16", "desktop": 138, "mobile": 69, "tablet": 41, "watch": 17 }, { "date": "Apr 17", "desktop": 446, "mobile": 223, "tablet": 134, "watch": 56 }, { "date": "Apr 18", "desktop": 364, "mobile": 182, "tablet": 109, "watch": 46 }, { "date": "Apr 19", "desktop": 243, "mobile": 122, "tablet": 73, "watch": 30 }, { "date": "Apr 20", "desktop": 89, "mobile": 44, "tablet": 26, "watch": 11 }, { "date": "Apr 21", "desktop": 137, "mobile": 69, "tablet": 41, "watch": 17 }, { "date": "Apr 22", "desktop": 224, "mobile": 112, "tablet": 67, "watch": 28 }, { "date": "Apr 23", "desktop": 138, "mobile": 69, "tablet": 41, "watch": 17 }, { "date": "Apr 24", "desktop": 387, "mobile": 194, "tablet": 116, "watch": 48 }, { "date": "Apr 25", "desktop": 215, "mobile": 108, "tablet": 65, "watch": 27 }, { "date": "Apr 26", "desktop": 75, "mobile": 38, "tablet": 23, "watch": 10 }, { "date": "Apr 27", "desktop": 383, "mobile": 192, "tablet": 115, "watch": 48 }, { "date": "Apr 28", "desktop": 122, "mobile": 61, "tablet": 37, "watch": 15 }, { "date": "Apr 29", "desktop": 315, "mobile": 158, "tablet": 95, "watch": 40 }, { "date": "Apr 30", "desktop": 454, "mobile": 227, "tablet": 136, "watch": 57 }, { "date": "May 1", "desktop": 165, "mobile": 82, "tablet": 49, "watch": 20 }, { "date": "May 2", "desktop": 293, "mobile": 146, "tablet": 88, "watch": 36 }, { "date": "May 3", "desktop": 247, "mobile": 124, "tablet": 74, "watch": 31 }, { "date": "May 4", "desktop": 385, "mobile": 192, "tablet": 115, "watch": 48 }, { "date": "May 5", "desktop": 481, "mobile": 241, "tablet": 145, "watch": 60 }, { "date": "May 6", "desktop": 498, "mobile": 249, "tablet": 149, "watch": 62 }, { "date": "May 7", "desktop": 388, "mobile": 194, "tablet": 116, "watch": 48 }, { "date": "May 8", "desktop": 149, "mobile": 74, "tablet": 44, "watch": 18 }, { "date": "May 9", "desktop": 227, "mobile": 114, "tablet": 68, "watch": 28 }, { "date": "May 10", "desktop": 293, "mobile": 146, "tablet": 88, "watch": 36 }, { "date": "May 11", "desktop": 335, "mobile": 168, "tablet": 101, "watch": 42 }, { "date": "May 12", "desktop": 197, "mobile": 98, "tablet": 59, "watch": 24 }, { "date": "May 13", "desktop": 197, "mobile": 98, "tablet": 59, "watch": 24 }, { "date": "May 14", "desktop": 448, "mobile": 224, "tablet": 134, "watch": 56 }, { "date": "May 15", "desktop": 473, "mobile": 236, "tablet": 142, "watch": 59 }, { "date": "May 16", "desktop": 338, "mobile": 169, "tablet": 101, "watch": 42 }, { "date": "May 17", "desktop": 499, "mobile": 250, "tablet": 150, "watch": 62 }, { "date": "May 18", "desktop": 315, "mobile": 158, "tablet": 95, "watch": 40 }, { "date": "May 19", "desktop": 235, "mobile": 118, "tablet": 71, "watch": 30 }, { "date": "May 20", "desktop": 177, "mobile": 88, "tablet": 53, "watch": 22 }, { "date": "May 21", "desktop": 82, "mobile": 41, "tablet": 25, "watch": 10 }, { "date": "May 22", "desktop": 81, "mobile": 41, "tablet": 25, "watch": 10 }, { "date": "May 23", "desktop": 252, "mobile": 126, "tablet": 76, "watch": 32 }, { "date": "May 24", "desktop": 294, "mobile": 147, "tablet": 88, "watch": 37 }, { "date": "May 25", "desktop": 201, "mobile": 100, "tablet": 60, "watch": 25 }, { "date": "May 26", "desktop": 213, "mobile": 106, "tablet": 64, "watch": 26 }, { "date": "May 27", "desktop": 420, "mobile": 210, "tablet": 126, "watch": 52 }, { "date": "May 28", "desktop": 233, "mobile": 116, "tablet": 70, "watch": 29 }, { "date": "May 29", "desktop": 78, "mobile": 39, "tablet": 23, "watch": 10 }, { "date": "May 30", "desktop": 340, "mobile": 170, "tablet": 102, "watch": 42 }, { "date": "May 31", "desktop": 178, "mobile": 89, "tablet": 53, "watch": 22 }, { "date": "Jun 1", "desktop": 178, "mobile": 89, "tablet": 53, "watch": 22 }, { "date": "Jun 2", "desktop": 470, "mobile": 235, "tablet": 141, "watch": 59 }, { "date": "Jun 3", "desktop": 103, "mobile": 52, "tablet": 31, "watch": 13 }, { "date": "Jun 4", "desktop": 439, "mobile": 220, "tablet": 132, "watch": 55 }, { "date": "Jun 5", "desktop": 88, "mobile": 44, "tablet": 26, "watch": 11 }, { "date": "Jun 6", "desktop": 294, "mobile": 147, "tablet": 88, "watch": 37 }, { "date": "Jun 7", "desktop": 323, "mobile": 162, "tablet": 97, "watch": 40 }, { "date": "Jun 8", "desktop": 385, "mobile": 192, "tablet": 115, "watch": 48 }, { "date": "Jun 9", "desktop": 438, "mobile": 219, "tablet": 131, "watch": 55 }, { "date": "Jun 10", "desktop": 155, "mobile": 78, "tablet": 47, "watch": 20 }, { "date": "Jun 11", "desktop": 92, "mobile": 46, "tablet": 28, "watch": 12 }, { "date": "Jun 12", "desktop": 492, "mobile": 246, "tablet": 148, "watch": 62 }, { "date": "Jun 13", "desktop": 81, "mobile": 41, "tablet": 25, "watch": 10 }, { "date": "Jun 14", "desktop": 426, "mobile": 213, "tablet": 128, "watch": 53 }, { "date": "Jun 15", "desktop": 307, "mobile": 154, "tablet": 92, "watch": 38 }, { "date": "Jun 16", "desktop": 371, "mobile": 186, "tablet": 112, "watch": 46 }, { "date": "Jun 17", "desktop": 475, "mobile": 238, "tablet": 143, "watch": 60 }, { "date": "Jun 18", "desktop": 107, "mobile": 54, "tablet": 32, "watch": 14 }, { "date": "Jun 19", "desktop": 341, "mobile": 171, "tablet": 103, "watch": 43 }, { "date": "Jun 20", "desktop": 408, "mobile": 204, "tablet": 122, "watch": 51 }, { "date": "Jun 21", "desktop": 169, "mobile": 84, "tablet": 50, "watch": 21 }, { "date": "Jun 22", "desktop": 317, "mobile": 158, "tablet": 95, "watch": 40 }, { "date": "Jun 23", "desktop": 480, "mobile": 240, "tablet": 144, "watch": 60 }, { "date": "Jun 24", "desktop": 132, "mobile": 66, "tablet": 40, "watch": 16 }, { "date": "Jun 25", "desktop": 141, "mobile": 70, "tablet": 42, "watch": 18 }, { "date": "Jun 26", "desktop": 434, "mobile": 217, "tablet": 130, "watch": 54 }, { "date": "Jun 27", "desktop": 448, "mobile": 224, "tablet": 134, "watch": 56 }, { "date": "Jun 28", "desktop": 149, "mobile": 74, "tablet": 44, "watch": 18 }, { "date": "Jun 29", "desktop": 103, "mobile": 52, "tablet": 31, "watch": 13 }, { "date": "Jun 30", "desktop": 446, "mobile": 223, "tablet": 134, "watch": 56 } ] ================================================ FILE: crates/story/src/fixtures/monthly-devices.json ================================================ [ { "month": "Jan", "desktop": 186.0, "color_alpha": 0.5 }, { "month": "Feb", "desktop": 305.0, "color_alpha": 0.6 }, { "month": "March", "desktop": 237.0, "color_alpha": 0.7 }, { "month": "April", "desktop": 73.0, "color_alpha": 0.8 }, { "month": "May", "desktop": 209.0, "color_alpha": 0.9 }, { "month": "June", "desktop": 214.0, "color_alpha": 1.0 } ] ================================================ FILE: crates/story/src/fixtures/stock-prices.json ================================================ [ { "date": "Jan", "open": 100.0, "high": 112.0, "low": 95.0, "close": 110.0 }, { "date": "Feb", "open": 110.0, "high": 112.0, "low": 108.0, "close": 111.0 }, { "date": "Mar", "open": 111.0, "high": 118.0, "low": 110.0, "close": 116.0 }, { "date": "Apr", "open": 116.0, "high": 120.0, "low": 108.0, "close": 110.0 }, { "date": "May", "open": 110.0, "high": 118.0, "low": 105.0, "close": 115.0 }, { "date": "Jun", "open": 115.0, "high": 125.0, "low": 113.0, "close": 123.0 } ] ================================================ FILE: crates/story/src/gallery.rs ================================================ use gpui::{prelude::*, *}; use gpui_component::{ ActiveTheme as _, Icon, IconName, h_flex, input::{Input, InputEvent, InputState}, resizable::{h_resizable, resizable_panel}, sidebar::{Sidebar, SidebarGroup, SidebarHeader, SidebarMenu, SidebarMenuItem}, v_flex, }; use crate::*; pub struct Gallery { stories: Vec<(&'static str, Vec>)>, active_group_index: Option, active_index: Option, collapsed: bool, search_input: Entity, _subscriptions: Vec, } impl Gallery { pub fn new(init_story: Option<&str>, window: &mut Window, cx: &mut Context) -> Self { let search_input = cx.new(|cx| InputState::new(window, cx).placeholder("Search...")); let _subscriptions = vec![cx.subscribe(&search_input, |this, _, e, cx| match e { InputEvent::Change => { this.active_group_index = Some(0); this.active_index = Some(0); cx.notify() } _ => {} })]; let stories = vec![ ( "Getting Started", vec![StoryContainer::panel::(window, cx)], ), ( "Components", vec![ StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), ], ), ]; let mut this = Self { search_input, stories, active_group_index: Some(0), active_index: Some(0), collapsed: false, _subscriptions, }; if let Some(init_story) = init_story { this.set_active_story(init_story, window, cx); } this } fn set_active_story(&mut self, name: &str, window: &mut Window, cx: &mut App) { let name = name.to_string(); self.search_input.update(cx, |this, cx| { this.set_value(&name, window, cx); }) } pub fn view(init_story: Option<&str>, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(init_story, window, cx)) } } impl Render for Gallery { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let query = self.search_input.read(cx).value().trim().to_lowercase(); let stories: Vec<_> = self .stories .iter() .filter_map(|(name, items)| { let filtered_items: Vec<_> = items .iter() .filter(|story| story.read(cx).name.to_lowercase().contains(&query)) .cloned() .collect(); if !filtered_items.is_empty() { Some((name, filtered_items)) } else { None } }) .collect(); let active_group = self.active_group_index.and_then(|index| stories.get(index)); let active_story = self .active_index .and(active_group) .and_then(|group| group.1.get(self.active_index.unwrap())); let (story_name, description) = if let Some(story) = active_story.as_ref().map(|story| story.read(cx)) { (story.name.clone(), story.description.clone()) } else { ("".into(), "".into()) }; h_resizable("gallery-container") .child( resizable_panel() .size(px(255.)) .size_range(px(200.)..px(320.)) .child( Sidebar::new("gallery-sidebar") .w(relative(1.)) .border_0() .collapsed(self.collapsed) .header( v_flex() .w_full() .gap_4() .child( SidebarHeader::new() .w_full() .child( div() .flex() .items_center() .justify_center() .rounded(cx.theme().radius_lg) .bg(cx.theme().primary) .text_color(cx.theme().primary_foreground) .size_8() .flex_shrink_0() .when(!self.collapsed, |this| { this.child(Icon::new( IconName::GalleryVerticalEnd, )) }) .when(self.collapsed, |this| { this.size_4() .bg(cx.theme().transparent) .text_color(cx.theme().foreground) .child(Icon::new( IconName::GalleryVerticalEnd, )) }), ) .when(!self.collapsed, |this| { this.child( v_flex() .gap_0() .text_sm() .flex_1() .line_height(relative(1.25)) .overflow_hidden() .text_ellipsis() .child("GPUI Component") .child( div() .text_color( cx.theme().muted_foreground, ) .child("Gallery") .text_xs(), ), ) }), ) .child( div() .bg(cx.theme().sidebar_accent) .rounded_full() .px_1() .when(cx.theme().radius.is_zero(), |this| { this.rounded(px(0.)) }) .flex_1() .mx_1() .child( Input::new(&self.search_input) .appearance(false) .cleanable(true), ), ), ) .children(stories.clone().into_iter().enumerate().map( |(group_ix, (group_name, sub_stories))| { SidebarGroup::new(*group_name).child( SidebarMenu::new().children( sub_stories.iter().enumerate().map(|(ix, story)| { SidebarMenuItem::new(story.read(cx).name.clone()) .active( self.active_group_index == Some(group_ix) && self.active_index == Some(ix), ) .on_click(cx.listener( move |this, _: &ClickEvent, _, cx| { this.active_group_index = Some(group_ix); this.active_index = Some(ix); cx.notify(); }, )) }), ), ) }, )), ), ) .child( v_flex() .flex_1() .h_full() .overflow_x_hidden() .child( h_flex() .id("header") .p_4() .border_b_1() .border_color(cx.theme().border) .justify_between() .items_start() .child( v_flex() .gap_1() .child(div().text_xl().child(story_name)) .child( div() .text_color(cx.theme().muted_foreground) .child(description), ), ), ) .child( div() .id("story") .flex_1() .overflow_y_scroll() .when_some(active_story, |this, active_story| { this.child(active_story.clone()) }), ) .into_any_element(), ) } } ================================================ FILE: crates/story/src/lib.rs ================================================ use gpui::{ Action, AnyElement, AnyView, App, AppContext, Bounds, Context, Div, Entity, EventEmitter, FocusHandle, Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, RenderOnce, SharedString, Size, StyleRefinement, Styled, Window, WindowBounds, WindowKind, WindowOptions, actions, div, prelude::FluentBuilder as _, px, rems, size, }; use gpui_component::{ ActiveTheme, IconName, Root, TitleBar, WindowExt, button::Button, dock::{Panel, PanelControl, PanelEvent, PanelInfo, PanelState, TitleStyle, register_panel}, group_box::{GroupBox, GroupBoxVariants as _}, h_flex, menu::PopupMenu, notification::Notification, scroll::{ScrollableElement as _, ScrollbarShow}, text::markdown, v_flex, }; use serde::{Deserialize, Serialize}; mod app_menus; mod embedded_themes; mod gallery; mod stories; mod themes; mod title_bar; pub use crate::title_bar::AppTitleBar; pub use gallery::Gallery; pub use stories::*; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = story, no_json)] pub struct SelectScrollbarShow(ScrollbarShow); #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = story, no_json)] pub struct SelectLocale(SharedString); #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = story, no_json)] pub struct SelectFont(usize); #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = story, no_json)] pub struct SelectRadius(usize); actions!( story, [ About, Open, Quit, ToggleSearch, TestAction, Tab, TabPrev, ShowPanelInfo, ToggleListActiveHighlight ] ); const PANEL_NAME: &str = "StoryContainer"; pub struct AppState { pub invisible_panels: Entity>, } impl AppState { fn init(cx: &mut App) { let state = Self { invisible_panels: cx.new(|_| Vec::new()), }; cx.set_global::(state); } pub fn global(cx: &App) -> &Self { cx.global::() } pub fn global_mut(cx: &mut App) -> &mut Self { cx.global_mut::() } } pub fn create_new_window(title: &str, crate_view_fn: F, cx: &mut App) where E: Into, F: FnOnce(&mut Window, &mut App) -> E + Send + 'static, { create_new_window_with_size(title, None, crate_view_fn, cx); } pub fn create_new_window_with_size( title: &str, window_size: Option>, crate_view_fn: F, cx: &mut App, ) where E: Into, F: FnOnce(&mut Window, &mut App) -> E + Send + 'static, { let mut window_size = window_size.unwrap_or(size(px(1600.0), px(1200.0))); if let Some(display) = cx.primary_display() { let display_size = display.bounds().size; window_size.width = window_size.width.min(display_size.width * 0.85); window_size.height = window_size.height.min(display_size.height * 0.85); } let window_bounds = Bounds::centered(None, window_size, cx); let title = SharedString::from(title.to_string()); cx.spawn(async move |cx| { let options = WindowOptions { window_bounds: Some(WindowBounds::Windowed(window_bounds)), titlebar: Some(TitleBar::title_bar_options()), window_min_size: Some(gpui::Size { width: px(480.), height: px(320.), }), kind: WindowKind::Normal, #[cfg(target_os = "linux")] window_background: gpui::WindowBackgroundAppearance::Transparent, #[cfg(target_os = "linux")] window_decorations: Some(gpui::WindowDecorations::Client), ..Default::default() }; let window = cx .open_window(options, |window, cx| { let view = crate_view_fn(window, cx); let story_root = cx.new(|cx| StoryRoot::new(title.clone(), view, window, cx)); // Set focus to the StoryRoot to enable it's actions. let focus_handle = story_root.focus_handle(cx); window.defer(cx, move |window, cx| { focus_handle.focus(window, cx); }); cx.new(|cx| Root::new(story_root, window, cx)) }) .expect("failed to open window"); window.update(cx, |_, window, _| { window.activate_window(); window.set_window_title(&title); })?; Ok::<_, anyhow::Error>(()) }) .detach(); } impl Global for AppState {} pub fn init(cx: &mut App) { // Try to initialize tracing subscriber, but ignore if already initialized #[cfg(not(target_family = "wasm"))] { use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _}; let _ = tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer()) .with( tracing_subscriber::EnvFilter::from_default_env() .add_directive("gpui_component=trace".parse().unwrap()), ) .try_init(); } // For WASM, use a subscriber without time support #[cfg(target_family = "wasm")] { use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _}; let _ = tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer().without_time()) .with( tracing_subscriber::EnvFilter::from_default_env() .add_directive("gpui_component=trace".parse().unwrap()), ) .try_init(); } gpui_component::init(cx); AppState::init(cx); themes::init(cx); stories::init(cx); #[cfg(not(target_family = "wasm"))] { let http_client = reqwest_client::ReqwestClient::user_agent("gpui-component/story").unwrap(); cx.set_http_client(std::sync::Arc::new(http_client)); } #[cfg(target_family = "wasm")] { // Safety: the web examples run single-threaded; the client is // created and used exclusively on the main thread. let http_client = unsafe { gpui_web::FetchHttpClient::with_user_agent("gpui-component/story") .expect("failed to create FetchHttpClient") }; cx.set_http_client(std::sync::Arc::new(http_client)); } cx.bind_keys([ KeyBinding::new("/", ToggleSearch, None), #[cfg(target_os = "macos")] KeyBinding::new("cmd-o", Open, None), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-o", Open, None), #[cfg(target_os = "macos")] KeyBinding::new("cmd-q", Quit, None), #[cfg(not(target_os = "macos"))] KeyBinding::new("alt-f4", Quit, None), ]); cx.on_action(|_: &Quit, cx: &mut App| { cx.quit(); }); cx.on_action(|_: &About, cx: &mut App| { if let Some(window) = cx.active_window().and_then(|w| w.downcast::()) { cx.defer(move |cx| { window .update(cx, |_, window, cx| { window.defer(cx, |window, cx| { window.open_alert_dialog(cx, |alert, _, _| { alert.title("About").description(markdown( "GPUI Component Storybook\n\n\ Version 0.1.0\n\n\ https://longbridge.github.io/gpui-component", )) }); }); }) .unwrap(); }); } }); register_panel(cx, PANEL_NAME, |_, _, info, window, cx| { let story_state = match info { PanelInfo::Panel(value) => StoryState::from_value(value.clone()), _ => { unreachable!("Invalid PanelInfo: {:?}", info) } }; let view = cx.new(|cx| { let (title, description, closable, zoomable, story, on_active) = story_state.to_story(window, cx); let mut container = StoryContainer::new(window, cx) .story(story, story_state.story_klass) .on_active(on_active); cx.on_focus_in( &container.focus_handle, window, |this: &mut StoryContainer, _, _| { println!("StoryContainer focus in: {}", this.name); }, ) .detach(); container.name = title.into(); container.description = description.into(); container.closable = closable; container.zoomable = zoomable; container }); Box::new(view) }); cx.activate(true); } #[derive(IntoElement)] struct StorySection { base: Div, title: SharedString, sub_title: Vec, children: Vec, } impl StorySection { pub fn sub_title(mut self, sub_title: impl IntoElement) -> Self { self.sub_title.push(sub_title.into_any_element()); self } #[allow(unused)] fn max_w_md(mut self) -> Self { self.base = self.base.max_w(rems(48.)); self } #[allow(unused)] fn max_w_lg(mut self) -> Self { self.base = self.base.max_w(rems(64.)); self } #[allow(unused)] fn max_w_xl(mut self) -> Self { self.base = self.base.max_w(rems(80.)); self } #[allow(unused)] fn max_w_2xl(mut self) -> Self { self.base = self.base.max_w(rems(96.)); self } } impl ParentElement for StorySection { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } impl Styled for StorySection { fn style(&mut self) -> &mut gpui::StyleRefinement { self.base.style() } } impl RenderOnce for StorySection { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { GroupBox::new() .id(self.title.clone()) .outline() .title( h_flex() .justify_between() .w_full() .gap_4() .child(self.title) .children(self.sub_title), ) .content_style( StyleRefinement::default() .rounded(cx.theme().radius_lg) .overflow_x_hidden() .items_center() .justify_center(), ) .child(self.base.children(self.children)) } } pub(crate) fn section(title: impl Into) -> StorySection { StorySection { title: title.into(), sub_title: vec![], base: h_flex() .w_full() .flex_wrap() .justify_center() .items_center() .gap_4(), children: vec![], } } pub struct StoryContainer { focus_handle: gpui::FocusHandle, pub name: SharedString, pub title_bg: Option, pub description: SharedString, width: Option, height: Option, story: Option, story_klass: Option, closable: bool, zoomable: Option, paddings: Pixels, on_active: Option, } #[derive(Debug)] pub enum ContainerEvent { Close, } impl EventEmitter for StoryContainer {} impl StoryContainer { pub fn new(_window: &mut Window, cx: &mut App) -> Self { let focus_handle = cx.focus_handle(); Self { focus_handle, name: "".into(), title_bg: None, description: "".into(), width: None, height: None, story: None, story_klass: None, closable: true, zoomable: Some(PanelControl::default()), paddings: px(16.), on_active: None, } } pub fn panel(window: &mut Window, cx: &mut App) -> Entity { let name = S::title(); let description = S::description(); let story = S::new_view(window, cx); let story_klass = S::klass(); let view = cx.new(|cx| { let mut story = Self::new(window, cx) .story(story.into(), story_klass) .on_active(S::on_active_any); story.focus_handle = cx.focus_handle(); story.closable = S::closable(); story.zoomable = S::zoomable(); story.name = name.into(); story.description = description.into(); story.title_bg = S::title_bg(); story.paddings = S::paddings(); story }); view } pub fn width(mut self, width: gpui::Pixels) -> Self { self.width = Some(width); self } pub fn height(mut self, height: gpui::Pixels) -> Self { self.height = Some(height); self } pub fn story(mut self, story: AnyView, story_klass: impl Into) -> Self { self.story = Some(story); self.story_klass = Some(story_klass.into()); self } pub fn on_active(mut self, on_active: fn(AnyView, bool, &mut Window, &mut App)) -> Self { self.on_active = Some(on_active); self } } #[derive(Debug, Serialize, Deserialize)] pub struct StoryState { pub story_klass: SharedString, } impl StoryState { fn to_value(&self) -> serde_json::Value { serde_json::json!({ "story_klass": self.story_klass, }) } fn from_value(value: serde_json::Value) -> Self { serde_json::from_value(value).unwrap() } fn to_story( &self, window: &mut Window, cx: &mut App, ) -> ( &'static str, &'static str, bool, Option, AnyView, fn(AnyView, bool, &mut Window, &mut App), ) { macro_rules! story { ($klass:tt) => { ( $klass::title(), $klass::description(), $klass::closable(), $klass::zoomable(), $klass::view(window, cx).into(), $klass::on_active_any, ) }; } match self.story_klass.to_string().as_str() { "BreadcrumbStory" => story!(BreadcrumbStory), "ButtonStory" => story!(ButtonStory), "CalendarStory" => story!(CalendarStory), "SelectStory" => story!(SelectStory), "IconStory" => story!(IconStory), "ImageStory" => story!(ImageStory), "InputStory" => story!(InputStory), "ListStory" => story!(ListStory), "DialogStory" => story!(DialogStory), "DividerStory" => story!(DividerStory), "PopoverStory" => story!(PopoverStory), "ProgressStory" => story!(ProgressStory), "ResizableStory" => story!(ResizableStory), "ScrollbarStory" => story!(ScrollbarStory), "SwitchStory" => story!(SwitchStory), "DataTableStory" => story!(DataTableStory), "TableStory" => story!(TableStory), "LabelStory" => story!(LabelStory), "TooltipStory" => story!(TooltipStory), "AccordionStory" => story!(AccordionStory), "SidebarStory" => story!(SidebarStory), "FormStory" => story!(FormStory), "NotificationStory" => story!(NotificationStory), "ThemeColorsStory" => story!(ThemeColorsStory), _ => { unreachable!("Invalid story klass: {}", self.story_klass) } } } } impl Panel for StoryContainer { fn panel_name(&self) -> &'static str { "StoryContainer" } fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { self.name.clone().into_any_element() } fn title_style(&self, cx: &App) -> Option { if let Some(bg) = self.title_bg { Some(TitleStyle { background: bg, foreground: cx.theme().foreground, }) } else { None } } fn closable(&self, _cx: &App) -> bool { self.closable } fn zoomable(&self, _cx: &App) -> Option { self.zoomable } fn visible(&self, cx: &App) -> bool { !AppState::global(cx) .invisible_panels .read(cx) .contains(&self.name) } fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, _cx: &mut Context) { println!("panel: {} zoomed: {}", self.name, zoomed); } fn set_active(&mut self, active: bool, _window: &mut Window, cx: &mut Context) { println!("panel: {} active: {}", self.name, active); if let Some(on_active) = self.on_active { if let Some(story) = self.story.clone() { on_active(story, active, _window, cx); } } } fn dropdown_menu( &mut self, menu: PopupMenu, _window: &mut Window, _cx: &mut Context, ) -> PopupMenu { menu.menu("Info", Box::new(ShowPanelInfo)) } fn toolbar_buttons( &mut self, _window: &mut Window, _cx: &mut Context, ) -> Option> { Some(vec![ Button::new("info") .icon(IconName::Info) .on_click(|_, window, cx| { window.push_notification("You have clicked info button", cx); }), Button::new("search") .icon(IconName::Search) .on_click(|_, window, cx| { window.push_notification("You have clicked search button", cx); }), ]) } fn dump(&self, _cx: &App) -> PanelState { let mut state = PanelState::new(self); let story_state = StoryState { story_klass: self.story_klass.clone().unwrap(), }; state.info = PanelInfo::panel(story_state.to_value()); state } } impl EventEmitter for StoryContainer {} impl Focusable for StoryContainer { fn focus_handle(&self, _: &App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for StoryContainer { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .id("story-container") .size_full() .overflow_y_scrollbar() .track_focus(&self.focus_handle) .when_some(self.story.clone(), |this, story| { this.child(div().size_full().p(self.paddings).child(story)) }) } } pub struct StoryRoot { pub(crate) focus_handle: FocusHandle, pub(crate) title_bar: Entity, pub(crate) view: AnyView, } impl StoryRoot { pub fn new( title: impl Into, view: impl Into, window: &mut Window, cx: &mut Context, ) -> Self { let title_bar = cx.new(|cx| AppTitleBar::new(title, window, cx)); Self { focus_handle: cx.focus_handle(), title_bar, view: view.into(), } } fn on_action_panel_info( &mut self, _: &ShowPanelInfo, window: &mut Window, cx: &mut Context, ) { struct Info; let note = Notification::new() .message("You have clicked panel info.") .id::(); window.push_notification(note, cx); } fn on_action_toggle_search( &mut self, _: &ToggleSearch, window: &mut Window, cx: &mut Context, ) { cx.propagate(); if window.has_focused_input(cx) { return; } struct Search; let note = Notification::new() .message("You have toggled search.") .id::(); window.push_notification(note, cx); } } impl Focusable for StoryRoot { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for StoryRoot { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let sheet_layer = Root::render_sheet_layer(window, cx); let dialog_layer = Root::render_dialog_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); div() .id("story-root") .on_action(cx.listener(Self::on_action_panel_info)) .on_action(cx.listener(Self::on_action_toggle_search)) .size_full() .child( v_flex() .size_full() .child(self.title_bar.clone()) .child( div() .track_focus(&self.focus_handle) .flex_1() .overflow_hidden() .child(self.view.clone()), ) .children(sheet_layer) .children(dialog_layer) .children(notification_layer), ) } } ================================================ FILE: crates/story/src/main.rs ================================================ use gpui_component_assets::Assets; use gpui_component_story::{Gallery, init, create_new_window}; fn main() { let app = gpui_platform::application().with_assets(Assets); // Parse `cargo run -- ` let name = std::env::args().nth(1); app.run(move |cx| { init(cx); cx.activate(true); create_new_window( "GPUI Component", move |window, cx| Gallery::view(name.as_deref(), window, cx), cx, ); }); } ================================================ FILE: crates/story/src/stories/accordion_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement as _, Render, Styled as _, Window, prelude::FluentBuilder as _, }; use gpui_component::{ IconName, Selectable, Sizable, Size, accordion::Accordion, button::{Button, ButtonGroup}, checkbox::Checkbox, h_flex, switch::Switch, v_flex, }; use crate::section; pub struct AccordionStory { open_ixs: Vec, size: Size, bordered: bool, disabled: bool, multiple: bool, show_icon: bool, focus_handle: FocusHandle, } impl super::Story for AccordionStory { fn title() -> &'static str { "Accordion" } fn description() -> &'static str { "The accordion uses collapse internally to make it collapsible." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl AccordionStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { bordered: false, open_ixs: vec![0, 1, 2], size: Size::default(), disabled: false, multiple: true, show_icon: false, focus_handle: cx.focus_handle(), } } fn toggle_accordion(&mut self, open_ixs: Vec, _: &mut Window, cx: &mut Context) { self.open_ixs = open_ixs; cx.notify(); } fn set_size(&mut self, size: Size, _: &mut Window, cx: &mut Context) { self.size = size; cx.notify(); } } impl Focusable for AccordionStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for AccordionStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_5() .child( h_flex() .items_center() .justify_between() .gap_4() .flex_wrap() .child( ButtonGroup::new("toggle-size") .outline() .compact() .child( Button::new("xsmall") .label("XSmall") .selected(self.size == Size::XSmall), ) .child( Button::new("small") .label("Small") .selected(self.size == Size::Small), ) .child( Button::new("medium") .label("Medium") .selected(self.size == Size::Medium), ) .child( Button::new("large") .label("Large") .selected(self.size == Size::Large), ) .on_click(cx.listener(|this, selecteds: &Vec, window, cx| { let size = match selecteds[0] { 0 => Size::XSmall, 1 => Size::Small, 2 => Size::Medium, 3 => Size::Large, _ => unreachable!(), }; this.set_size(size, window, cx); })), ) .child( h_flex() .gap_2() .child( Checkbox::new("multiple") .label("Multiple") .checked(self.multiple) .on_click(cx.listener(|this, checked, _, cx| { this.multiple = *checked; cx.notify(); })), ) .child( Checkbox::new("show_icon") .label("Icon") .checked(self.show_icon) .on_click(cx.listener(|this, checked, _, cx| { this.show_icon = *checked; cx.notify(); })), ) .child( Checkbox::new("disabled") .label("Disabled") .checked(self.disabled) .on_click(cx.listener(|this, checked, _, cx| { this.disabled = *checked; cx.notify(); })), ) .child( Checkbox::new("bordered") .label("Bordered") .checked(self.bordered) .on_click(cx.listener(|this, checked, _, cx| { this.bordered = *checked; cx.notify(); })), ), ), ) .child( section("Normal").max_w_md().child( Accordion::new("test") .bordered(self.bordered) .with_size(self.size) .disabled(self.disabled) .multiple(self.multiple) .item(|this| { this.open(self.open_ixs.contains(&0)) .when(self.show_icon, |this| this.icon(IconName::Info)) .title("Is it accessible?") .child("Yes. It adheres to the WAI-ARIA design pattern.") }) .item(|this| { this.open(self.open_ixs.contains(&1)) .when(self.show_icon, |this| this.icon(IconName::Inbox)) .title("Is it styled with complex elements?") .child( v_flex() .gap_4() .child( "We can put any view here, like a v_flex with a text view", ) .child( h_flex() .gap_4() .child(Switch::new("switch1").label("Switch")) .child( Checkbox::new("checkbox1").label("Or a Checkbox"), ), ), ) }) .item(|this| { this.open(self.open_ixs.contains(&2)) .when(self.show_icon, |this| this.icon(IconName::Moon)) .title("This is third accordion") .child( "This is the third accordion content. \ It can be any view, like a text view or a button.", ) }) .on_toggle_click(cx.listener(|this, open_ixs: &[usize], window, cx| { this.toggle_accordion(open_ixs.to_vec(), window, cx); })), ), ) } } ================================================ FILE: crates/story/src/stories/alert_dialog_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, InteractiveElement as _, IntoElement, ParentElement, Render, Styled, Window, div, px, }; use gpui_component::{ ActiveTheme, Icon, IconName, StyledExt, WindowExt as _, button::{Button, ButtonVariant, ButtonVariants}, dialog::{ AlertDialog, DialogAction, DialogButtonProps, DialogClose, DialogDescription, DialogFooter, DialogHeader, DialogTitle, }, v_flex, }; use crate::section; pub struct AlertDialogStory { focus_handle: FocusHandle, } impl super::Story for AlertDialogStory { fn title() -> &'static str { "AlertDialog" } fn description() -> &'static str { "A modal dialog that interrupts the user with important content" } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl AlertDialogStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), } } } impl Focusable for AlertDialogStory { fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { self.focus_handle.clone() } } impl Render for AlertDialogStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div().id("alert-dialog-story").track_focus(&self.focus_handle).size_full().child( v_flex() .gap_6() .child( section("AlertDialog").child( AlertDialog::new(cx) .p_0() .trigger(Button::new("info-alert").outline().label("Show Info Alert")) .on_ok(|_, window, cx| { window.push_notification("You have confirmed the alert", cx); true }) .on_cancel(|_, window, cx| { window.push_notification("Ok, you canceled the alert", cx); true }) .content(|content, _, cx| { content .child(DialogHeader::new().p_4().child(DialogTitle::new().child("Are you absolutely sure?")).child( DialogDescription::new().child( "This action cannot be undone. \ This will permanently delete your account from our servers.", ), )) .child(DialogFooter::new() .p_4() .border_t_1() .border_color(cx.theme().border) .bg(cx.theme().muted) .child( DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("ok").label("Continue").primary() ) ) ) }), ), ) .child(section("With open_alert_dialog").child( Button::new("confirm-alert").outline().label("Show Confirmation").on_click(cx.listener( |_, _, window, cx| { use gpui_component::dialog::DialogButtonProps; window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete File") .description( "Are you sure you want to delete this file? \ This action cannot be undone.", ) .button_props( DialogButtonProps::default() .ok_variant(ButtonVariant::Danger) .ok_text("Delete") .cancel_text("Cancel") .show_cancel(true), ) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true }) }); }, )), )) .child(section("With Icon").child( AlertDialog::new(cx).w(px(320.)).trigger( Button::new("icon-alert").outline().label("Request Permission"), ).on_ok(|_, window, cx| { window.push_notification("Thank you for allowing network access", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .items_center() .child(Icon::new(IconName::TriangleAlert).size_10().text_color(cx.theme().warning)) .child( DialogTitle::new().child("Network Permission Required"), ).child( DialogDescription::new().child( "We need your permission to access the network to provide better services. \ Please allow network access in your system settings.", ), ), ) .child( DialogFooter::new() .v_flex() .child( DialogAction::new().child( Button::new("agree").w_full().primary().label("Allow") ) ) .child( DialogClose::new().child( Button::new("disagree").w_full().outline().label("Don't Allow") ) ) ) }), )) .child( section("Destructive Action").child( AlertDialog::new(cx) .trigger(Button::new("destructive-action").outline().danger().label("Delete Account")) .on_ok(|_, window, cx| { window.push_notification("Your account has been deleted", cx); true }) .content(|content, _, _| { content .child(DialogHeader::new().child(DialogTitle::new().child("Delete Account")).child( DialogDescription::new().child( "This will permanently delete your account \ and all associated data. This action cannot be undone.", ), )) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").flex_1().outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("delete") .flex_1() .outline() .danger() .label("Delete Forever") ) ) ) }), ), ) .child(section("Without Title").child( Button::new("without-title").outline().label("Dialog without Title").on_click(cx.listener( |_, _, window, cx| { window.open_alert_dialog(cx, |alert, _, _| { alert .confirm() .child("This is a AlertDialog with `confirm` mode.\ Will have OK, CANCEL buttons.") }); }, )), )) .child(section("Session Timeout").child( Button::new("session-timeout").outline().label("Session Timeout").on_click(cx.listener( |_, _, window, cx| { window.open_alert_dialog(cx, |alert, _, _| { alert .on_ok(|_, window, cx| { window.push_notification("Redirecting to login...", cx); true }) .title("Session Expired") .description("Your session has expired due to inactivity.\ Please log in again to continue.") .footer( DialogFooter::new().child( Button::new("sign-in").label("Sign in").primary().flex_1().on_click( move |_, window, cx| { window.push_notification("Redirecting to login...", cx); window.close_dialog(cx); }, ), ) ) }); }, )), )) .child(section("Update Available").child( AlertDialog::new(cx) .trigger(Button::new("update").outline().label("Update Available")) .on_cancel(|_, window, cx| { window.push_notification("Update postponed", cx); true }) .on_ok(|_, window, cx| { window.push_notification("Starting update...", cx); true }) .content( |content, _, cx| { content .child(DialogHeader::new().child(DialogTitle::new().child("Update Available")).child( DialogDescription::new().child( "A new version (v2.0.0) is available.\ This update includes new features and bug fixes.", ), )) .child( DialogFooter::new() .bg(cx.theme().muted) .child( DialogClose::new().child( Button::new("later").flex_1().outline().label("Later") ), ) .child( DialogAction::new().child( Button::new("update-now").flex_1().primary().label("Update Now") ) ) ) }, ), )) .child(section("Keyboard Disabled").child( Button::new("keyboard-disabled").outline().label("Keyboard Disabled").on_click(cx.listener( |_, _, window, cx| { window.open_alert_dialog(cx, |alert, _, _| { alert .title("Important Notice") .description( "Please read this important notice \ carefully before proceeding.", ) .keyboard(false) }); }, )), )) .child(section("With confirm mode").child( Button::new("overlay-closable").outline().label("Confirm Mode").on_click(cx.listener( |_, _, window, cx| { window.open_alert_dialog(cx, |alert, _, _| { alert .confirm() .title("Are you sure?") .child("This is a AlertDialog with `confirm` mode.\ Will have OK, CANCEL buttons.") }); }, )), )) .child(section("Overlay Closable").child( Button::new("overlay-closable").outline().label("Overlay Closable").on_click(cx.listener( |_, _, window, cx| { window.open_alert_dialog(cx, |alert, _, _| { alert .title("Overlay Closable") .description("Click outside this dialog or press ESC to close it.") .overlay_closable(true) }); }, )), )) .child(section("Prevent Close").child( Button::new("prevent-close").outline().label("Prevent Close").on_click(cx.listener( |_, _, window, cx| { window.open_alert_dialog(cx, |alert, _, _| { alert .title("Processing") .close_button(true) .description( "A process is running. \ Click Continue to stop it or Cancel to keep waiting.", ) .button_props(DialogButtonProps::default().ok_text("Continue").show_cancel(true)) .on_ok(|_, window, cx| { // Return false to prevent closing window.push_notification("Cannot close: Process still running", cx); false }) .on_cancel(|_, window, cx| { window.push_notification("Waiting...", cx); false }) }); }, )), )) ) } } ================================================ FILE: crates/story/src/stories/alert_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Window, }; use gpui_component::{ IconName, Selectable as _, Sizable as _, Size, alert::Alert, button::{Button, ButtonGroup}, dock::PanelControl, text::markdown, v_flex, }; use crate::section; pub struct AlertStory { size: Size, banner_visible: bool, focus_handle: gpui::FocusHandle, } impl AlertStory { fn new(_: &mut Window, cx: &mut Context) -> Self { Self { size: Size::default(), banner_visible: true, focus_handle: cx.focus_handle() } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn set_size(&mut self, size: Size, _: &mut Window, cx: &mut Context) { self.size = size; cx.notify(); } } impl super::Story for AlertStory { fn title() -> &'static str { "Alert" } fn description() -> &'static str { "Displays a callout for user attention." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn zoomable() -> Option { None } } impl Focusable for AlertStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for AlertStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( ButtonGroup::new("toggle-size") .outline() .compact() .child( Button::new("xsmall").label("XSmall").selected(self.size == Size::XSmall), ) .child(Button::new("small").label("Small").selected(self.size == Size::Small)) .child( Button::new("medium").label("Medium").selected(self.size == Size::Medium), ) .child(Button::new("large").label("Large").selected(self.size == Size::Large)) .on_click(cx.listener(|this, selecteds: &Vec, window, cx| { let size = match selecteds[0] { 0 => Size::XSmall, 1 => Size::Small, 2 => Size::Medium, 3 => Size::Large, _ => unreachable!(), }; this.set_size(size, window, cx); })), ) .child( section("Default").w_2_3().child( Alert::new( "alert-default", markdown( "This is an alert with icon, title and description (in Markdown).\n\ - This is a **list** item.\n\ - This is another list item.", ), ) .with_size(self.size) .title("Success! Your changes have been saved"), ), ) .child( section("With variant").w_2_3().child( v_flex() .w_full() .gap_3() .child( Alert::info("info1", "This is an info alert.") .with_size(self.size) .title("Info message") .on_close(cx.listener(|_, _, _, _| { println!("Info alert closed"); })), ) .child( Alert::success( "success-1", "You have successfully submitted your form.\n\ Thank you for your submission!", ) .with_size(self.size) .title("Submit Successful"), ) .child( Alert::warning( "warning-1", "This is a warning alert with icon, but no title.\n\ This is second line of text to test is the line-height is correct.", ) .with_size(self.size), ) .child( Alert::error( "error-1", markdown( "Please verify your billing information and try again.\n\ - Check your card details\n\ - Ensure sufficient funds\n\ - Verify billing address", ), ) .with_size(self.size) .title("Unable to process your payment."), ), ), ) .child( section("Banner").w_2_3().child( v_flex() .w_full() .gap_2() .child( Alert::new( "banner-1", "This is a banner alert, it will take \ the full width of the container.", ) .banner() .on_close(cx.listener(|this, _, _, cx| { this.banner_visible = !this.banner_visible; cx.notify(); })) .visible(self.banner_visible) .with_size(self.size), ) .child( Alert::info( "banner-info", "This is a banner alert, it will take the full width of the\ container.", ) .banner() .with_size(self.size), ) .child( Alert::success( "banner-success", "This is a banner alert, it will take the full width of the\ container.", ) .banner() .with_size(self.size), ) .child( Alert::warning( "banner-warning", "This is a banner alert, it will take the full width of the\ container.", ) .banner() .with_size(self.size), ) .child( Alert::error( "banner-error", "This is a banner alert, it will take the full width of the\ container.", ) .banner() .with_size(self.size), ), ), ) .child( section("Custom Icon").w_2_3().child( Alert::new( "other-1", "Custom icon with info alert with long \ long long long long long long long long \ long long long long long long long long long \ long long messageeeeeeeee.", ) .title("Custom Icon") .with_size(self.size) .icon(IconName::Calendar), ), ) } } ================================================ FILE: crates/story/src/stories/avatar_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Window, px, }; use gpui_component::{ ActiveTheme, IconName, Sizable as _, StyledExt, avatar::{Avatar, AvatarGroup}, dock::PanelControl, v_flex, }; use crate::section; pub struct AvatarStory { focus_handle: gpui::FocusHandle, } impl AvatarStory { fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl super::Story for AvatarStory { fn title() -> &'static str { "Avatar" } fn description() -> &'static str { "Avatar is an image that represents a user or organization." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn zoomable() -> Option { None } } impl Focusable for AvatarStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for AvatarStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( section("Avatar with image") .max_w_md() .child( Avatar::new() .name("Jason lee") .src("https://avatars.githubusercontent.com/u/5518?v=4") .with_size(px(100.)), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/28998859?v=4") .large(), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/10757551?s=64&v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/20092316?v=4") .small(), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/150917089?v=4") .xsmall(), ), ) .child( section("Avatar with text") .max_w_md() .child(Avatar::new().name("Jason Lee").large()) .child(Avatar::new().name("Floyd Wang")) .child(Avatar::new().name("xda").small()) .child(Avatar::new().name("ihavecoke").xsmall()), ) .child( section("Placeholder") .max_w_md() .child(Avatar::new().large()) .child(Avatar::new()) .child(Avatar::new().small()) .child(Avatar::new().xsmall()) .child(Avatar::new().placeholder(IconName::Building2)), ) .child( section("Avatar Group") .v_flex() .max_w_md() .child( AvatarGroup::new() .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/5518?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/28998859?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/20092316?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/22312482?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/150917089?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/1253486?v=4"), ), ) .child( AvatarGroup::new() .small() .limit(5) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/5518?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/28998859?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/20092316?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/22312482?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/150917089?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/20337280?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/629429?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/583231?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/1264109?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/2936367?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/1253486?v=4"), ), ) .child( AvatarGroup::new() .xsmall() .limit(6) .ellipsis() .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/5518?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/28998859?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/20092316?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/22312482?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/150917089?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/20337280?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/2936367?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/583231?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/1264109?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/10757551?v=4"), ) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/1506323?v=4"), ), ), ) .child( section("Custom rounded").child( Avatar::new() .src("https://avatars.githubusercontent.com/u/5518?v=4") .with_size(px(100.)) .rounded(px(20.)), ), ) .child( section("Custom Style").child( Avatar::new() .src("https://avatars.githubusercontent.com/u/20092316?v=4") .with_size(px(100.)) .border_3() .border_color(cx.theme().foreground) .shadow_sm(), ), ) } } ================================================ FILE: crates/story/src/stories/badge_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Window, }; use gpui_component::{ avatar::Avatar, badge::Badge, dock::PanelControl, v_flex, ActiveTheme as _, Icon, IconName, Sizable as _, }; use crate::section; pub struct BadgeStory { focus_handle: gpui::FocusHandle, } impl BadgeStory { fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl super::Story for BadgeStory { fn title() -> &'static str { "Badge" } fn description() -> &'static str { "A red dot that indicates the number of unread messages." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn zoomable() -> Option { None } } impl Focusable for BadgeStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for BadgeStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( section("Badge on icon") .max_w_md() .child( Badge::new() .count(3) .child(Icon::new(IconName::Bell).large()), ) .child( Badge::new() .count(103) .child(Icon::new(IconName::Inbox).large()), ), ) .child( section("Badge with count") .max_w_md() .child(Badge::new().count(3).child( Avatar::new().src("https://avatars.githubusercontent.com/u/5518?v=4"), )) .child(Badge::new().count(103).child( Avatar::new().src("https://avatars.githubusercontent.com/u/28998859?v=4"), )), ) .child( section("Badge with icon") .max_w_md() .child( Badge::new() .icon(IconName::Check) .color(cx.theme().cyan) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/5518?v=4"), ), ) .child( Badge::new() .icon(IconName::Star) .color(cx.theme().yellow) .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/20092316?v=4"), ), ), ) .child( section("Badge with dot").max_w_md().child( Badge::new().dot().count(1).child( Avatar::new().src("https://avatars.githubusercontent.com/u/5518?v=4"), ), ), ) .child( section("Badge with color") .max_w_md() .child(Badge::new().count(3).color(cx.theme().blue).child( Avatar::new().src("https://avatars.githubusercontent.com/u/5518?v=4"), )) .child(Badge::new().dot().color(cx.theme().green).count(1).child( Avatar::new().src("https://avatars.githubusercontent.com/u/5518?v=4"), )), ) .child( section("Complex use") .max_w_md() .child( Badge::new().count(212).large().child( Badge::new() .icon(IconName::Check) .large() .color(cx.theme().cyan) .child( Avatar::new() .large() .src("https://avatars.githubusercontent.com/u/5518?v=4"), ), ), ) .child( Badge::new().count(2).color(cx.theme().green).large().child( Badge::new() .icon(IconName::Star) .large() .color(cx.theme().yellow) .child( Avatar::new().large().src( "https://avatars.githubusercontent.com/u/20092316?v=4", ), ), ), ) .child( Badge::new().count(3).color(cx.theme().green).child( Badge::new() .icon(IconName::Asterisk) .color(cx.theme().green) .child( Avatar::new().src( "https://avatars.githubusercontent.com/u/22312482?v=4", ), ), ), ) .child( Badge::new().dot().child( Badge::new() .icon(IconName::Sun) .small() .color(cx.theme().red) .child( Avatar::new().small().src( "https://avatars.githubusercontent.com/u/150917089?v=4", ), ), ), ), ) } } ================================================ FILE: crates/story/src/stories/breadcrumb_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Window, prelude::FluentBuilder as _, }; use gpui_component::{ breadcrumb::{Breadcrumb, BreadcrumbItem}, v_flex, }; use crate::section; pub struct BreadcrumbStory { focus_handle: gpui::FocusHandle, clicked_item: Option, } impl BreadcrumbStory { fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), clicked_item: None, } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl super::Story for BreadcrumbStory { fn title() -> &'static str { "Breadcrumb" } fn description() -> &'static str { "A breadcrumb navigation element that shows the current location in a hierarchy." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl Focusable for BreadcrumbStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for BreadcrumbStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_6() .child( section("Basic Breadcrumb").max_w_md().child( Breadcrumb::new() .child("Home") .child("Documents") .child("Projects"), ), ) .child( section("Click Handlers").max_w_md().child( v_flex() .gap_4() .items_center() .child( Breadcrumb::new() .child("Home") .child(BreadcrumbItem::new("Documents").on_click(cx.listener( |this, _, _, cx| { this.clicked_item = Some("Documents".to_string()); cx.notify(); }, ))) .child(BreadcrumbItem::new("Projects").on_click(cx.listener( |this, _, _, cx| { this.clicked_item = Some("Projects".to_string()); cx.notify(); }, ))) .child(BreadcrumbItem::new("Current").on_click(cx.listener( |this, _, _, cx| { this.clicked_item = Some("Current".to_string()); cx.notify(); }, ))), ) .when_some(self.clicked_item.clone(), |this, item| { this.child(format!("Clicked: {}", item)) }), ), ) } } ================================================ FILE: crates/story/src/stories/button_story.rs ================================================ use gpui::{ Action, App, AppContext as _, Axis, ClickEvent, Context, Entity, Focusable, InteractiveElement, IntoElement, ParentElement as _, Render, Styled as _, Window, prelude::FluentBuilder, px, }; use gpui_component::{ ActiveTheme, Disableable as _, Icon, IconName, Selectable as _, Sizable as _, Theme, button::{Button, ButtonCustomVariant, ButtonGroup, ButtonVariants as _}, checkbox::Checkbox, h_flex, progress::ProgressCircle, v_flex, }; use serde::Deserialize; use crate::section; #[derive(Clone, Action, PartialEq, Eq, Deserialize)] #[action(namespace = button_story, no_json)] enum ButtonAction { Disabled, Loading, Selected, Compact, } pub struct ButtonStory { focus_handle: gpui::FocusHandle, disabled: bool, loading: bool, selected: bool, compact: bool, toggle_multiple: bool, } impl ButtonStory { pub fn view(_: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self { focus_handle: cx.focus_handle(), disabled: false, loading: false, selected: false, compact: false, toggle_multiple: false, }) } fn on_click(ev: &ClickEvent, _: &mut Window, _: &mut App) { println!("Button clicked {:?}", ev); } fn on_hover(hovered: &bool, _: &mut Window, _: &mut App) { println!("Button hovered {:?}", hovered); } } impl super::Story for ButtonStory { fn title() -> &'static str { "Button" } fn description() -> &'static str { "Displays a button or a component that looks like a button." } fn closable() -> bool { false } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl Focusable for ButtonStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for ButtonStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let disabled = self.disabled; let loading = self.loading; let selected = self.selected; let compact = self.compact; let toggle_multiple = self.toggle_multiple; let custom_variant = ButtonCustomVariant::new(cx) .color(cx.theme().magenta) .foreground(cx.theme().magenta) .hover(cx.theme().magenta.opacity(0.1)) .active(cx.theme().magenta); v_flex() .on_action( cx.listener(|this, action: &ButtonAction, _, _| match action { ButtonAction::Disabled => this.disabled = !this.disabled, ButtonAction::Loading => this.loading = !this.loading, ButtonAction::Selected => this.selected = !this.selected, ButtonAction::Compact => this.compact = !this.compact, }), ) .gap_6() .child( h_flex() .gap_3() .child( Checkbox::new("disabled-button") .label("Disabled") .checked(self.disabled) .on_click(cx.listener(|view, _, _, cx| { view.disabled = !view.disabled; cx.notify(); })), ) .child( Checkbox::new("loading-button") .label("Loading") .checked(self.loading) .on_click(cx.listener(|view, _, _, cx| { view.loading = !view.loading; cx.notify(); })), ) .child( Checkbox::new("selected-button") .label("Selected") .checked(self.selected) .on_click(cx.listener(|view, _, _, cx| { view.selected = !view.selected; cx.notify(); })), ) .child( Checkbox::new("compact-button") .label("Compact") .checked(self.compact) .on_click(cx.listener(|view, _, _, cx| { view.compact = !view.compact; cx.notify(); })), ) .child( Checkbox::new("shadow-button") .label("Shadow") .checked(cx.theme().shadow) .on_click(cx.listener(|_, _, window, cx| { let mut theme = cx.theme().clone(); theme.shadow = !theme.shadow; cx.set_global::(theme); window.refresh(); })), ), ) .child( section("Normal Button") .max_w_lg() .child( Button::new("button-0") .label("Default") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click) .on_hover(Self::on_hover), ) .child( Button::new("button-1") .primary() .label("Primary") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click) .on_hover(Self::on_hover), ) .child( Button::new("button-2") .secondary() .label("Secondary") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click) .on_hover(Self::on_hover), ) .child( Button::new("button-4") .danger() .label("Danger") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click) .on_hover(Self::on_hover), ) .child( Button::new("button-4-warning") .warning() .label("Warning") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click) .on_hover(Self::on_hover), ) .child( Button::new("button-4-success") .success() .label("Success") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click) .on_hover(Self::on_hover), ) .child( Button::new("button-5-info") .info() .label("Info") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click) .on_hover(Self::on_hover), ) .child( Button::new("button-5-ghost") .ghost() .label("Ghost") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click) .on_hover(Self::on_hover), ) .child( Button::new("button-5-link") .link() .label("Link") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click) .on_hover(Self::on_hover), ) .child( Button::new("button-5-text") .text() .label("Text") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click) .on_hover(Self::on_hover), ), ) .child( section("Button with Icon") .child( Button::new("button-icon-1") .outline() .label("Confirm") .icon(IconName::Check) .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-icon-2") .outline() .label("Abort") .icon(IconName::Close) .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-icon-3") .outline() .label("Maximize") .icon(Icon::new(IconName::Maximize)) .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-icon-4") .child( h_flex() .items_center() .gap_2() .child("Custom Child") .child(IconName::ChevronDown) .child(IconName::Eye), ) .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-icon-5-ghost") .ghost() .icon(IconName::Check) .label("Confirm") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-icon-6-link") .link() .icon(IconName::Check) .label("Link") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-icon-6-text") .text() .icon(IconName::Check) .label("Text Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ), ) .child( section("With Progress").child( h_flex() .gap_4() .child( Button::new("progress-button-1") .primary() .large() .icon( ProgressCircle::new("circle-progress-1") .color(cx.theme().primary_foreground) .value(25.), ) .label("Installing..."), ) .child( Button::new("progress-button-2") .icon(ProgressCircle::new("circle-progress-2").value(35.)) .label("Installing..."), ) .child( Button::new("progress-button-3") .small() .icon(ProgressCircle::new("circle-progress-3").value(68.)) .label("Installing..."), ) .child( Button::new("progress-button-4") .xsmall() .icon(ProgressCircle::new("circle-progress-4").value(85.)) .label("Installing..."), ), ), ) .child( section("Outline Button") .max_w_lg() .child( Button::new("button-outline-1") .primary() .outline() .label("Primary Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-2") .outline() .label("Normal Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-4-danger") .danger() .outline() .label("Danger Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-4-warning") .warning() .outline() .label("Warning Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-4-success") .success() .outline() .label("Success Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-5-info") .info() .outline() .label("Info Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-5-ghost") .ghost() .outline() .label("Ghost Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-5-link") .link() .outline() .label("Link Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-5-text") .text() .outline() .label("Text Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ), ) .child( section("With Dropdown Caret") .max_w_lg() .child( Button::new("button-outline-1") .primary() .dropdown_caret(true) .label("Primary Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-2") .label("Default Button") .dropdown_caret(true) .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-2") .secondary() .label("Secondary Button") .dropdown_caret(true) .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-5-ghost") .ghost() .dropdown_caret(true) .label("Ghost Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-5-link") .link() .dropdown_caret(true) .label("Link Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-5-text") .outline() .small() .dropdown_caret(true) .label("Small Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ), ) .child( section("Small Size") .child( Button::new("button-6") .label("Primary Button") .icon(IconName::Check) .primary() .small() .loading(true) .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-7") .label("Secondary Button") .small() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-8") .label("Danger Button") .danger() .small() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-8-outline") .label("Outline Button") .outline() .small() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-8-ghost") .label("Ghost Button") .ghost() .small() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-8-link") .label("Link Button") .link() .small() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ), ) .child( section("XSmall Size") .child( Button::new("button-xs-1") .label("Primary Button") .primary() .icon(IconName::Check) .xsmall() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-xs-2") .label("Secondary Button") .xsmall() .loading(true) .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-xs-3") .label("Danger Button") .danger() .xsmall() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-xs-3-ghost") .label("Ghost Button") .ghost() .xsmall() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-xs-3-outline") .label("Outline Button") .outline() .xsmall() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-xs-3-link") .label("Link Button") .link() .xsmall() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ), ) .child( section("Button Group").child( ButtonGroup::new("button-group") .outline() .disabled(disabled) .child( Button::new("button-one") .label("One") .disabled(disabled) .selected(selected) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-two") .label("Two") .disabled(disabled) .selected(selected) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-three") .label("Three") .disabled(disabled) .selected(selected) .when(compact, |this| this.compact()) .on_click(Self::on_click), ), ), ) .child( section("Button Group (Vertical)").child( ButtonGroup::new("button-group-vertical") .outline() .layout(Axis::Vertical) .disabled(disabled) .child( Button::new("button-one") .label("One") .disabled(disabled) .selected(selected) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-two") .label("Two") .disabled(disabled) .selected(selected) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-three") .label("Three") .disabled(disabled) .selected(selected) .when(compact, |this| this.compact()) .on_click(Self::on_click), ), ), ) .child( section("Toggle Button Group") .sub_title( Checkbox::new("multiple-button") .text_sm() .label("Multiple") .checked(toggle_multiple) .on_click(cx.listener(|view, _, _, cx| { view.toggle_multiple = !view.toggle_multiple; cx.notify(); })), ) .child( ButtonGroup::new("toggle-button-group") .outline() .compact() .multiple(toggle_multiple) .child( Button::new("disabled-toggle-button") .label("Disabled") .selected(disabled), ) .child( Button::new("loading-toggle-button") .label("Loading") .selected(loading), ) .child( Button::new("selected-toggle-button") .label("Selected") .selected(selected), ) .child( Button::new("compact-toggle-button") .label("Compact") .selected(compact), ) .on_click(cx.listener(|view, selected: &Vec, _, cx| { view.disabled = selected.contains(&0); view.loading = selected.contains(&1); view.selected = selected.contains(&2); view.compact = selected.contains(&3); cx.notify(); })), ), ) .child( section("Icon Button") .child( Button::new("icon-button-primary") .icon(IconName::Search) .loading_icon(IconName::LoaderCircle) .primary() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ) .child( Button::new("icon-button-secondary") .icon(IconName::Info) .loading(true) .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ) .child( Button::new("icon-button-danger") .icon(IconName::Close) .danger() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ) .child( Button::new("icon-button-small-primary") .icon(IconName::Search) .small() .primary() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ) .child( Button::new("icon-button-outline") .icon(IconName::Search) .outline() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ) .child( Button::new("icon-button-ghost") .icon(IconName::ArrowLeft) .loading_icon(IconName::LoaderCircle) .ghost() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ), ) .child( section("Icon Button") .child( Button::new("icon-button-4") .icon(IconName::Info) .small() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ) .child( Button::new("icon-button-5") .icon(IconName::Close) .small() .danger() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ) .child( Button::new("icon-button-6") .icon(IconName::Search) .small() .primary() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ) .child( Button::new("icon-button-7") .icon(IconName::Info) .xsmall() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ) .child( Button::new("icon-button-8") .icon(IconName::Close) .xsmall() .danger() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ) .child( Button::new("icon-button-9") .icon(IconName::Heart) .size(px(24.)) .ghost() .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()), ), ) .child( section("Custom Button") .child( Button::new("button-6-custom") .custom(custom_variant) .label("Custom Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-6-custom") .outline() .custom(custom_variant) .label("Outline Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ) .child( Button::new("button-outline-6-custom-1") .outline() .icon(IconName::Bell) .custom(custom_variant) .label("Icon Button") .disabled(disabled) .selected(selected) .loading(loading) .when(compact, |this| this.compact()) .on_click(Self::on_click), ), ) } } ================================================ FILE: crates/story/src/stories/calendar_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement as _, Render, Styled as _, Window, }; use gpui_component::{ calendar::{Calendar, CalendarState}, v_flex, }; use crate::section; pub struct CalendarStory { focus_handle: FocusHandle, calendar: Entity, calendar_wide: Entity, calendar_with_disabled_matcher: Entity, } impl super::Story for CalendarStory { fn title() -> &'static str { "Calendar" } fn description() -> &'static str { "A calendar to select a date or date range." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl CalendarStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let calendar = cx.new(|cx| CalendarState::new(window, cx)); let calendar_wide = cx.new(|cx| CalendarState::new(window, cx)); let calendar_with_disabled_matcher = cx.new(|cx| CalendarState::new(window, cx).disabled_matcher(vec![0, 3, 6])); Self { calendar, calendar_wide, calendar_with_disabled_matcher, focus_handle: cx.focus_handle(), } } } impl Focusable for CalendarStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for CalendarStory { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( section("Normal") .max_w_md() .child(Calendar::new(&self.calendar)), ) .child( section("With 3 Months") .max_w_md() .child(Calendar::new(&self.calendar_wide).number_of_months(3)), ) .child( section("With Disabled matcher (Sundays, Wednesdays, Saturdays)") .max_w_md() .child(Calendar::new(&self.calendar_with_disabled_matcher)), ) } } ================================================ FILE: crates/story/src/stories/chart_story/chart_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, Hsla, IntoElement, ParentElement, Render, SharedString, Styled, Window, div, linear_color_stop, linear_gradient, prelude::FluentBuilder, px, }; use gpui_component::{ ActiveTheme, StyledExt, chart::{AreaChart, BarChart, CandlestickChart, LineChart, PieChart}, divider::Divider, dock::PanelControl, h_flex, v_flex, }; use serde::Deserialize; use super::StackedBarChart; use crate::Story; #[derive(Clone, Deserialize)] struct MonthlyDevice { pub month: SharedString, pub desktop: f64, pub color_alpha: f32, } impl MonthlyDevice { pub fn color(&self, color: Hsla) -> Hsla { color.alpha(self.color_alpha) } } #[derive(Clone, Deserialize)] pub struct DailyDevice { pub date: SharedString, pub desktop: f64, pub mobile: f64, pub tablet: f64, pub watch: f64, } #[derive(Clone, Deserialize)] pub struct StockPrice { pub date: SharedString, pub open: f64, pub high: f64, pub low: f64, pub close: f64, } pub struct ChartStory { focus_handle: FocusHandle, daily_devices: Vec, monthly_devices: Vec, stock_prices: Vec, } impl ChartStory { fn new(_: &mut Window, cx: &mut Context) -> Self { let daily_devices = serde_json::from_str::>(include_str!( "../../fixtures/daily-devices.json" )) .unwrap(); let monthly_devices = serde_json::from_str::>(include_str!( "../../fixtures/monthly-devices.json" )) .unwrap(); let stock_prices = serde_json::from_str::>(include_str!( "../../fixtures/stock-prices.json" )) .unwrap(); Self { daily_devices, monthly_devices, stock_prices, focus_handle: cx.focus_handle(), } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl Story for ChartStory { fn title() -> &'static str { "Chart" } fn description() -> &'static str { "Beautiful Charts & Graphs." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn zoomable() -> Option { None } } impl Focusable for ChartStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } fn chart_container( title: &str, chart: impl IntoElement, center: bool, cx: &mut Context, ) -> impl IntoElement { v_flex() .flex_1() .h(px(400.)) .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius_lg) .p_4() .child( div() .when(center, |this| this.text_center()) .font_semibold() .child(title.to_string()), ) .child( div() .when(center, |this| this.text_center()) .text_color(cx.theme().muted_foreground) .text_sm() .child("January-June 2025"), ) .child(div().flex_1().py_4().child(chart)) .child( div() .when(center, |this| this.text_center()) .font_semibold() .text_sm() .child("Trending up by 5.2% this month"), ) .child( div() .when(center, |this| this.text_center()) .text_color(cx.theme().muted_foreground) .text_sm() .child("Showing total visitors for the last 6 months"), ) } impl Render for ChartStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let color = cx.theme().chart_3; v_flex() .size_full() .gap_y_4() .bg(cx.theme().background) .child( div().child(chart_container( "Area Chart - Stacked", AreaChart::new(self.daily_devices.clone()) .x(|d| d.date.clone()) .y(|d| d.desktop) .stroke(cx.theme().chart_1) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_1.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) .y(|d| d.mobile) .stroke(cx.theme().chart_2) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_2.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) .tick_margin(8), false, cx, )), ) .child( h_flex() .flex_wrap() .gap_4() .child(chart_container( "Pie Chart", PieChart::new(self.monthly_devices.clone()) .value(|d| d.desktop as f32) .outer_radius(100.) .color(move |d| d.color(color)), true, cx, )) .child(chart_container( "Pie Chart - Donut", PieChart::new(self.monthly_devices.clone()) .value(|d| d.desktop as f32) .inner_radius(60.) .outer_radius_fn(|d| 100. - d.index as f32 * 4.) .color(move |d| d.color(color)), true, cx, )) .child(chart_container( "Pie Chart - Pad Angle", PieChart::new(self.monthly_devices.clone()) .value(|d| d.desktop as f32) .inner_radius(60.) .outer_radius(100.) .pad_angle(4. / 100.) .color(move |d| d.color(color)), true, cx, )), ) .child(Divider::horizontal()) .child( h_flex() .flex_wrap() .gap_4() .child(chart_container( "Bar Chart", BarChart::new(self.monthly_devices.clone()) .x(|d| d.month.clone()) .y(|d| d.desktop), false, cx, )) .child(chart_container( "Bar Chart - Mixed", BarChart::new(self.monthly_devices.clone()) .x(|d| d.month.clone()) .y(|d| d.desktop) .fill(move |d| d.color(color)), false, cx, )) .child({ let data = self.daily_devices.iter().take(8).cloned().collect(); chart_container( "Bar Chart - Stacked", StackedBarChart::new(data), false, cx, ) }) .child(chart_container( "Bar Chart - Label", BarChart::new(self.monthly_devices.clone()) .x(|d| d.month.clone()) .y(|d| d.desktop) .label(|d| d.desktop.to_string()), false, cx, )), ) .child(Divider::horizontal()) .child( h_flex() .flex_wrap() .gap_4() .child(chart_container( "Line Chart", LineChart::new(self.monthly_devices.clone()) .x(|d| d.month.clone()) .y(|d| d.desktop), false, cx, )) .child(chart_container( "Line Chart - Linear", LineChart::new(self.monthly_devices.clone()) .x(|d| d.month.clone()) .y(|d| d.desktop) .linear(), false, cx, )) .child(chart_container( "Line Chart - Step After", LineChart::new(self.monthly_devices.clone()) .x(|d| d.month.clone()) .y(|d| d.desktop) .step_after(), false, cx, )) .child(chart_container( "Line Chart - Dots", LineChart::new(self.monthly_devices.clone()) .x(|d| d.month.clone()) .y(|d| d.desktop) .dot() .stroke(cx.theme().chart_5), false, cx, )), ) .child(Divider::horizontal()) .child( h_flex() .flex_wrap() .gap_4() .child(chart_container( "Area Chart", AreaChart::new(self.monthly_devices.clone()) .x(|d| d.month.clone()) .y(|d| d.desktop), false, cx, )) .child(chart_container( "Area Chart - Linear", AreaChart::new(self.monthly_devices.clone()) .x(|d| d.month.clone()) .y(|d| d.desktop) .linear(), false, cx, )) .child(chart_container( "Area Chart - Step After", AreaChart::new(self.monthly_devices.clone()) .x(|d| d.month.clone()) .y(|d| d.desktop) .step_after(), false, cx, )) .child(chart_container( "Area Chart - Linear Gradient", AreaChart::new(self.monthly_devices.clone()) .x(|d| d.month.clone()) .y(|d| d.desktop) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_1.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )), false, cx, )), ) .child(Divider::horizontal()) .child( h_flex() .flex_wrap() .gap_4() .child(chart_container( "Candlestick Chart", CandlestickChart::new(self.stock_prices.clone()) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close), false, cx, )) .child(chart_container( "Candlestick Chart - Narrow", CandlestickChart::new(self.stock_prices.clone()) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .body_width_ratio(0.5), false, cx, )) .child(chart_container( "Candlestick Chart - Wide", CandlestickChart::new(self.stock_prices.clone()) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .body_width_ratio(1.0), false, cx, )) .child(chart_container( "Candlestick Chart - Tick Margin", CandlestickChart::new(self.stock_prices.clone()) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .tick_margin(2), false, cx, )), ) } } ================================================ FILE: crates/story/src/stories/chart_story/stacked_bar_chart.rs ================================================ // You can draw any chart you want by using the `Plot`. use gpui::{App, Bounds, Pixels, TextAlign, Window, px}; use gpui_component::{ ActiveTheme, plot::{ AXIS_GAP, AxisText, Grid, IntoPlot, Plot, PlotAxis, scale::{Scale, ScaleBand, ScaleLinear, ScaleOrdinal}, shape::{Bar, Stack, StackSeries}, }, }; use super::DailyDevice; #[derive(IntoPlot)] pub struct StackedBarChart { data: Vec, series: Vec>, } impl StackedBarChart { pub fn new(data: Vec) -> Self { // 1. Calculate the stacked data let series = Stack::new() .data(data.clone()) .keys(vec!["desktop", "mobile", "tablet", "watch"]) .value(move |d: &DailyDevice, key| match key { "desktop" => Some(d.desktop as f32), "mobile" => Some(d.mobile as f32), "tablet" => Some(d.tablet as f32), "watch" => Some(d.watch as f32), _ => None, }) .series(); Self { data, series } } } impl Plot for StackedBarChart { fn paint(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { let width = bounds.size.width.as_f32(); let height = bounds.size.height.as_f32() - AXIS_GAP; // 2. Calculate X/Y scales let x = ScaleBand::new( self.data.iter().map(|v| v.date.clone()).collect(), vec![0., width], ) .padding_inner(0.4) .padding_outer(0.2); let band_width = x.band_width(); let max = self .series .iter() .flat_map(|s| s.points.iter().map(|p| p.y1)) .fold(0., f32::max) as f64; let y = ScaleLinear::new(vec![0., max], vec![height, 10.]); // 3. Draw X axis labels let x_label = self.data.iter().filter_map(|d| { x.tick(&d.date.clone()).map(|x_tick| { AxisText::new( d.date.clone(), x_tick + band_width / 2., cx.theme().muted_foreground, ) .align(TextAlign::Center) }) }); PlotAxis::new() .x(height) .x_label(x_label) .stroke(cx.theme().border) .paint(&bounds, window, cx); // 4. Setup color scale let keys = self.series.iter().map(|s| s.key.clone()).collect(); let colors = vec![ cx.theme().chart_4, cx.theme().chart_3, cx.theme().chart_2, cx.theme().chart_1, ]; let ordinal = ScaleOrdinal::new(keys, colors); // 5. Draw grid lines Grid::new() .y((0..=3).map(|i| height * i as f32 / 4.0).collect()) .stroke(cx.theme().border) .dash_array(&[px(4.), px(2.)]) .paint(&bounds, window); // 6. Draw stacked bars for series in self.series.iter() { let x = x.clone(); let y0 = y.clone(); let y1 = y.clone(); let key = &series.key; let fill = ordinal.map(&key).unwrap_or(cx.theme().chart_4); Bar::new() .data(&series.points) .band_width(band_width) .x(move |d| x.tick(&d.data.date.clone())) .y0(move |d| y0.tick(&(d.y0 as f64)).unwrap_or(height)) .y1(move |d| y1.tick(&(d.y1 as f64))) .fill(move |_| fill) .paint(&bounds, window, cx); } } } ================================================ FILE: crates/story/src/stories/chart_story.rs ================================================ mod chart_story; mod stacked_bar_chart; pub use chart_story::*; pub use stacked_bar_chart::StackedBarChart; ================================================ FILE: crates/story/src/stories/checkbox_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled, Window, div, px, }; use gpui_component::{ ActiveTheme, Disableable as _, Sizable, checkbox::Checkbox, h_flex, text::markdown, v_flex, }; use crate::section; pub struct CheckboxStory { focus_handle: gpui::FocusHandle, check1: bool, check2: bool, check3: bool, check4: bool, check5: bool, } impl super::Story for CheckboxStory { fn title() -> &'static str { "Checkbox" } fn description() -> &'static str { "A control that allows the user to toggle between checked and not checked." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl CheckboxStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), check1: false, check2: false, check3: false, check4: false, check5: false, } } } impl Focusable for CheckboxStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for CheckboxStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .justify_start() .gap_3() .child( section("Checkbox") .child( Checkbox::new("1") .checked(self.check1) .label("A normal checkbox") .on_click(cx.listener(|this, checked: &bool, _, cx| { this.check1 = *checked; cx.notify(); })), ) .child( Checkbox::new("2") .checked(self.check2) .label("Remember me") .on_click(cx.listener(|this, checked: &bool, _, cx| { this.check2 = *checked; cx.notify(); })), ), ) .child( section("Without label").child(Checkbox::new("3").checked(self.check3).on_click( cx.listener(|this, checked: &bool, _, _| { this.check3 = *checked; }), )), ) .child( section("Small size").max_w_md().child( Checkbox::new("4") .small() .checked(self.check4) .label("A small checkbox") .on_click(cx.listener(|this, checked: &bool, _, _| { this.check4 = *checked; })), ), ) .child( section("Large size").max_w_md().child( Checkbox::new("check5") .large() .checked(self.check2) .label("A large checkbox") .on_click(cx.listener(|this, checked: &bool, _, _| { this.check2 = *checked; })), ), ) .child( section("Disabled").max_w_md().child( h_flex() .items_center() .gap_6() .child( Checkbox::new("check3") .label("Disabled Checked") .checked(true) .disabled(true), ) .child( Checkbox::new("check3_1") .label("Disabled Unchecked") .checked(false) .disabled(true), ), ), ) .child( section("Multi-line").child( v_flex().gap_4().child( Checkbox::new("multi-line-checkbox") .w(px(300.)) .checked(self.check4) .label("A multi-line checkbox.") .child(div().text_color(cx.theme().muted_foreground).child( "This is a long long label text that \ should wrap when the text is too long.", )) .on_click(cx.listener(|this, checked: &bool, _, _| { this.check4 = *checked; })), ), ), ) .child( section("Rich description (Markdown)").child( Checkbox::new("longlong-markdown-checkbox") .w(px(300.)) .checked(self.check5) .label("Label with description (Markdown)") .child( div() .text_color(cx.theme().muted_foreground) .child(markdown( "The [long long label](https://github.com) \ text used **Markdown**, \ it should wrap when the text is too long.", )), ) .on_click(cx.listener(|this, checked: &bool, _, _| { this.check5 = *checked; })), ), ) } } ================================================ FILE: crates/story/src/stories/clipboard_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window, }; use gpui_component::{ WindowExt, clipboard::Clipboard, h_flex, input::{Input, InputState}, label::Label, v_flex, }; use crate::section; pub struct ClipboardStory { focus_handle: gpui::FocusHandle, url_state: Entity, masked: bool, } impl super::Story for ClipboardStory { fn title() -> &'static str { "Clipboard" } fn description() -> &'static str { "A button that helps you copy text or other content to your clipboard." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl ClipboardStory { pub(crate) fn new(window: &mut Window, cx: &mut App) -> Self { let url_state = cx.new(|cx| InputState::new(window, cx).default_value("https://github.com")); Self { url_state, focus_handle: cx.focus_handle(), masked: false, } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl Focusable for ClipboardStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for ClipboardStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .justify_start() .gap_3() .child( section("Clipboard").max_w_md().child( h_flex() .gap_2() .child(Label::new("A clipboard button")) .child( Clipboard::new("clipboard1") .value_fn({ let view = cx.entity().clone(); move |_, cx| { SharedString::from(format!( "masked :{}", view.read(cx).masked )) } }) .on_copied(|value, window, cx| { window.push_notification(format!("Copied value: {}", value), cx) }), ), ), ) .child( section("With in an Input").max_w_md().child( Input::new(&self.url_state).suffix( Clipboard::new("clipboard2") .value_fn({ let state = self.url_state.clone(); move |_, cx| state.read(cx).value() }) .on_copied(|value, window, cx| { window.push_notification(format!("Copied value: {}", value), cx) }), ), ), ) } } ================================================ FILE: crates/story/src/stories/collapsible_story.rs ================================================ use gpui::div; use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Window, prelude::FluentBuilder as _, }; use gpui_component::group_box::{GroupBox, GroupBoxVariants as _}; use gpui_component::label::Label; use gpui_component::tag::Tag; use gpui_component::{ActiveTheme, IconName, StyledExt, h_flex}; use gpui_component::{ Sizable, button::{Button, ButtonVariants}, collapsible::Collapsible, v_flex, }; use crate::section; pub struct CollapsibleStory { focus_handle: FocusHandle, item1_open: bool, item2_open: bool, } impl super::Story for CollapsibleStory { fn title() -> &'static str { "Collapsible" } fn description() -> &'static str { "An interactive element that expands/collapses." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl CollapsibleStory { pub(crate) fn new(_: &mut Window, cx: &mut App) -> Self { Self { focus_handle: cx.focus_handle(), item1_open: false, item2_open: false, } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl Focusable for CollapsibleStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for CollapsibleStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let items = [ ["TSLA.US", "$423.00", "+30.25%"], ["NVDA.US", "$312.00", "+12.12%"], ["AAPL.US", "$145.00", "-8.50%"], ]; v_flex() .gap_6() .child( section("Expland Paragraphs").v_flex().child( Collapsible::new() .max_w_128() .gap_1() .open(self.item1_open) .child( "This is a collapsible component. \ Click the header to expand or collapse the content.", ) .content( "This is the full content of the Collapsible component. \ It is only visible when the component is expanded. \n\ You can put any content you like here, including text, images, \ or other UI elements. ", ) .child( h_flex().justify_center().child( Button::new("toggle1") .icon(IconName::ChevronDown) .label("Show more") .when(self.item1_open, |this| { this.icon(IconName::ChevronUp).label("Show less") }) .xsmall() .link() .on_click({ cx.listener(move |this, _, _, cx| { this.item1_open = !this.item1_open; cx.notify(); }) }), ), ), ), ) .child( section("Card").child( GroupBox::new() .outline() .w_80() .title("Collapsible in a Card") .child( Collapsible::new() .gap_1() .open(self.item2_open) .child( h_flex() .justify_between() .child( v_flex().child("Total Return").child( h_flex() .gap_1() .child( Label::new("123.5%") .text_2xl() .font_semibold(), ) .child( Tag::info() .child("+4.5%") .outline() .rounded_full() .small(), ), ), ) .child( Button::new("toggle2") .small() .outline() .icon(IconName::ChevronDown) .label("Details") .when(self.item2_open, |this| { this.icon(IconName::ChevronUp) }) .on_click({ cx.listener(move |this, _, _, cx| { this.item2_open = !this.item2_open; cx.notify(); }) }), ), ) .content(v_flex().gap_2().children(items.iter().map(|item| { let is_up = item[2].starts_with('+'); h_flex().justify_between().child(item[0]).child( h_flex() .flex_1() .justify_end() .gap_4() .child(div().w_16().justify_end().child(item[1])) .child( Label::new(item[2]) .text_xs() .w_16() .justify_end() .when(is_up, |this| { this.text_color(cx.theme().green) }) .when(!is_up, |this| { this.text_color(cx.theme().red) }), ), ) }))), ), ), ) } } ================================================ FILE: crates/story/src/stories/color_picker_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, Hsla, IntoElement, ParentElement as _, Render, Styled as _, Subscription, Window, div, prelude::FluentBuilder as _, }; use gpui_component::{ ActiveTheme as _, Colorize, Sizable, color_picker::{ColorPicker, ColorPickerEvent, ColorPickerState}, v_flex, }; use crate::section; pub struct ColorPickerStory { color: Entity, selected_color: Option, _subscriptions: Vec, } impl super::Story for ColorPickerStory { fn title() -> &'static str { "ColorPicker" } fn description() -> &'static str { "A color picker to select color." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl ColorPickerStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let color = cx.new(|cx| ColorPickerState::new(window, cx).default_value(cx.theme().primary)); let _subscriptions = vec![cx.subscribe(&color, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { this.selected_color = *color; } })]; Self { color, selected_color: Some(cx.theme().primary), _subscriptions, } } } impl Focusable for ColorPickerStory { fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { self.color.read(cx).focus_handle(cx) } } impl Render for ColorPickerStory { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex().gap_3().child( section("Normal") .max_w_md() .child(ColorPicker::new(&self.color).small()) .when_some(self.selected_color, |this, color| { this.child(div().w_24().child(color.to_hex())) }), ) } } ================================================ FILE: crates/story/src/stories/data_table_story.rs ================================================ use std::{ ops::Range, sync::LazyLock, time::{self, Duration}, }; use fake::Fake; use gpui::{ Action, AnyElement, App, AppContext, ClickEvent, Context, Div, Entity, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Window, div, prelude::FluentBuilder as _, }; use gpui_component::{ ActiveTheme as _, Selectable, Sizable as _, Size, StyleSized as _, StyledExt, button::Button, checkbox::Checkbox, h_flex, input::{Input, InputEvent, InputState}, label::Label, menu::{DropdownMenu, PopupMenu}, spinner::Spinner, table::{Column, ColumnFixed, ColumnSort, DataTable, TableDelegate, TableEvent, TableState}, v_flex, }; use serde::{Deserialize, Serialize}; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = data_table_story, no_json)] struct ChangeSize(Size); #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = data_table_story, no_json)] struct OpenDetail(usize); #[derive(Debug, Clone, Serialize, Deserialize, Default)] struct Counter { symbol: SharedString, market: SharedString, name: SharedString, } static ALL_COUNTERS: LazyLock> = LazyLock::new(|| serde_json::from_str(include_str!("../fixtures/counters.json")).unwrap()); static INCREMENT_ID: LazyLock> = LazyLock::new(|| std::sync::Mutex::new(0)); impl Counter { fn random() -> Self { let len = ALL_COUNTERS.len(); let ix = rand::random::() % len; ALL_COUNTERS[ix].clone() } fn symbol_code(&self) -> SharedString { format!("{}.{}", self.symbol, self.market).into() } } #[derive(Clone, Debug, Default)] struct Stock { id: usize, counter: Counter, price: f64, change: f64, change_percent: f64, volume: f64, turnover: f64, market_cap: f64, ttm: f64, five_mins_ranking: f64, th60_days_ranking: f64, year_change_percent: f64, bid: f64, bid_volume: f64, ask: f64, ask_volume: f64, open: f64, prev_close: f64, high: f64, low: f64, turnover_rate: f64, rise_rate: f64, amplitude: f64, pe_status: f64, pb_status: f64, volume_ratio: f64, bid_ask_ratio: f64, latest_pre_close: f64, latest_post_close: f64, pre_market_cap: f64, pre_market_percent: f64, pre_market_change: f64, post_market_cap: f64, post_market_percent: f64, post_market_change: f64, float_cap: f64, shares: i64, shares_float: i64, day_5_ranking: f64, day_10_ranking: f64, day_30_ranking: f64, day_120_ranking: f64, day_250_ranking: f64, } impl Stock { fn random_update(&mut self) { self.price = (-300.0..999.999).fake::(); self.change = (-0.1..5.0).fake::(); self.change_percent = (-0.1..0.1).fake::(); self.volume = (-300.0..999.999).fake::(); self.turnover = (-300.0..999.999).fake::(); self.market_cap = (-1000.0..9999.999).fake::(); self.ttm = (-1000.0..9999.999).fake::(); self.five_mins_ranking = self.five_mins_ranking * (1.0 + (-0.2..0.2).fake::()); self.bid = self.price * (1.0 + (-0.2..0.2).fake::()); self.bid_volume = (100.0..1000.0).fake::(); self.ask = self.price * (1.0 + (-0.2..0.2).fake::()); self.ask_volume = (100.0..1000.0).fake::(); self.bid_ask_ratio = self.bid / self.ask; self.volume_ratio = self.volume / self.turnover; self.high = self.price * (1.0 + (0.0..1.5).fake::()); self.low = self.price * (1.0 + (-1.5..0.0).fake::()); } } fn random_stocks(size: usize) -> Vec { // Incremental ID with size. let start = { let mut id_lock = INCREMENT_ID.lock().unwrap(); let start = *id_lock; *id_lock += size + 1; start }; (start..start + size) .map(|id| Stock { id, counter: Counter::random(), change: (-100.0..100.0).fake(), change_percent: (-0.1..0.1).fake(), volume: (0.0..1000.0).fake(), turnover: (0.0..1000.0).fake(), market_cap: (0.0..1000.0).fake(), ttm: (0.0..1000.0).fake(), five_mins_ranking: (0.0..1000.0).fake(), th60_days_ranking: (0.0..1000.0).fake(), year_change_percent: (-1.0..1.0).fake(), bid: (0.0..1000.0).fake(), bid_volume: (0.0..1000.0).fake(), ask: (0.0..1000.0).fake(), ask_volume: (0.0..1000.0).fake(), open: (0.0..1000.0).fake(), prev_close: (0.0..1000.0).fake(), high: (0.0..1000.0).fake(), low: (0.0..1000.0).fake(), turnover_rate: (0.0..1.0).fake(), rise_rate: (0.0..1.0).fake(), amplitude: (0.0..1000.0).fake(), pe_status: (0.0..1000.0).fake(), pb_status: (0.0..1000.0).fake(), volume_ratio: (0.0..1.0).fake(), bid_ask_ratio: (0.0..1.0).fake(), latest_pre_close: (0.0..1000.0).fake(), latest_post_close: (0.0..1000.0).fake(), pre_market_cap: (0.0..1000.0).fake(), pre_market_percent: (-1.0..1.0).fake(), pre_market_change: (-100.0..100.0).fake(), post_market_cap: (0.0..1000.0).fake(), post_market_percent: (-1.0..1.0).fake(), post_market_change: (-100.0..100.0).fake(), float_cap: (0.0..1000.0).fake(), shares: (100000..9999999).fake(), shares_float: (100000..9999999).fake(), day_5_ranking: (0.0..1000.0).fake(), day_10_ranking: (0.0..1000.0).fake(), day_30_ranking: (0.0..1000.0).fake(), day_120_ranking: (0.0..1000.0).fake(), day_250_ranking: (0.0..1000.0).fake(), ..Default::default() }) .collect() } struct StockTableDelegate { stocks: Vec, columns: Vec, size: Size, loading: bool, lazy_load: bool, full_loading: bool, clicked_row: Option, eof: bool, visible_rows: Range, visible_cols: Range, _load_task: Task<()>, } impl StockTableDelegate { fn new(size: usize) -> Self { Self { size: Size::default(), stocks: random_stocks(size), lazy_load: false, clicked_row: None, columns: vec![ Column::new("id", "ID") .width(60.) .fixed(ColumnFixed::Left) .resizable(true) .min_width(40.) .max_width(100.) .text_center(), Column::new("market", "Market") .width(60.) .fixed(ColumnFixed::Left) .resizable(true) .min_width(50.), Column::new("name", "Name").width(180.).fixed(ColumnFixed::Left).max_width(300.), Column::new("symbol", "Symbol").width(100.).fixed(ColumnFixed::Left).sortable(), Column::new("price", "Price").sortable().text_right().p_0(), Column::new("change", "Chg").sortable().text_right().p_0(), Column::new("change_percent", "Chg%").sortable().text_right().p_0(), Column::new("volume", "Volume").p_0(), Column::new("turnover", "Turnover").p_0(), Column::new("market_cap", "Market Cap").p_0(), Column::new("ttm", "TTM").p_0(), Column::new("five_mins_ranking", "5m Ranking").text_right().p_0(), Column::new("th60_days_ranking", "60d Ranking"), Column::new("year_change_percent", "Year Chg%"), Column::new("bid", "Bid").text_right().p_0(), Column::new("bid_volume", "Bid Vol").text_right().p_0(), Column::new("ask", "Ask").text_right().p_0(), Column::new("ask_volume", "Ask Vol").text_right().p_0(), Column::new("open", "Open").text_right().p_0(), Column::new("prev_close", "Prev Close").text_right().p_0(), Column::new("high", "High").text_right().p_0(), Column::new("low", "Low").text_right().p_0(), Column::new("turnover_rate", "Turnover Rate"), Column::new("rise_rate", "Rise Rate"), Column::new("amplitude", "Amplitude"), Column::new("pe_status", "P/E"), Column::new("pb_status", "P/B"), Column::new("volume_ratio", "Volume Ratio").text_right().p_0(), Column::new("bid_ask_ratio", "Bid Ask Ratio").text_right().p_0(), Column::new("latest_pre_close", "Latest Pre Close"), Column::new("latest_post_close", "Latest Post Close"), Column::new("pre_market_cap", "Pre Mkt Cap"), Column::new("pre_market_percent", "Pre Mkt%"), Column::new("pre_market_change", "Pre Mkt Chg"), Column::new("post_market_cap", "Post Mkt Cap"), Column::new("post_market_percent", "Post Mkt%"), Column::new("post_market_change", "Post Mkt Chg"), Column::new("float_cap", "Float Cap"), Column::new("shares", "Shares"), Column::new("shares_float", "Float Shares"), Column::new("day_5_ranking", "5d Ranking"), Column::new("day_10_ranking", "10d Ranking"), Column::new("day_30_ranking", "30d Ranking"), Column::new("day_120_ranking", "120d Ranking"), Column::new("day_250_ranking", "250d Ranking"), ], loading: false, full_loading: false, eof: false, visible_cols: Range::default(), visible_rows: Range::default(), _load_task: Task::ready(()), } } fn update_stocks(&mut self, size: usize) { // Reset incremental ID { let mut id_lock = INCREMENT_ID.lock().unwrap(); *id_lock = 0; } self.stocks = random_stocks(size); self.eof = size <= 50; self.loading = false; self.full_loading = false; } fn render_percent(&self, col: &Column, val: f64, cx: &mut App) -> AnyElement { let right_num = ((val - val.floor()) * 1000.).floor() as i32; div() .h_full() .table_cell_size(self.size) .when(col.align == TextAlign::Right, |this| this.h_flex().justify_end()) .map(|this| { if right_num % 3 == 0 { this.text_color(cx.theme().red).bg(cx.theme().red_light.alpha(0.05)) } else if right_num % 3 == 1 { this.text_color(cx.theme().green).bg(cx.theme().green_light.alpha(0.05)) } else { this } }) .child(format!("{:.2}%", val * 100.)) .into_any_element() } fn render_value_cell(&self, col: &Column, val: f64, cx: &mut App) -> AnyElement { let this = div().h_full().table_cell_size(self.size).child(format!("{:.3}", val)); // Val is a 0.0 .. n.0 // 30% to red, 30% to green, others to default let right_num = ((val - val.floor()) * 1000.).floor() as i32; let this = if right_num % 3 == 0 { this.text_color(cx.theme().red).bg(cx.theme().red_light.alpha(0.05)) } else if right_num % 3 == 1 { this.text_color(cx.theme().green).bg(cx.theme().green_light.alpha(0.05)) } else { this }; this.when(col.align == TextAlign::Right, |this| this.h_flex().justify_end()) .into_any_element() } } impl TableDelegate for StockTableDelegate { fn columns_count(&self, _: &App) -> usize { self.columns.len() } fn rows_count(&self, _: &App) -> usize { self.stocks.len() } fn column(&self, col_ix: usize, _cx: &App) -> Column { self.columns[col_ix].clone() } fn render_th( &mut self, col_ix: usize, _: &mut Window, _: &mut Context>, ) -> impl IntoElement { let col = self.columns.get(col_ix).unwrap(); div() .child(col.name.clone()) .when(col_ix >= 3 && col_ix <= 10, |this| this.table_cell_size(self.size)) .when(col.align == TextAlign::Center, |this| this.h_flex().w_full().justify_center()) .when(col.align == TextAlign::Right, |this| this.h_flex().w_full().justify_end()) } fn context_menu( &mut self, row_ix: usize, menu: PopupMenu, _window: &mut Window, _: &mut Context>, ) -> PopupMenu { menu.menu(format!("Selected Row: {}", row_ix), Box::new(OpenDetail(row_ix))) .separator() .menu("Size Large", Box::new(ChangeSize(Size::Large))) .menu("Size Medium", Box::new(ChangeSize(Size::Medium))) .menu("Size Small", Box::new(ChangeSize(Size::Small))) .menu("Size XSmall", Box::new(ChangeSize(Size::XSmall))) } fn render_tr( &mut self, row_ix: usize, _: &mut Window, cx: &mut Context>, ) -> Stateful
{ div().id(row_ix).on_click(cx.listener(move |table, ev: &ClickEvent, _window, cx| { println!("You have clicked row with secondary: {}", ev.modifiers().secondary()); table.delegate_mut().clicked_row = Some(row_ix); cx.notify(); })) } /// NOTE: Performance metrics /// /// last render 561 cells total: 232.745µs, avg: 414ns /// frame duration: 8.825083ms /// /// This is means render the full table cells takes 232.745µs. Then 232.745µs / 8.82ms = 2.6% of the frame duration. /// /// If we improve the td rendering, we can reduce the time to render the full table cells. fn render_td( &mut self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>, ) -> impl IntoElement { let stock = self.stocks.get(row_ix).unwrap(); let col = self.columns.get(col_ix).unwrap(); match col.key.as_ref() { "id" => div() .child(stock.id.to_string()) .when(col.align == TextAlign::Center, |this| this.text_center()) .into_any_element(), "market" => div() .map(|this| { if stock.counter.market == "US" { this.text_color(cx.theme().blue) } else { this.text_color(cx.theme().magenta) } }) .child(stock.counter.market.clone()) .into_any_element(), "symbol" => stock.counter.symbol_code().into_any_element(), "name" => stock.counter.name.clone().into_any_element(), "price" => self.render_value_cell(&col, stock.price, cx), "change" => self.render_value_cell(&col, stock.change, cx), "change_percent" => self.render_percent(&col, stock.change_percent, cx), "volume" => self.render_value_cell(&col, stock.volume, cx), "turnover" => self.render_value_cell(&col, stock.turnover, cx), "market_cap" => self.render_value_cell(&col, stock.market_cap, cx), "ttm" => self.render_value_cell(&col, stock.ttm, cx), "five_mins_ranking" => self.render_value_cell(&col, stock.five_mins_ranking, cx), "th60_days_ranking" => stock.th60_days_ranking.floor().to_string().into_any_element(), "year_change_percent" => self.render_percent(&col, stock.year_change_percent, cx), "bid" => self.render_value_cell(&col, stock.bid, cx), "bid_volume" => self.render_value_cell(&col, stock.bid_volume, cx), "ask" => self.render_value_cell(&col, stock.ask, cx), "ask_volume" => self.render_value_cell(&col, stock.ask_volume, cx), "open" => self.render_value_cell(&col, stock.open, cx), "prev_close" => self.render_value_cell(&col, stock.prev_close, cx), "high" => self.render_value_cell(&col, stock.high, cx), "low" => self.render_value_cell(&col, stock.low, cx), "turnover_rate" => (stock.turnover_rate * 100.0).floor().to_string().into_any_element(), "rise_rate" => (stock.rise_rate * 100.0).floor().to_string().into_any_element(), "amplitude" => (stock.amplitude * 100.0).floor().to_string().into_any_element(), "pe_status" => stock.pe_status.floor().to_string().into_any_element(), "pb_status" => stock.pb_status.floor().to_string().into_any_element(), "volume_ratio" => self.render_value_cell(&col, stock.volume_ratio, cx), "bid_ask_ratio" => self.render_value_cell(&col, stock.bid_ask_ratio, cx), "latest_pre_close" => stock.latest_pre_close.floor().to_string().into_any_element(), "latest_post_close" => stock.latest_post_close.floor().to_string().into_any_element(), "pre_market_cap" => stock.pre_market_cap.floor().to_string().into_any_element(), "pre_market_percent" => self.render_percent(&col, stock.pre_market_percent, cx), "pre_market_change" => stock.pre_market_change.floor().to_string().into_any_element(), "post_market_cap" => stock.post_market_cap.floor().to_string().into_any_element(), "post_market_percent" => self.render_percent(&col, stock.post_market_percent, cx), "post_market_change" => stock.post_market_change.floor().to_string().into_any_element(), "float_cap" => stock.float_cap.floor().to_string().into_any_element(), "shares" => stock.shares.to_string().into_any_element(), "shares_float" => stock.shares_float.to_string().into_any_element(), "day_5_ranking" => stock.day_5_ranking.floor().to_string().into_any_element(), "day_10_ranking" => stock.day_10_ranking.floor().to_string().into_any_element(), "day_30_ranking" => stock.day_30_ranking.floor().to_string().into_any_element(), "day_120_ranking" => stock.day_120_ranking.floor().to_string().into_any_element(), "day_250_ranking" => stock.day_250_ranking.floor().to_string().into_any_element(), _ => "--".to_string().into_any_element(), } } fn move_column( &mut self, col_ix: usize, to_ix: usize, _: &mut Window, _: &mut Context>, ) { let col = self.columns.remove(col_ix); self.columns.insert(to_ix, col); } fn perform_sort( &mut self, col_ix: usize, sort: ColumnSort, _: &mut Window, _: &mut Context>, ) { if let Some(col) = self.columns.get_mut(col_ix) { match col.key.as_ref() { "id" => self.stocks.sort_by(|a, b| match sort { ColumnSort::Descending => b.id.cmp(&a.id), _ => a.id.cmp(&b.id), }), "symbol" => self.stocks.sort_by(|a, b| match sort { ColumnSort::Descending => b.counter.symbol.cmp(&a.counter.symbol), _ => a.id.cmp(&b.id), }), "change" | "change_percent" => self.stocks.sort_by(|a, b| match sort { ColumnSort::Descending => { b.change.partial_cmp(&a.change).unwrap_or(std::cmp::Ordering::Equal) } _ => a.id.cmp(&b.id), }), _ => {} } } } fn loading(&self, _: &App) -> bool { self.full_loading } fn has_more(&self, _: &App) -> bool { if !self.lazy_load { return false; } if self.loading { return false; } return !self.eof; } fn load_more_threshold(&self) -> usize { 150 } fn load_more(&mut self, _: &mut Window, cx: &mut Context>) { if !self.lazy_load { return; } self.loading = true; self._load_task = cx.spawn(async move |view, cx| { // Simulate network request, delay 1s to load data. cx.background_executor().timer(Duration::from_secs(1)).await; _ = cx.update(|cx| { let _ = view.update(cx, |view, _| { view.delegate_mut().stocks.extend(random_stocks(200)); view.delegate_mut().loading = false; view.delegate_mut().eof = view.delegate().stocks.len() >= 6000; }); }); }); } fn visible_rows_changed( &mut self, visible_range: Range, _: &mut Window, _: &mut Context>, ) { self.visible_rows = visible_range; } fn visible_columns_changed( &mut self, visible_range: Range, _: &mut Window, _: &mut Context>, ) { self.visible_cols = visible_range; } fn cell_text(&self, row_ix: usize, col_ix: usize, _cx: &App) -> String { let Some(stock) = self.stocks.get(row_ix) else { return String::new(); }; let Some(col) = self.columns.get(col_ix) else { return String::new(); }; match col.key.as_ref() { "id" => stock.id.to_string(), "market" => stock.counter.market.to_string(), "symbol" => stock.counter.symbol_code().to_string(), "name" => stock.counter.name.to_string(), "price" => format!("{:.3}", stock.price), "change" => format!("{:.3}", stock.change), "change_percent" => format!("{:.2}%", stock.change_percent * 100.), "volume" => format!("{:.3}", stock.volume), "turnover" => format!("{:.3}", stock.turnover), "market_cap" => format!("{:.3}", stock.market_cap), "ttm" => format!("{:.3}", stock.ttm), "five_mins_ranking" => format!("{:.3}", stock.five_mins_ranking), "th60_days_ranking" => stock.th60_days_ranking.floor().to_string(), "year_change_percent" => format!("{:.2}%", stock.year_change_percent * 100.), "bid" => format!("{:.3}", stock.bid), "bid_volume" => format!("{:.3}", stock.bid_volume), "ask" => format!("{:.3}", stock.ask), "ask_volume" => format!("{:.3}", stock.ask_volume), "open" => format!("{:.3}", stock.open), "prev_close" => format!("{:.3}", stock.prev_close), "high" => format!("{:.3}", stock.high), "low" => format!("{:.3}", stock.low), "turnover_rate" => format!("{:.0}", stock.turnover_rate * 100.), "rise_rate" => format!("{:.0}", stock.rise_rate * 100.), "amplitude" => format!("{:.0}", stock.amplitude * 100.), "pe_status" => stock.pe_status.floor().to_string(), "pb_status" => stock.pb_status.floor().to_string(), "volume_ratio" => format!("{:.3}", stock.volume_ratio), "bid_ask_ratio" => format!("{:.3}", stock.bid_ask_ratio), "latest_pre_close" => stock.latest_pre_close.floor().to_string(), "latest_post_close" => stock.latest_post_close.floor().to_string(), "pre_market_cap" => stock.pre_market_cap.floor().to_string(), "pre_market_percent" => format!("{:.2}%", stock.pre_market_percent * 100.), "pre_market_change" => stock.pre_market_change.floor().to_string(), "post_market_cap" => stock.post_market_cap.floor().to_string(), "post_market_percent" => format!("{:.2}%", stock.post_market_percent * 100.), "post_market_change" => stock.post_market_change.floor().to_string(), "float_cap" => stock.float_cap.floor().to_string(), "shares" => stock.shares.to_string(), "shares_float" => stock.shares_float.to_string(), "day_5_ranking" => stock.day_5_ranking.floor().to_string(), "day_10_ranking" => stock.day_10_ranking.floor().to_string(), "day_30_ranking" => stock.day_30_ranking.floor().to_string(), "day_120_ranking" => stock.day_120_ranking.floor().to_string(), "day_250_ranking" => stock.day_250_ranking.floor().to_string(), _ => String::new(), } } } pub struct DataTableStory { table: Entity>, num_stocks_input: Entity, stripe: bool, refresh_data: bool, size: Size, _subscriptions: Vec, _load_task: Task<()>, } impl super::Story for DataTableStory { fn title() -> &'static str { "DataTable" } fn description() -> &'static str { "A complex data table with selection, sorting, column moving, and loading more." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn closable() -> bool { false } } impl Focusable for DataTableStory { fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { self.table.focus_handle(cx) } } impl DataTableStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { // Create the number input field with validation for positive integers let num_stocks_input = cx.new(|cx| { let mut input = InputState::new(window, cx) .placeholder("Enter number of Stocks to display") .validate(|s, _| s.parse::().is_ok()); input.set_value("5000", window, cx); input }); let delegate = StockTableDelegate::new(5000); let table = cx.new(|cx| TableState::new(delegate, window, cx)); let _subscriptions = vec![ cx.subscribe_in(&table, window, Self::on_table_event), cx.subscribe_in(&num_stocks_input, window, Self::on_num_stocks_input_change), // Spawn a background to random refresh the list ]; let _load_task = cx.spawn(async move |this, cx| { loop { cx.background_executor().timer(time::Duration::from_millis(33)).await; this.update(cx, |this, cx| { if !this.refresh_data { return; } this.table.update(cx, |table, _| { table.delegate_mut().stocks.iter_mut().enumerate().for_each( |(i, stock)| { let n = (3..10).fake::(); // update 30% of the stocks if i % n == 0 { stock.random_update(); } }, ); }); cx.notify(); }) .ok(); } }); Self { table, num_stocks_input, stripe: false, refresh_data: false, size: Size::default(), _subscriptions, _load_task, } } // Event handler for changes in the number input field fn on_num_stocks_input_change( &mut self, _: &Entity, event: &InputEvent, _: &mut Window, cx: &mut Context, ) { match event { // Update when the user presses Enter or the input loses focus InputEvent::PressEnter { .. } | InputEvent::Blur => { let text = self.num_stocks_input.read(cx).value().to_string(); if let Ok(total_count) = text.parse::() { if total_count == self.table.read(cx).delegate().stocks.len() { return; } self.table.update(cx, |table, _| { table.delegate_mut().update_stocks(total_count); }); cx.notify(); } } _ => {} } } fn toggle_loop_selection(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { table.loop_selection = *checked; cx.notify(); }); } fn toggle_col_resize(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { table.col_resizable = *checked; cx.notify(); }); } fn toggle_col_order(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { table.col_movable = *checked; cx.notify(); }); } fn toggle_col_sort(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { table.sortable = *checked; cx.notify(); }); } fn toggle_col_fixed(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { table.col_fixed = *checked; cx.notify(); }); } fn toggle_col_selection(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { table.col_selectable = *checked; cx.notify(); }); } fn toggle_row_selection(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { table.row_selectable = *checked; cx.notify(); }); } fn toggle_cell_selection(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { table.cell_selectable = *checked; cx.notify(); }); } fn toggle_stripe(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.stripe = *checked; cx.notify(); } fn on_change_size(&mut self, a: &ChangeSize, _: &mut Window, cx: &mut Context) { self.size = a.0; cx.notify(); } fn toggle_refresh_data(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.refresh_data = *checked; cx.notify(); } fn on_table_event( &mut self, _: &Entity>, event: &TableEvent, _window: &mut Window, _cx: &mut Context, ) { match event { TableEvent::ColumnWidthsChanged(col_widths) => { println!("Column widths changed: {:?}", col_widths) } TableEvent::SelectColumn(ix) => println!("Select col: {}", ix), TableEvent::SelectCell(row_ix, col_ix) => { println!("Select cell: row={}, col={}", row_ix, col_ix) } TableEvent::DoubleClickedCell(row_ix, col_ix) => { println!("Double clicked cell: row={}, col={}", row_ix, col_ix) } TableEvent::DoubleClickedRow(ix) => println!("Double clicked row: {}", ix), TableEvent::SelectRow(ix) => println!("Select row: {}", ix), TableEvent::MoveColumn(origin_idx, target_idx) => { println!("Move col index: {} -> {}", origin_idx, target_idx); } TableEvent::RightClickedRow(ix) => println!("Right clicked row: {:?}", ix), TableEvent::RightClickedCell(row_ix, col_ix) => { println!("Right clicked cell: row={}, col={}", row_ix, col_ix) } TableEvent::ClearSelection => { println!("Selection cleared"); } } } fn dump_csv(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { match self.write_csv(cx) { Ok(csv_content) => { let Some(path) = dirs::download_dir() else { eprintln!("Failed to get download directory"); return; }; let receiver = cx.prompt_for_new_path(&path, Some("export.csv")); cx.spawn_in(window, async move |_, _| { if let Some(path) = receiver.await.ok().into_iter().flatten().flatten().next() { match std::fs::write(&path, csv_content) { Ok(_) => { println!("CSV exported successfully to: {:?}", path); } Err(e) => { eprintln!("Failed to save CSV file: {}", e); } } } else { println!("CSV export cancelled by user"); }; }) .detach(); } Err(e) => { eprintln!("Failed to export CSV: {}", e); } } } fn write_csv(&mut self, cx: &mut Context) -> anyhow::Result { let (headers, rows) = self.table.update(cx, |table, cx| table.dump(cx)); // Convert to CSV format using rust-csv let mut wtr = csv::Writer::from_writer(vec![]); // Write header wtr.write_record(&headers)?; // Write data rows for row in rows { wtr.write_record(&row)?; } // Flush and get the CSV data wtr.flush()?; let data = wtr.into_inner().map_err(csv::IntoInnerError::into_error)?; let csv_content = String::from_utf8(data)?; Ok(csv_content) } } impl Render for DataTableStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { let table = &self.table.read(cx); let delegate = table.delegate(); let rows_count = delegate.rows_count(cx); let size = self.size; v_flex() .on_action(cx.listener(Self::on_change_size)) .size_full() .text_sm() .gap_4() .child( h_flex() .items_center() .gap_3() .flex_wrap() .child( Checkbox::new("loop-selection") .label("Loop Selection") .selected(table.loop_selection) .on_click(cx.listener(Self::toggle_loop_selection)), ) .child( Checkbox::new("col-resize") .label("Column Resize") .selected(table.col_resizable) .on_click(cx.listener(Self::toggle_col_resize)), ) .child( Checkbox::new("col-order") .label("Column Order") .selected(table.col_movable) .on_click(cx.listener(Self::toggle_col_order)), ) .child( Checkbox::new("col-sort") .label("Sortable") .selected(table.sortable) .on_click(cx.listener(Self::toggle_col_sort)), ) .child( Checkbox::new("col-selection") .label("Column Selectable") .selected(table.col_selectable) .on_click(cx.listener(Self::toggle_col_selection)), ) .child( Checkbox::new("row-selection") .label("Row Selectable") .selected(table.row_selectable) .on_click(cx.listener(Self::toggle_row_selection)), ) .child( Checkbox::new("cell-selection") .label("Cell Selectable") .selected(table.cell_selectable) .on_click(cx.listener(Self::toggle_cell_selection)), ) .child( Checkbox::new("fixed") .label("Column Fixed") .selected(table.col_fixed) .on_click(cx.listener(Self::toggle_col_fixed)), ) .child( Checkbox::new("stripe") .label("Stripe") .selected(self.stripe) .on_click(cx.listener(Self::toggle_stripe)), ) .child( Checkbox::new("loading") .label("Loading") .checked(self.table.read(cx).delegate().full_loading) .on_click(cx.listener(|this, check: &bool, _, cx| { this.table.update(cx, |this, cx| { this.delegate_mut().full_loading = *check; cx.notify(); }) })), ) .child( Checkbox::new("refresh-data") .label("Refresh Data") .selected(self.refresh_data) .on_click(cx.listener(Self::toggle_refresh_data)), ), ) .child( h_flex() .gap_2() .child( Button::new("size") .outline() .small() .label(format!("size: {:?}", self.size)) .dropdown_menu(move |menu, _, _| { menu.menu_with_check( "Large", size == Size::Large, Box::new(ChangeSize(Size::Large)), ) .menu_with_check( "Medium", size == Size::Medium, Box::new(ChangeSize(Size::Medium)), ) .menu_with_check( "Small", size == Size::Small, Box::new(ChangeSize(Size::Small)), ) .menu_with_check( "XSmall", size == Size::XSmall, Box::new(ChangeSize(Size::XSmall)), ) }), ) .child( Button::new("scroll-top") .outline() .small() .child("Scroll to Top") .on_click(cx.listener(|this, _, _, cx| { this.table.update(cx, |table, cx| { table.scroll_to_row(0, cx); }) })), ) .child( Button::new("scroll-bottom") .outline() .small() .child("Scroll to Bottom") .on_click(cx.listener(|this, _, _, cx| { this.table.update(cx, |table, cx| { table.scroll_to_row(table.delegate().rows_count(cx) - 1, cx); }) })), ) .child( Button::new("dump-csv") .outline() .small() .label("Dump CSV") .on_click(cx.listener(Self::dump_csv)), ) .child( Button::new("select-cell-5-3") .outline() .small() .child("Select Cell (5, 3)") .on_click(cx.listener(|this, _, _, cx| { this.table.update(cx, |table, cx| { table.set_selected_cell(5, 3, cx); }) })), ) .child( Button::new("select-cell-10-7") .outline() .small() .child("Select Cell (10, 7)") .on_click(cx.listener(|this, _, _, cx| { this.table.update(cx, |table, cx| { table.set_selected_cell(10, 7, cx); }) })), ) .child( Button::new("clear-selection") .outline() .small() .child("Clear Selection") .on_click(cx.listener(|this, _, _, cx| { this.table.update(cx, |table, cx| { table.clear_selection(cx); }) })), ), ) .child( h_flex().items_center().gap_2().child( h_flex() .w_full() .items_center() .justify_between() .gap_2() .child( h_flex() .gap_2() .flex_1() .child(Label::new("Number of Stocks:")) .child( h_flex() .min_w_32() .child(Input::new(&self.num_stocks_input).small()) .into_any_element(), ) .when(delegate.loading, |this| { this.child( h_flex().gap_1().child(Spinner::new()).child("Loading..."), ) }) .child( Checkbox::new("lazy-load") .label("Lazy Load") .checked(delegate.lazy_load) .on_click(cx.listener(|this, check: &bool, _, cx| { this.table.update(cx, |table, cx| { table.delegate_mut().lazy_load = *check; cx.notify(); }) })), ), ) .child( h_flex() .gap_2() .child(format!("Total Rows: {}", rows_count)) .child(format!("Visible Rows: {:?}", delegate.visible_rows)) .child(format!("Visible Cols: {:?}", delegate.visible_cols)) .when_some(table.selected_cell(), |this, (row, col)| { this.child(format!("Selected Cell: ({}, {})", row, col)) }) .when(delegate.eof, |this| this.child("All data loaded.")), ), ), ) .child(DataTable::new(&self.table).with_size(self.size).stripe(self.stripe)) } } ================================================ FILE: crates/story/src/stories/date_picker_story.rs ================================================ use chrono::{Datelike, Days, Duration, Utc}; use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement as _, Render, Styled as _, Subscription, Window, div, px, }; use gpui_component::{ ActiveTheme as _, Sizable as _, calendar, date_picker::{DatePicker, DatePickerEvent, DatePickerState, DateRangePreset}, v_flex, }; use crate::section; pub struct DatePickerStory { date_picker: Entity, date_picker_small: Entity, date_picker_large: Entity, data_picker_custom: Entity, date_picker_value: Option, date_range_picker: Entity, default_range_mode_picker: Entity, without_appearance_picker: Entity, _subscriptions: Vec, } impl super::Story for DatePickerStory { fn title() -> &'static str { "DatePicker" } fn description() -> &'static str { "A date picker to select a date or date range." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl DatePickerStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let now = chrono::Local::now().naive_local().date(); let date_picker = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx).disabled_matcher(vec![0, 6]); picker.set_date(now, window, cx); picker }); let date_picker_large = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx) .date_format("%Y-%m-%d") .disabled_matcher(calendar::Matcher::range( Some(now), now.checked_add_days(Days::new(7)), )); picker.set_date( now.checked_sub_days(Days::new(1)).unwrap_or_default(), window, cx, ); picker }); let date_picker_small = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx).disabled_matcher( calendar::Matcher::interval(Some(now), now.checked_add_days(Days::new(5))), ); picker.set_date(now, window, cx); picker }); let data_picker_custom = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| date.day0() < 5)); picker.set_date(now, window, cx); picker }); let date_range_picker = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx); picker.set_date( (now, now.checked_add_days(Days::new(4)).unwrap()), window, cx, ); picker }); let default_range_mode_picker = cx.new(|cx| DatePickerState::range(window, cx)); let without_appearance_picker = cx.new(|cx| DatePickerState::new(window, cx)); let _subscriptions = vec![ cx.subscribe(&date_picker, |this, _, ev, _| match ev { DatePickerEvent::Change(date) => { this.date_picker_value = date.format("%Y-%m-%d").map(|s| s.to_string()); } }), cx.subscribe(&date_range_picker, |this, _, ev, _| match ev { DatePickerEvent::Change(date) => { this.date_picker_value = date.format("%Y-%m-%d").map(|s| s.to_string()); } }), cx.subscribe(&default_range_mode_picker, |this, _, ev, _| match ev { DatePickerEvent::Change(date) => { this.date_picker_value = date.format("%Y-%m-%d").map(|s| s.to_string()); } }), ]; Self { date_picker, date_picker_large, date_picker_small, data_picker_custom, date_range_picker, default_range_mode_picker, without_appearance_picker, date_picker_value: None, _subscriptions, } } } impl Focusable for DatePickerStory { fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { self.date_picker.focus_handle(cx) } } impl Render for DatePickerStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let presets = vec![ DateRangePreset::single( "Yesterday", (Utc::now() - Duration::days(1)).naive_local().date(), ), DateRangePreset::single( "Last Week", (Utc::now() - Duration::weeks(1)).naive_local().date(), ), DateRangePreset::single( "Last Month", (Utc::now() - Duration::days(30)).naive_local().date(), ), ]; let range_presets = vec![ DateRangePreset::range( "Last 7 Days", (Utc::now() - Duration::days(7)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 14 Days", (Utc::now() - Duration::days(14)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 30 Days", (Utc::now() - Duration::days(30)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 90 Days", (Utc::now() - Duration::days(90)).naive_local().date(), Utc::now().naive_local().date(), ), ]; v_flex() .gap_3() .child( section("Normal").max_w_128().child( DatePicker::new(&self.date_picker) .cleanable(true) .presets(presets), ), ) .child( section("Small with 180px width") .max_w_128() .child(DatePicker::new(&self.date_picker_small).small().w(px(180.))), ) .child( section("Large") .max_w_128() .child(DatePicker::new(&self.date_picker_large).large().w(px(300.))), ) .child( section("Custom (First 5 days of each month disabled)") .max_w_128() .child(DatePicker::new(&self.data_picker_custom)), ) .child( section("Date Range").max_w_128().child( DatePicker::new(&self.date_range_picker) .number_of_months(2) .cleanable(true) .presets(range_presets.clone()), ), ) .child( section("Default Range Mode").max_w_128().child( DatePicker::new(&self.default_range_mode_picker) .placeholder("Range mode picker") .cleanable(true) .presets(range_presets.clone()), ), ) .child( section("Date Picker Value").max_w_128().child( format!("Date picker value: {:?}", self.date_picker_value).into_element(), ), ) .child( section("Without Appearance").max_w_128().child( div().w_full().bg(cx.theme().secondary).child( DatePicker::new(&self.without_appearance_picker) .appearance(false) .placeholder("Without appearance"), ), ), ) } } ================================================ FILE: crates/story/src/stories/description_list_story.rs ================================================ use gpui::*; use gpui::{ Action, App, AppContext, Axis, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Window, }; use gpui_component::{AxisExt, h_flex, menu::DropdownMenu as _}; use gpui_component::{ Sizable as _, Size, button::Button, checkbox::Checkbox, description_list::{DescriptionItem, DescriptionList}, dock::PanelControl, text::TextView, v_flex, }; use serde::Deserialize; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = description_list_story, no_json)] struct ChangeSize(Size); pub struct DescriptionListStory { focus_handle: FocusHandle, layout: Axis, bordered: bool, size: Size, items: Vec<(&'static str, &'static str, usize)>, } impl DescriptionListStory { fn new(_: &mut Window, cx: &mut Context) -> Self { let items = vec![ ("Name", "GPUI Component", 1), ( "Description", "UI components for building fantastic desktop application by using [GPUI](https://gpui.rs).\ \n\n \ Contains a lot of useful UI components, such as **Button**, **Input**, **Table**, **List**, **Select**, **DatePicker** ... \ \n\n \ You can easily create your native desktop application by using GPUI Component. ", 3, ), ("Version", "0.1.0", 1), ("License", "Apache-2.0", 1), ("Author", "Longbridge", 1), ("--", "--", 1), ( "Repository", "https://github.com/longbridge/gpui-component", 2, ), ( "Category", "UI, Desktop, Framework", 1, ), ( "This is a long label for Platform", "macOS, Windows, Linux", 1, ), ]; Self { items, bordered: true, size: Size::default(), layout: Axis::Horizontal, focus_handle: cx.focus_handle(), } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn set_layout(&mut self, layout: Axis, cx: &mut Context) { self.layout = layout; cx.notify(); } fn set_bordered(&mut self, bordered: bool, cx: &mut Context) { self.bordered = bordered; cx.notify(); } fn on_change_size(&mut self, a: &ChangeSize, _: &mut Window, cx: &mut Context) { self.size = a.0; cx.notify(); } } impl super::Story for DescriptionListStory { fn title() -> &'static str { "DescriptionList" } fn description() -> &'static str { "Use to display details with a tidy layout." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn zoomable() -> Option { None } } impl Focusable for DescriptionListStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for DescriptionListStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .id("example") .on_action(cx.listener(Self::on_change_size)) .p_4() .size_full() .gap_2() .child( h_flex() .gap_3() .child( Checkbox::new("layout") .checked(self.layout.is_vertical()) .label("Vertical Layout") .on_click(cx.listener(|this, checked: &bool, _, cx| { let new_layout = if *checked { Axis::Vertical } else { Axis::Horizontal }; this.set_layout(new_layout, cx); })), ) .child( Checkbox::new("bordered") .checked(self.bordered) .label("Bordered") .on_click(cx.listener(|this, checked: &bool, _, cx| { this.set_bordered(*checked, cx); })), ) .child( Button::new("size") .small() .outline() .label(format!("size: {:?}", self.size)) .dropdown_menu({ let size = self.size; move |menu, _, _| { menu.menu_with_check( "Large", size == Size::Large, Box::new(ChangeSize(Size::Large)), ) .menu_with_check( "Medium", size == Size::Medium, Box::new(ChangeSize(Size::Medium)), ) .menu_with_check( "Small", size == Size::Small, Box::new(ChangeSize(Size::Small)), ) } }), ), ) .child( DescriptionList::new() .columns(3) .layout(self.layout) .bordered(self.bordered) .with_size(self.size) .children(self.items.clone().into_iter().enumerate().map( |(ix, (label, value, span))| { if label == "--" { return DescriptionItem::Divider; } DescriptionItem::new(label) .value(TextView::markdown(ix, value).into_any_element()) .span(span) }, )), ) } } ================================================ FILE: crates/story/src/stories/dialog_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, InteractiveElement as _, IntoElement, ParentElement, Render, SharedString, Styled, Window, div, px, }; use gpui_component::{ ActiveTheme, Icon, IconName, WindowExt as _, button::{Button, ButtonVariants as _}, checkbox::Checkbox, date_picker::{DatePicker, DatePickerState}, dialog::{ Dialog, DialogAction, DialogClose, DialogDescription, DialogFooter, DialogHeader, DialogTitle, }, h_flex, input::{Input, InputState}, select::{Select, SelectState}, table::{Column, DataTable, TableDelegate, TableState}, text::markdown, v_flex, }; use crate::{TestAction, section}; pub struct DialogStory { focus_handle: FocusHandle, selected_value: Option, input1: Entity, input2: Entity, date: Entity, select: Entity>>, table: Entity>, dialog_overlay: bool, close_button: bool, keyboard: bool, overlay_closable: bool, } struct MyTable { columns: Vec, } impl MyTable { fn new(_: &mut App) -> Self { let columns = vec![ Column::new("id", "ID").width(px(50.)), Column::new("name", "Name").width(px(150.)), Column::new("email", "Email").width(px(250.)), Column::new("role", "Role").width(px(150.)), Column::new("status", "Status").width(px(100.)), ]; Self { columns } } } impl TableDelegate for MyTable { fn columns_count(&self, _: &App) -> usize { 5 } fn rows_count(&self, _: &App) -> usize { 200 } fn column(&self, col_ix: usize, _: &App) -> Column { self.columns[col_ix].clone() } fn render_td( &mut self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>, ) -> impl IntoElement { match col_ix { 0 => format!("{}", row_ix).into_any_element(), 1 => format!("User {}", row_ix).into_any_element(), 2 => format!("user-{}@mail.com", row_ix).into_any_element(), 3 => "User".into_any_element(), 4 => "Active".into_any_element(), _ => panic!("Invalid column index"), } } } impl super::Story for DialogStory { fn title() -> &'static str { "Dialog" } fn description() -> &'static str { "A dialog dialog" } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl DialogStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let input1 = cx.new(|cx| InputState::new(window, cx).placeholder("Your Name")); let input2 = cx.new(|cx| { InputState::new(window, cx).placeholder("For test focus back on dialog close.") }); let date = cx.new(|cx| DatePickerState::new(window, cx)); let select = cx.new(|cx| { SelectState::new( vec!["Option 1".to_string(), "Option 2".to_string(), "Option 3".to_string()], None, window, cx, ) }); let table = cx.new(|cx| TableState::new(MyTable::new(cx), window, cx)); Self { focus_handle: cx.focus_handle(), selected_value: None, input1, input2, date, select, dialog_overlay: true, close_button: true, keyboard: true, overlay_closable: true, table, } } fn on_action_test_action( &mut self, _: &TestAction, window: &mut Window, cx: &mut Context, ) { window.push_notification("You have clicked the TestAction.", cx); } fn render_basic_dialog(&self, cx: &mut Context) -> impl IntoElement { let dialog_overlay = self.dialog_overlay; let overlay_closable = self.overlay_closable; let input1 = self.input1.clone(); let date = self.date.clone(); let select = self.select.clone(); let view = cx.entity(); section("Basic Dialog").child( Dialog::new(cx) .trigger(Button::new("show-dialog").outline().label("Open Dialog")) .overlay(dialog_overlay) .keyboard(self.keyboard) .close_button(self.close_button) .overlay_closable(overlay_closable) .on_ok({ let view = view.clone(); let input1 = input1.clone(); let date = date.clone(); move |_, window, cx| { view.update(cx, |view, cx| { view.selected_value = Some( format!( "Hello, {}, date: {}", input1.read(cx).value(), date.read(cx).date() ) .into(), ) }); window.push_notification("You have pressed confirm.", cx); true } }) .p_0() .content({ move |content, _, cx| { content .child( DialogHeader::new() .p_4() .child(DialogTitle::new().child("Basic Dialog")) .child(DialogDescription::new().child( "This is a basic dialog created \ using the declarative API.", )), ) .child( v_flex() .px_4() .pb_4() .gap_3() .child( "This is a dialog dialog, \ you can put anything here.", ) .child(Input::new(&input1)) .child(Select::new(&select)) .child(DatePicker::new(&date).placeholder("Date of Birth")), ) .child( DialogFooter::new() .p_4() .bg(cx.theme().muted) .justify_between() .child( Button::new("new-dialog") .label("Open Other Dialog") .outline() .on_click(move |_, window, cx| { window.open_dialog(cx, move |dialog, _, _| { dialog .title("Other Dialog") .child("This is another dialog.") .min_h(px(100.)) .overlay_closable(overlay_closable) }); }), ) .child( h_flex() .gap_2() .child(DialogClose::new().child( Button::new("cancel").label("Cancel").outline(), )) .child(DialogAction::new().child( Button::new("confirm").primary().label("Confirm"), )), ), ) } }), ) } fn render_focus_back_test(&self, _cx: &mut Context) -> impl IntoElement { section("Focus back test").max_w_md().child(Input::new(&self.input2)).child( Button::new("test-action") .outline() .label("Test Action") .flex_shrink_0() .on_click(|_, window, cx| { window.dispatch_action(Box::new(TestAction), cx); }) .tooltip( "This button for test dispatch action, \ to make sure when Dialog close,\ \nthis still can handle the action.", ), ) } fn render_dialog_without_title(&self, cx: &mut Context) -> impl IntoElement { let dialog_overlay = self.dialog_overlay; let overlay_closable = self.overlay_closable; section("Dialog without Title").child( Button::new("dialog-no-title").outline().label("Dialog without Title").on_click( cx.listener(move |_, _, window, cx| { window.open_dialog(cx, move |dialog, _, _| { dialog.overlay(dialog_overlay).overlay_closable(overlay_closable).child( "This is a dialog without title, \ you can use it when the title is not necessary.", ) }); }), ), ) } fn render_custom_buttons(&self, cx: &mut Context) -> impl IntoElement { let dialog_overlay = self.dialog_overlay; let overlay_closable = self.overlay_closable; section("Custom buttons").child( Button::new("confirm-dialog1").outline().label("Custom Buttons").on_click(cx.listener( move |_, _, window, cx| { window.open_dialog(cx, move |dialog, _, cx| { dialog .rounded(cx.theme().radius_lg) .overlay(dialog_overlay) .overlay_closable(overlay_closable) .child( v_flex() .gap_3() .items_center() .child( div() .flex() .items_center() .justify_center() .rounded(cx.theme().radius_lg) .bg(cx.theme().warning.opacity(0.2)) .size_12() .text_color(cx.theme().warning) .child(Icon::new(IconName::TriangleAlert).size_8()), ) .child( "Update successful, \ we need to restart the application.", ), ) .footer( DialogFooter::new() .child( DialogClose::new() .child(Button::new("cancel").label("Later").outline()), ) .child( DialogAction::new().child( Button::new("ok").label("Restart Now").primary(), ), ), ) .on_ok(|_, window, cx| { window.push_notification("You have pressed restart.", cx); true }) .on_cancel(|_, window, cx| { window.push_notification("You have pressed later.", cx); true }) }); }, )), ) } fn render_scrollable_dialog(&self, cx: &mut Context) -> impl IntoElement { let dialog_overlay = self.dialog_overlay; let overlay_closable = self.overlay_closable; section("Scrollable Dialog").child( Button::new("scrollable-dialog").outline().label("Scrollable Dialog").on_click( cx.listener(move |_, _, window, cx| { window.open_dialog(cx, move |dialog, _, _| { dialog .w(px(720.)) .h(px(600.)) .overlay(dialog_overlay) .overlay_closable(overlay_closable) .title("Dialog with scrollbar") .child(markdown(include_str!("../../../../README.md"))) .footer( DialogFooter::new() .gap_2() .child( DialogClose::new() .child(Button::new("cancel").label("Cancel").outline()), ) .child( DialogAction::new().child( Button::new("confirm").label("Confirm").primary(), ), ), ) }); }), ), ) } fn render_table_in_dialog(&self, cx: &mut Context) -> impl IntoElement { let dialog_overlay = self.dialog_overlay; let overlay_closable = self.overlay_closable; section("Table in Dialog").child( Button::new("table-dialog").outline().label("Table Dialog").on_click(cx.listener({ move |this, _, window, cx| { window.open_dialog(cx, { let table = this.table.clone(); move |dialog, _, _| { dialog .w(px(800.)) .h(px(600.)) .overlay(dialog_overlay) .overlay_closable(overlay_closable) .title("Dialog with Table") .child( v_flex() .size_full() .gap_3() .child("This is a dialog contains a table component.") .child(DataTable::new(&table)), ) } }); } })), ) } fn render_custom_paddings(&self, cx: &mut Context) -> impl IntoElement { section("Custom Paddings").child( Button::new("custom-dialog-paddings").outline().label("Custom Paddings").on_click( cx.listener(move |_, _, window, cx| { window.open_dialog(cx, move |dialog, _, _| { dialog.p_3().title("Custom Dialog Title").child( "This is a custom dialog content, we can use \ paddings to control the layout and spacing within \ the dialog.", ) }); }), ), ) } fn render_custom_style(&self, cx: &mut Context) -> impl IntoElement { section("Custom Style").child( Button::new("custom-dialog-style").outline().label("Custom Dialog Style").on_click( cx.listener(move |_, _, window, cx| { window.open_dialog(cx, move |dialog, _, cx| { dialog .rounded(cx.theme().radius_lg) .bg(cx.theme().cyan) .text_color(cx.theme().info_foreground) .title("Custom Dialog Title") .child("This is a custom dialog content.") }); }), ), ) } fn render_dialog_with_content(&self, cx: &mut Context) -> impl IntoElement { section("Open Dialog with DialogContent").sub_title("Declarative API").child( Button::new("custom-width-dialog-btn") .outline() .label("Custom Width (400px)") .on_click(cx.listener(move |_, _, window, cx| { window.open_dialog(cx, move |dialog, _, _| { dialog.w(px(400.)).content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Custom Width")) .child( DialogDescription::new() .child("This dialog has a custom width of 400px."), ), ) .child( "Content area with custom width configuration, \ and the footer is used flex 1 button widths.", ) .child( DialogFooter::new() .justify_center() .child( Button::new("cancel") .flex_1() .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }), ) .child( Button::new("done") .flex_1() .primary() .label("Done") .on_click(|_, window, cx| { window.close_dialog(cx); }), ), ) }) }) })), ) } } impl Focusable for DialogStory { fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { self.focus_handle.clone() } } impl Render for DialogStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .id("dialog-story") .track_focus(&self.focus_handle) .on_action(cx.listener(Self::on_action_test_action)) .size_full() .child( v_flex() .gap_6() .child( h_flex() .items_center() .gap_3() .child( Checkbox::new("dialog-overlay") .label("Dialog Overlay") .checked(self.dialog_overlay) .on_click(cx.listener(|view, _, _, cx| { view.dialog_overlay = !view.dialog_overlay; cx.notify(); })), ) .child( Checkbox::new("overlay-closable") .label("Overlay Closable") .checked(self.overlay_closable) .on_click(cx.listener(|view, _, _, cx| { view.overlay_closable = !view.overlay_closable; cx.notify(); })), ) .child( Checkbox::new("dialog-show-close") .label("Model Close Button") .checked(self.close_button) .on_click(cx.listener(|view, _, _, cx| { view.close_button = !view.close_button; cx.notify(); })), ) .child( Checkbox::new("dialog-keyboard") .label("Keyboard") .checked(self.keyboard) .on_click(cx.listener(|view, _, _, cx| { view.keyboard = !view.keyboard; cx.notify(); })), ), ) .child(self.render_basic_dialog(cx)) .child(self.render_focus_back_test(cx)) .child(self.render_custom_buttons(cx)) .child(self.render_scrollable_dialog(cx)) .child(self.render_table_in_dialog(cx)) .child(self.render_dialog_without_title(cx)) .child(self.render_custom_paddings(cx)) .child(self.render_custom_style(cx)) .child(self.render_dialog_with_content(cx)), ) } } ================================================ FILE: crates/story/src/stories/divider_story.rs ================================================ use crate::section; use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled, Window, px, }; use gpui_component::{ActiveTheme, divider::Divider, h_flex, label::Label, v_flex}; const DESCRIPTION: &str = "GPUI Component is a Rust GUI components for building fantastic cross-platform desktop application by using GPUI."; pub struct DividerStory { focus_handle: gpui::FocusHandle, } impl super::Story for DividerStory { fn title() -> &'static str { "Divider" } fn description() -> &'static str { "A divider that can be either vertical or horizontal." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl DividerStory { pub fn view(_window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self { focus_handle: cx.focus_handle(), }) } } impl Focusable for DividerStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for DividerStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_6() .child( section("Horizontal Dividers").child( v_flex() .gap_4() .w_full() .mt_4() .child(Divider::horizontal()) .child(Divider::horizontal().label("With Label")) .child(Divider::horizontal_dashed()) .child(Divider::horizontal_dashed().label("Dashed With Label")), ), ) .child( section("Vertical Dividers").child( h_flex() .gap_4() .h(px(100.)) .child(Divider::vertical()) .child(Divider::vertical().label("Solid")) .child(Divider::vertical_dashed()) .child(Divider::vertical_dashed().label("Dashed")), ), ) .child( section("Combination Dividers").child( v_flex() .gap_y_4() .child( v_flex().gap_y_2().child("Hello GPUI Component").child( Label::new(DESCRIPTION) .text_color(cx.theme().muted_foreground) .text_sm(), ), ) .child(Divider::horizontal()) .child( h_flex() .gap_x_4() .child("Docs") .child(Divider::vertical().dashed()) .child("Github") .child(Divider::vertical().dashed()) .child("Source"), ), ), ) } } ================================================ FILE: crates/story/src/stories/dropdown_button_story.rs ================================================ use gpui::{ Action, App, AppContext as _, Context, Corner, Entity, Focusable, IntoElement, ParentElement as _, Render, Styled as _, Window, prelude::FluentBuilder as _, }; use serde::Deserialize; use crate::section; use gpui_component::{ ActiveTheme, Disableable, Selectable as _, Sizable as _, Theme, button::{Button, ButtonVariants as _, DropdownButton}, checkbox::Checkbox, h_flex, v_flex, }; #[derive(Clone, Action, PartialEq, Eq, Deserialize)] #[action(namespace = dropdown_button_story, no_json)] enum ButtonAction { Disabled, Loading, Selected, Compact, } pub struct DropdownButtonStory { focus_handle: gpui::FocusHandle, disabled: bool, loading: bool, selected: bool, compact: bool, } impl DropdownButtonStory { pub fn view(_: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self { focus_handle: cx.focus_handle(), disabled: false, loading: false, selected: false, compact: false, }) } } impl super::Story for DropdownButtonStory { fn title() -> &'static str { "DropdownButton" } fn description() -> &'static str { "A button with an attached dropdown menu for additional options." } fn closable() -> bool { false } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl Focusable for DropdownButtonStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for DropdownButtonStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let disabled = self.disabled; let loading = self.loading; let selected = self.selected; let compact = self.compact; v_flex() .gap_6() .child( h_flex() .gap_3() .child( Checkbox::new("disabled-button") .label("Disabled") .checked(self.disabled) .on_click(cx.listener(|view, _, _, cx| { view.disabled = !view.disabled; cx.notify(); })), ) .child( Checkbox::new("loading-button") .label("Loading") .checked(self.loading) .on_click(cx.listener(|view, _, _, cx| { view.loading = !view.loading; cx.notify(); })), ) .child( Checkbox::new("selected-button") .label("Selected") .checked(self.selected) .on_click(cx.listener(|view, _, _, cx| { view.selected = !view.selected; cx.notify(); })), ) .child( Checkbox::new("compact-button") .label("Compact") .checked(self.compact) .on_click(cx.listener(|view, _, _, cx| { view.compact = !view.compact; cx.notify(); })), ) .child( Checkbox::new("shadow-button") .label("Shadow") .checked(cx.theme().shadow) .on_click(cx.listener(|_, _, window, cx| { let mut theme = cx.theme().clone(); theme.shadow = !theme.shadow; cx.set_global::(theme); window.refresh(); })), ), ) .child( section("Dropdown Button").child( DropdownButton::new("btn0") .primary() .button(Button::new("btn").label("Primary Dropdown")) .when(self.compact, |this| this.compact()) .loading(self.loading) .disabled(self.disabled) .selected(selected) .dropdown_menu_with_anchor(Corner::BottomRight, move |this, _, _| { this.menu_with_check( "Disabled", disabled, Box::new(ButtonAction::Disabled), ) .menu_with_check("Loading", loading, Box::new(ButtonAction::Loading)) .menu_with_check("Selected", selected, Box::new(ButtonAction::Selected)) .menu_with_check( "Compact", compact, Box::new(ButtonAction::Compact), ) }), ), ) .child( section("Small Size").child( DropdownButton::new("btn-sm") .small() .button(Button::new("btn").label("Small Dropdown")) .when(self.compact, |this| this.compact()) .loading(self.loading) .disabled(self.disabled) .selected(selected) .dropdown_menu(move |this, _, _| { this.menu_with_check( "Disabled", disabled, Box::new(ButtonAction::Disabled), ) .menu_with_check("Loading", loading, Box::new(ButtonAction::Loading)) .menu_with_check("Selected", selected, Box::new(ButtonAction::Selected)) .menu_with_check( "Compact", compact, Box::new(ButtonAction::Compact), ) }), ), ) .child( section("Outline").child( DropdownButton::new("btn-outline") .outline() .danger() .button(Button::new("btn").label("Outline Dropdown")) .when(self.compact, |this| this.compact()) .loading(self.loading) .disabled(self.disabled) .selected(selected) .dropdown_menu(move |this, _, _| { this.menu_with_check( "Disabled", disabled, Box::new(ButtonAction::Disabled), ) .menu_with_check("Loading", loading, Box::new(ButtonAction::Loading)) .menu_with_check("Selected", selected, Box::new(ButtonAction::Selected)) .menu_with_check( "Compact", compact, Box::new(ButtonAction::Compact), ) }), ), ) .child( section("Ghost").child( DropdownButton::new("btn-ghost") .ghost() .button(Button::new("btn").label("Ghost Dropdown")) .when(self.compact, |this| this.compact()) .loading(self.loading) .disabled(self.disabled) .selected(selected) .dropdown_menu(move |this, _, _| { this.menu_with_check( "Disabled", disabled, Box::new(ButtonAction::Disabled), ) .menu_with_check("Loading", loading, Box::new(ButtonAction::Loading)) .menu_with_check("Selected", selected, Box::new(ButtonAction::Selected)) .menu_with_check( "Compact", compact, Box::new(ButtonAction::Compact), ) }), ), ) } } ================================================ FILE: crates/story/src/stories/editor_story.rs ================================================ use gpui::{App, AppContext as _, Context, Entity, IntoElement, Render, Styled, Window}; use gpui_component::input::*; const EXAMPLE_CODE: &str = include_str!("./editor_story.rs"); pub struct EditorStory { editor_state: Entity, } impl super::Story for EditorStory { fn title() -> &'static str { "Editor" } fn description() -> &'static str { "Code editor with syntax highlighting by tree-sitter." } fn closable() -> bool { false } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl EditorStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let editor_state = cx.new(|cx| { InputState::new(window, cx) .code_editor("rust") .multi_line(true) .tab_size(TabSize { tab_size: 4, ..Default::default() }) .default_value(EXAMPLE_CODE) }); Self { editor_state } } } impl Render for EditorStory { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { Input::new(&self.editor_state).size_full() } } ================================================ FILE: crates/story/src/stories/form_story.rs ================================================ use gpui::{ App, AppContext, Axis, Context, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement as _, Render, Styled, Window, div, prelude::FluentBuilder as _, px, }; use gpui_component::{ ActiveTheme, AxisExt, IndexPath, Selectable, Sizable, Size, button::{Button, ButtonGroup}, checkbox::Checkbox, color_picker::{ColorPicker, ColorPickerState}, date_picker::{DatePicker, DatePickerState}, divider::Divider, form::{field, v_form}, h_flex, input::{Input, InputState}, select::{Select, SelectState}, switch::Switch, v_flex, }; pub struct FormStory { focus_handle: FocusHandle, name_prefix_state: Entity>>, name_input: Entity, email_input: Entity, bio_input: Entity, color_state: Entity, subscribe_email: bool, date: Entity, layout: Axis, size: Size, columns: usize, } impl super::Story for FormStory { fn title() -> &'static str { "Form" } fn description() -> &'static str { "Form to collect multiple inputs." } fn closable() -> bool { false } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl FormStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let name_prefix_state = cx.new(|cx| { SelectState::new( vec![ "Mr.".to_string(), "Mrs.".to_string(), "Ms.".to_string(), "Dr.".to_string(), ], Some(IndexPath::default()), window, cx, ) }); let name_input = cx.new(|cx| InputState::new(window, cx).default_value("Jason Lee")); let color_state = cx.new(|cx| ColorPickerState::new(window, cx)); let email_input = cx.new(|cx| InputState::new(window, cx).placeholder("Enter text here...")); let bio_input = cx.new(|cx| { InputState::new(window, cx) .auto_grow(5, 20) .placeholder("Enter text here...") .default_value("Hello 世界,this is GPUI component.") }); let date = cx.new(|cx| DatePickerState::new(window, cx)); Self { focus_handle: cx.focus_handle(), name_prefix_state, name_input, email_input, bio_input, date, color_state, subscribe_email: false, layout: Axis::Vertical, size: Size::default(), columns: 1, } } } impl Focusable for FormStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for FormStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let is_multi_column = self.columns > 1; let is_horizontal = self.layout.is_horizontal(); v_flex() .id("form-story") .size_full() .p_4() .justify_start() .gap_3() .child( h_flex() .gap_3() .flex_wrap() .justify_between() .child( h_flex() .gap_x_3() .child( Switch::new("layout") .checked(self.layout.is_horizontal()) .label("Horizontal") .on_click(cx.listener(|this, checked: &bool, _, cx| { if *checked { this.layout = Axis::Horizontal; } else { this.layout = Axis::Vertical; } cx.notify(); })), ) .child( Switch::new("column") .checked(self.columns > 1) .label("Multi Columns") .on_click(cx.listener(|this, checked: &bool, _, cx| { if *checked { this.columns = 2; } else { this.columns = 1; } cx.notify(); })), ), ) .child( ButtonGroup::new("size") .outline() .small() .child( Button::new("large") .selected(self.size == Size::Large) .child("Large"), ) .child( Button::new("medium") .child("Medium") .selected(self.size == Size::Medium), ) .child( Button::new("small") .child("Small") .selected(self.size == Size::Small), ) .on_click(cx.listener(|this, selecteds: &Vec, _, cx| { if selecteds.contains(&0) { this.size = Size::Large; } else if selecteds.contains(&1) { this.size = Size::Medium; } else if selecteds.contains(&2) { this.size = Size::Small; } cx.notify(); })), ), ) .child(Divider::horizontal()) .child( v_form() .layout(self.layout) .with_size(self.size) .columns(self.columns) .label_width(px(if is_multi_column { 100. } else { 140. })) .child( field().label_fn(|_, _| "Name").child( h_flex() .gap_2() .border_1() .border_color(cx.theme().input) .bg(cx.theme().input_background()) .rounded(cx.theme().radius) .child( div().w(px(90.)).child( Select::new(&self.name_prefix_state) .pr_0() .appearance(false), ), ) .child( div().flex_1().child( Input::new(&self.name_input).pl_0().appearance(false), ), ), ), ) .child( field() .label("Email") .child(Input::new(&self.email_input)) .required(true), ) .child( field() .label("Bio") .when(self.layout.is_vertical(), |this| this.items_start()) .child(Input::new(&self.bio_input)) .description_fn(|_, _| { div().child("Use at most 100 words to describe yourself.") }), ) .child( field() .label_indent(false) .when(is_multi_column, |this| this.col_span(2)) .child("This is a full width form field."), ) .child( field() .label("Please select your birthday") .description("Select your birthday, we will send you a gift.") .child(DatePicker::new(&self.date)), ) .child( field() .when(is_horizontal && is_multi_column, |this| { this.label_indent(false) }) .when(is_multi_column, |this| this.col_start(1)) .child( Switch::new("subscribe-newsletter") .label("Subscribe our newsletter") .checked(self.subscribe_email) .on_click(cx.listener(|this, checked: &bool, _, cx| { this.subscribe_email = *checked; cx.notify(); })), ), ) .child( field() .when(is_horizontal && is_multi_column, |this| { this.label_indent(false) }) .child( ColorPicker::new(&self.color_state) .small() .label("Theme color"), ), ) .child( field() .when(is_horizontal && is_multi_column, |this| { this.label_indent(false) }) .child( Checkbox::new("use-vertical-layout") .label("Vertical layout") .checked(self.layout.is_vertical()) .on_click(cx.listener(|this, checked: &bool, _, cx| { this.layout = if *checked { Axis::Vertical } else { Axis::Horizontal }; cx.notify(); })), ), ), ) } } ================================================ FILE: crates/story/src/stories/group_box_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, StyleRefinement, Styled, Window, relative, }; use gpui_component::{ ActiveTheme as _, StyledExt, button::{Button, ButtonVariants}, checkbox::Checkbox, group_box::{GroupBox, GroupBoxVariants as _}, h_flex, radio::{Radio, RadioGroup}, switch::Switch, text::markdown, v_flex, }; use crate::section; pub struct GroupBoxStory { focus_handle: gpui::FocusHandle, } impl super::Story for GroupBoxStory { fn title() -> &'static str { "GroupBox" } fn description() -> &'static str { "A styled container element that with an optional title \ to groups related content together." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl GroupBoxStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), } } } impl Focusable for GroupBoxStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for GroupBoxStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .justify_center() .gap_4() .child( section("Default Style").w_128().child( GroupBox::new() .child("Subscriptions") .child(Checkbox::new("all").label("All")) .child(Checkbox::new("news-letter").label("News Letter")) .child(Checkbox::new("account-activity").label("Account Activity")) .child(Button::new("ok").primary().label("Update Subscriptions")), ), ) .child( section("Fill Style").w_128().child( GroupBox::new() .id("activity") .fill() .title("Contributions & activity") .child( h_flex() .justify_between() .child("Make profile private and hide activity") .child(Switch::new("toggle-0").checked(true)), ) .child( h_flex() .justify_between() .child("Include private contributions on my profile") .child(Switch::new("toggle-1").checked(false)), ) .child(Button::new("btn-1").primary().label("Save")), ), ) .child( section("Outline Style").w_128().child( GroupBox::new() .id("appearance") .outline() .title("Appearance") .child( RadioGroup::vertical("theme") .child(Radio::new("light").label("Light")) .child(Radio::new("dark").label("Dark")) .child(Radio::new("system").label("System")), ), ), ) .child( section("Without Title").w_128().child( GroupBox::new().outline().child( h_flex() .justify_between() .child("Make profile private and hide activity") .child(Switch::new("toggle-1").checked(true)), ), ), ) .child( section("Custom style").w_128().child( GroupBox::new() .outline() .bg(cx.theme().group_box) .rounded_xl() .p_5() .title("This is a custom style") .title_style( StyleRefinement::default() .font_semibold() .line_height(relative(1.0)) .px_3(), ) .content_style( StyleRefinement::default() .rounded_xl() .py_3() .px_4() .border_2(), ) .child(markdown( "You can use `title_style` to customize the style \ of the title. \n \ And any style in `GroupBox` will apply to the content container.", )), ), ) } } ================================================ FILE: crates/story/src/stories/hover_card_story.rs ================================================ use gpui::{ App, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _, Window, div, px, relative, }; use gpui_component::{ ActiveTheme, Anchor, StyledExt, avatar::Avatar, button::Button, h_flex, hover_card::HoverCard, v_flex, }; use std::time::Duration; use crate::{Story, section}; pub struct HoverCardStory {} impl HoverCardStory { fn new(_: &mut Window, _: &mut Context) -> Self { Self {} } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl Render for HoverCardStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_6() .child(self.render_basic_example(cx)) .child(self.render_user_profile_example(cx)) .child(self.render_custom_timing_example(cx)) .child(self.render_positioning_examples(cx)) } } impl HoverCardStory { /// Basic hover card example fn render_basic_example(&self, cx: &mut Context) -> impl IntoElement { section("Basic").child( HoverCard::new("basic") .trigger( div() .child("Hover over me") .text_color(cx.theme().primary) .cursor_pointer() .text_sm(), ) .child( v_flex() .gap_1() .w(px(450.)) .child( div() .child("This is a hover card") .font_semibold() .text_sm(), ) .child( div() .child("You can display rich content when hovering over a trigger element.") .text_color(cx.theme().muted_foreground) .text_sm(), ), ), ) } fn render_user_profile_example(&self, cx: &mut Context) -> impl IntoElement { section("User Profile Preview").child( h_flex() .child("Hover over ") .child( HoverCard::new("user-profile") .trigger( div() .child("@huacnlee") .cursor_pointer() .text_color(cx.theme().link), ) .content(|_, _, cx| { h_flex() .w(px(320.)) .gap_3() .items_start() .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/5518?s=64"), ) .child( v_flex() .gap_1() .line_height(relative(1.)) .child(div().child("Jason Lee").font_semibold()) .child( div() .child("@huacnlee") .text_color(cx.theme().link) .text_sm(), ) .child(div().mt_1().child("The author of GPUI Component.")), ) }), ) .child(" to see their profile"), ) } /// Custom timing configuration example fn render_custom_timing_example(&self, _: &mut Context) -> impl IntoElement { section("Custom Timing").child( h_flex() .gap_4() .child( HoverCard::new("fast-open") .open_delay(Duration::from_millis(200)) .close_delay(Duration::from_millis(100)) .trigger(Button::new("fast").label("Fast Open (200ms)").outline()) .child(div().child("This hover card opens after 200ms").text_sm()), ) .child( HoverCard::new("slow-open") .open_delay(Duration::from_secs(1)) .close_delay(Duration::from_secs_f32(0.5)) .trigger(Button::new("slow").label("Slow Open (1000ms)").outline()) .child(div().child("This hover card opens after 1000ms").text_sm()), ), ) } /// All positioning options fn render_positioning_examples(&self, _: &mut Context) -> impl IntoElement { section("Positioning").child( v_flex() .gap_4() .items_center() .justify_center() .child( h_flex() .gap_4() .child( HoverCard::new("anchor-top-left") .anchor(Anchor::TopLeft) .trigger(Button::new("tl").label("Top Left").outline()) .child(div().child("Positioned at Top Left").text_sm()), ) .child( HoverCard::new("anchor-top-center") .anchor(Anchor::TopCenter) .trigger(Button::new("tc").label("Top Center").outline()) .child(div().child("Positioned at Top Center").text_sm()), ) .child( HoverCard::new("anchor-top-right") .anchor(Anchor::TopRight) .trigger(Button::new("tr").label("Top Right").outline()) .child(div().child("Positioned at Top Right").text_sm()), ), ) // Bottom row .child( h_flex() .gap_4() .child( HoverCard::new("anchor-bottom-left") .anchor(Anchor::BottomLeft) .trigger(Button::new("bl").label("Bottom Left").outline()) .child(div().child("Positioned at Bottom Left").text_sm()), ) .child( HoverCard::new("anchor-bottom-center") .anchor(Anchor::BottomCenter) .trigger(Button::new("bc").label("Bottom Center").outline()) .child(div().child("Positioned at Bottom Center").text_sm()), ) .child( HoverCard::new("anchor-bottom-right") .anchor(Anchor::BottomRight) .trigger(Button::new("br").label("Bottom Right").outline()) .child(div().child("Positioned at Bottom Right").text_sm()), ), ), ) } } impl Story for HoverCardStory { fn title() -> &'static str { "HoverCard" } fn description() -> &'static str { "A hover card displays content when hovering over a trigger element, with configurable delays." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } ================================================ FILE: crates/story/src/stories/icon_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Window, }; use gpui_component::{ button::{Button, ButtonVariant, ButtonVariants}, dock::PanelControl, h_flex, neutral_500, v_flex, ActiveTheme as _, Icon, IconName, Sizable, }; use crate::section; pub struct IconStory { focus_handle: gpui::FocusHandle, } impl IconStory { fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl super::Story for IconStory { fn title() -> &'static str { "Icon" } fn description() -> &'static str { "SVG Icons based on Lucide.dev" } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn zoomable() -> Option { None } } impl Focusable for IconStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for IconStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( section("Icon") .text_lg() .child(IconName::Info) .child(IconName::Map) .child(IconName::Bot) .child(IconName::Github) .child(IconName::Calendar) .child(IconName::Globe) .child(IconName::Heart), ) .child( section("Color Icon") .child( Icon::new(IconName::Maximize) .size_6() .text_color(cx.theme().green), ) .child( Icon::new(IconName::Minimize) .size_6() .text_color(cx.theme().red), ), ) .child( section("Icon Button").child( h_flex() .gap_4() .child( Button::new("like1") .icon( Icon::new(IconName::Heart) .text_color(neutral_500()) .size_6(), ) .with_variant(ButtonVariant::Ghost), ) .child( Button::new("like2") .icon( Icon::new(IconName::HeartOff) .text_color(cx.theme().red) .size_6(), ) .with_variant(ButtonVariant::Ghost), ) .child( Button::new("like3") .icon( Icon::new(IconName::Heart) .text_color(cx.theme().green) .size_6(), ) .with_variant(ButtonVariant::Ghost), ), ), ) .child( section("Button with size").child( Button::new("button-with-size") .outline() .size_5() .small() .px_0() .label("10"), ), ) } } ================================================ FILE: crates/story/src/stories/image_story.rs ================================================ use crate::section; use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement as _, Render, Styled, Window, img, }; use gpui_component::{dock::PanelControl, v_flex}; pub struct ImageStory { focus_handle: gpui::FocusHandle, } impl super::Story for ImageStory { fn title() -> &'static str { "Image" } fn description() -> &'static str { "Image and SVG image supported." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn zoomable() -> Option { Some(PanelControl::Toolbar) } } impl ImageStory { pub fn new(_: &mut Window, cx: &mut App) -> Self { Self { focus_handle: cx.focus_handle(), } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl Focusable for ImageStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for ImageStory { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex().gap_4().size_full().child( section("SVG from URL") .child(img("https://pub.lbkrs.com/files/202503/vEnnmgUM6bo362ya/sdk.svg").h_24()), ) } } ================================================ FILE: crates/story/src/stories/input_story.rs ================================================ use gpui::{ App, AppContext as _, ClickEvent, Context, Entity, InteractiveElement, IntoElement, ParentElement as _, Render, Styled, Subscription, Window, div, }; use crate::section; use gpui_component::{button::*, input::*, *}; const CODE_EXAMPLE: &str = r#"{"single_line":"code editor"}"#; pub fn init(_: &mut App) {} pub struct InputStory { input1: Entity, input2: Entity, input_esc: Entity, input_text_centered: Entity, input_text_right: Entity, mask_input: Entity, disabled_input: Entity, prefix_input1: Entity, suffix_input1: Entity, both_input1: Entity, large_input: Entity, small_input: Entity, phone_input: Entity, mask_input2: Entity, currency_input: Entity, custom_input: Entity, code_input: Entity, _subscriptions: Vec, } impl super::Story for InputStory { fn title() -> &'static str { "Input" } fn closable() -> bool { false } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl InputStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let input1 = cx.new(|cx| { InputState::new(window, cx) .default_value("Hello 世界,this is GPUI component, this is a long text.") }); let input2 = cx.new(|cx| InputState::new(window, cx).placeholder("Enter text here...")); let input_esc = cx.new(|cx| { InputState::new(window, cx) .placeholder("Enter text and clear it by pressing ESC") .clean_on_escape() }); let mask_input = cx.new(|cx| { InputState::new(window, cx) .masked(true) .placeholder("Enter your password...") .default_value("this-is-password-中文🚀🎉") }); let prefix_input1 = cx.new(|cx| InputState::new(window, cx).placeholder("Search some thing...")); let suffix_input1 = cx.new(|cx| { InputState::new(window, cx) .placeholder("This input only support [a-zA-Z0-9] characters.") .pattern(regex::Regex::new(r"^[a-zA-Z0-9]*$").unwrap()) }); let both_input1 = cx.new(|cx| { InputState::new(window, cx).placeholder("This input have prefix and suffix.") }); let phone_input = cx.new(|cx| InputState::new(window, cx).mask_pattern("(999)-999-9999")); let mask_input2 = cx.new(|cx| InputState::new(window, cx).mask_pattern("AAA-###-AAA")); let currency_input = cx.new(|cx| { InputState::new(window, cx).mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(3), }) }); let custom_input = cx.new(|cx| { InputState::new(window, cx).placeholder("Custom Input use monospace, 0123456789.") }); let code_input = cx.new(|cx| { InputState::new(window, cx) .code_editor("json") .multi_line(false) .show_whitespaces(true) .default_value(CODE_EXAMPLE) }); let input_text_centered = cx.new(|cx| { InputState::new(window, cx) .placeholder("Enter text to test center layout...") .default_value("Centered Text") }); let input_text_right = cx.new(|cx| { InputState::new(window, cx) .placeholder("Enter text to test right layout...") .default_value("Right Aligned Text") }); let _subscriptions = vec![ cx.subscribe_in(&input1, window, Self::on_input_event), cx.subscribe_in(&input2, window, Self::on_input_event), cx.subscribe_in(&phone_input, window, Self::on_input_event), ]; Self { input1, input2, input_esc, mask_input, disabled_input: cx .new(|cx| InputState::new(window, cx).default_value("This is disabled input")), large_input: cx.new(|cx| InputState::new(window, cx).placeholder("Large input")), small_input: cx.new(|cx| { InputState::new(window, cx) .validate(|s, _| s.parse::().is_ok()) .placeholder("validate to limit float number.") }), prefix_input1, suffix_input1, both_input1, phone_input, mask_input2, currency_input, custom_input, code_input, input_text_centered, input_text_right, _subscriptions, } } fn on_input_event( &mut self, state: &Entity, event: &InputEvent, window: &mut Window, cx: &mut Context, ) { match event { InputEvent::Change => { let text = state.read(cx).value(); if state == &self.input2 { println!("Set disabled value: {}", text); self.disabled_input.update(cx, |this, cx| { this.set_value(text, window, cx); }) } else { println!("Change: {}", text) } } InputEvent::PressEnter { secondary } => println!("PressEnter secondary: {}", secondary), InputEvent::Focus => println!("Focus"), InputEvent::Blur => println!("Blur"), }; } fn on_click_reset(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { self.code_input.update(cx, |input_state, cx| { input_state.set_value(CODE_EXAMPLE, window, cx); }); } } impl Render for InputStory { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .id("input-story") .size_full() .justify_start() .gap_3() .child( section("Normal Input") .max_w_md() .child(Input::new(&self.input1).cleanable(true)) .child(Input::new(&self.input2)), ) .child( section("Input State") .max_w_md() .child(Input::new(&self.disabled_input).disabled(true)) .child(Input::new(&self.mask_input).mask_toggle().cleanable(true)), ) .child( section("Text Align").max_w_lg().child( h_flex() .w_full() .gap_4() .flex_wrap() .child(Input::new(&self.input_text_centered).text_center().flex_1()) .child(Input::new(&self.input_text_right).text_right().flex_1()), ), ) .child( section("Prefix and Suffix") .max_w_md() .child( Input::new(&self.prefix_input1) .cleanable(true) .prefix(Icon::new(IconName::Search).small()), ) .child( Input::new(&self.both_input1) .cleanable(true) .prefix(div().child(Icon::new(IconName::Search).small())) .suffix(Button::new("info").ghost().icon(IconName::Info).xsmall()), ) .child( Input::new(&self.suffix_input1) .cleanable(true) .suffix(Button::new("info").ghost().icon(IconName::Info).xsmall()), ), ) .child( section("Currency Input with thousands separator") .max_w_md() .child(Input::new(&self.currency_input)) .child( div().child(format!("Value: {:?}", self.currency_input.read(cx).value())), ), ) .child( section("Input with mask pattern: (999)-999-9999") .max_w_md() .child(Input::new(&self.phone_input)) .child( v_flex() .child(format!("Value: {:?}", self.phone_input.read(cx).value())) .child(format!( "Unmask Value: {:?}", self.phone_input.read(cx).unmask_value() )), ), ) .child( section("Input with mask pattern: AAA-###-AAA") .max_w_md() .child(Input::new(&self.mask_input2)) .child( v_flex() .child(format!("Value: {:?}", self.mask_input2.read(cx).value())) .child(format!( "Unmask Value: {:?}", self.mask_input2.read(cx).unmask_value() )), ), ) .child( section("Input Size") .max_w_md() .child(Input::new(&self.large_input).large()) .child(Input::new(&self.small_input).small()), ) .child( section("Cleanable and ESC to clean") .max_w_md() .child(Input::new(&self.input_esc).cleanable(true)), ) .child( section("Focused Input") .max_w_md() .whitespace_normal() .overflow_hidden() .child(div().child(format!( "Value: {:?}", window.focused_input(cx).map(|input| input.read(cx).value()) ))), ) .child( section("Custom Appearance").max_w_md().child( div() .border_b_2() .px_6() .py_3() .font_family(cx.theme().mono_font_family.clone()) .border_color(cx.theme().border) .bg(cx.theme().secondary) .text_color(cx.theme().secondary_foreground) .w_full() .child(Input::new(&self.custom_input).appearance(false)), ), ) .child( section("Single line code editor").max_w_md().child( Input::new(&self.code_input).suffix( Button::new("code-reset") .ghost() .label("Reset") .xsmall() .on_click(cx.listener(Self::on_click_reset)), ), ), ) } } ================================================ FILE: crates/story/src/stories/kbd_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, Keystroke, ParentElement, Render, Styled, Window, }; use gpui_component::{h_flex, kbd::Kbd, v_flex}; use crate::section; pub struct KbdStory { focus_handle: gpui::FocusHandle, } impl super::Story for KbdStory { fn title() -> &'static str { "Kbd" } fn description() -> &'static str { "A tag style to display keyboard shortcuts" } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl KbdStory { pub(crate) fn new(_: &mut Window, cx: &mut App) -> Self { Self { focus_handle: cx.focus_handle(), } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl Focusable for KbdStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for KbdStory { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_6() .child( section("Kbd").child( h_flex() .gap_2() .child(Kbd::new(Keystroke::parse("cmd-shift-p").unwrap())) .child(Kbd::new(Keystroke::parse("cmd-ctrl-t").unwrap())) .child(Kbd::new(Keystroke::parse("cmd--").unwrap())) .child(Kbd::new(Keystroke::parse("cmd-+").unwrap())) .child(Kbd::new(Keystroke::parse("escape").unwrap())) .child(Kbd::new(Keystroke::parse("backspace").unwrap())) .child(Kbd::new(Keystroke::parse("/").unwrap())) .child(Kbd::new(Keystroke::parse("enter").unwrap())), ), ) .child( section("Outline Style").child( h_flex() .gap_2() .child(Kbd::new(Keystroke::parse("cmd-shift-p").unwrap()).outline()) .child(Kbd::new(Keystroke::parse("cmd-ctrl-t").unwrap()).outline()) .child(Kbd::new(Keystroke::parse("enter").unwrap()).outline()), ), ) } } ================================================ FILE: crates/story/src/stories/label_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, div, px, rems, }; use gpui_component::{ IconName, StyledExt, button::{Button, ButtonVariant, ButtonVariants as _}, checkbox::Checkbox, green_500, h_flex, input::{Input, InputEvent, InputState}, label::{HighlightsMatch, Label}, v_flex, }; use crate::section; pub struct LabelStory { focus_handle: gpui::FocusHandle, masked: bool, highlights_text: SharedString, highlights_input: Entity, prefix: bool, _subscriptions: Vec, } impl super::Story for LabelStory { fn title() -> &'static str { "Label" } fn description() -> &'static str { "Label used to display text or other content." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl LabelStory { pub(crate) fn new(window: &mut Window, cx: &mut Context) -> Self { let highlights_input = cx.new(|cx| { InputState::new(window, cx) .placeholder("Enter text to highlight in the label") .clean_on_escape() }); let _subscriptions = vec![ cx.subscribe(&highlights_input, |this, state, e: &InputEvent, cx| { if let InputEvent::Change = e { this.highlights_text = state.read(cx).value(); cx.notify(); } }), ]; Self { focus_handle: cx.focus_handle(), masked: false, highlights_text: Default::default(), highlights_input, prefix: false, _subscriptions, } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } #[allow(unused)] fn on_click(checked: &bool, window: &mut Window, cx: &mut App) { println!("Check value changed: {}", checked); } fn highlights_text(&self) -> HighlightsMatch { if self.prefix { HighlightsMatch::Prefix(self.highlights_text.clone()) } else { HighlightsMatch::Full(self.highlights_text.clone()) } } } impl Focusable for LabelStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for LabelStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let ht = self.highlights_text(); v_flex() .gap_6() .child( h_flex() .gap_x_3() .child(Input::new(&self.highlights_input).w_1_3()) .child( Checkbox::new("prefix") .label("Prefix") .checked(self.prefix) .on_click(cx.listener(|view, _, _, cx| { view.prefix = !view.prefix; cx.notify(); })), ), ) .child( section("Label").max_w_md().items_start().child( v_flex() .gap_y_4() .child(Label::new("This is a label").highlights(ht.clone())) // This case for test match CJK with ASCII, it was has a crash bug before. // Try to input "AA" to see the highlights effect. .child(Label::new("AAA中文BB").highlights(ht.clone())), ), ) .child( section("Label with secondary text") .max_w_md() .items_start() .child( Label::new("Company Address") .secondary("(optional)") .highlights(ht.clone()), ), ) .child( section("Alignment").max_w_md().child( v_flex() .w_full() .gap_4() .child(Label::new("Text align left").highlights(ht.clone())) .child( Label::new("Text align center") .text_center() .highlights(ht.clone()), ) .child( Label::new("Text align right") .text_right() .highlights(ht.clone()), ), ), ) .child( section("Label with color").max_w_md().child( Label::new("Color Label") .text_color(green_500()) .highlights(ht.clone()), ), ) .child( section("Font Size").max_w_md().child( Label::new("Font Size Label") .text_size(px(20.)) .font_semibold() .line_height(rems(1.8)) .highlights(ht.clone()), ), ) .child( section("Multi-line, line-height and text wrap") .max_w_md() .child( div().w(px(200.)).child( Label::new( "Label should support text wrap in default, \ if the text is too long, it should wrap to the next line.", ) .line_height(rems(1.8)) .highlights(ht.clone()), ), ), ) .child( section("Masked Label").max_w_md().child( v_flex() .w_full() .gap_4() .child( h_flex() .child( Label::new("9,182,1 USD") .text_2xl() .masked(self.masked) .highlights(ht.clone()), ) .child( Button::new("btn-mask") .with_variant(ButtonVariant::Ghost) .icon(if self.masked { IconName::EyeOff } else { IconName::Eye }) .on_click(cx.listener(|this, _, _, _| { this.masked = !this.masked; })), ), ) .child( Label::new("500 USD") .text_xl() .masked(self.masked) .highlights(ht.clone()), ), ), ) } } ================================================ FILE: crates/story/src/stories/list_story.rs ================================================ use std::{rc::Rc, time::Duration}; use fake::Fake; use gpui::{ App, AppContext, Context, ElementId, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, RenderOnce, ScrollStrategy, SharedString, Styled, Subscription, Task, Window, actions, div, px, }; use gpui_component::{ ActiveTheme, Icon, IconName, IndexPath, Selectable, Sizable, button::Button, checkbox::Checkbox, h_flex, label::Label, list::{List, ListDelegate, ListEvent, ListItem, ListState}, v_flex, }; actions!(list_story, [SelectedCompany]); #[derive(Clone, Default)] struct Company { name: SharedString, industry: SharedString, last_done: f64, prev_close: f64, change_percent: f64, change_percent_str: SharedString, last_done_str: SharedString, prev_close_str: SharedString, // description: String, } impl Company { fn prepare(mut self) -> Self { self.change_percent = (self.last_done - self.prev_close) / self.prev_close; self.change_percent_str = format!("{:.2}%", self.change_percent).into(); self.last_done_str = format!("{:.2}", self.last_done).into(); self.prev_close_str = format!("{:.2}", self.prev_close).into(); self } } #[derive(IntoElement)] struct CompanyListItem { base: ListItem, company: Rc, selected: bool, } impl CompanyListItem { pub fn new(id: impl Into, company: Rc, selected: bool) -> Self { CompanyListItem { company, base: ListItem::new(id).selected(selected), selected, } } } impl Selectable for CompanyListItem { fn selected(mut self, selected: bool) -> Self { self.selected = selected; self } fn is_selected(&self) -> bool { self.selected } } impl RenderOnce for CompanyListItem { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let text_color = if self.selected { cx.theme().accent_foreground } else { cx.theme().foreground }; let trend_color = match self.company.change_percent { change if change > 0.0 => cx.theme().green, change if change < 0.0 => cx.theme().red, _ => cx.theme().foreground, }; self.base .px_2() .py_1() .overflow_x_hidden() .border_1() .rounded(cx.theme().radius) .child( h_flex() .items_center() .justify_between() .gap_2() .text_color(text_color) .child( h_flex().gap_2().child( v_flex() .gap_1() .max_w(px(500.)) .overflow_x_hidden() .flex_nowrap() .child(Label::new(self.company.name.clone()).whitespace_nowrap()), ), ) .child( h_flex() .gap_2() .items_center() .justify_end() .child( div() .w(px(65.)) .text_color(text_color) .child(self.company.last_done_str.clone()), ) .child( h_flex().w(px(65.)).justify_end().child( div() .rounded(cx.theme().radius) .whitespace_nowrap() .text_size(px(12.)) .px_1() .text_color(trend_color) .child(self.company.change_percent_str.clone()), ), ), ), ) } } struct CompanyListDelegate { industries: Vec, _companies: Vec>, matched_companies: Vec>>, selected_index: Option, confirmed_index: Option, query: SharedString, loading: bool, eof: bool, lazy_load: bool, } impl CompanyListDelegate { fn prepare(&mut self, query: impl Into) { self.query = query.into(); let companies: Vec> = self ._companies .iter() .filter(|company| { company .name .to_lowercase() .contains(&self.query.to_lowercase()) }) .cloned() .collect(); for company in companies.into_iter() { if let Some(ix) = self.industries.iter().position(|s| s == &company.industry) { self.matched_companies[ix].push(company); } else { self.industries.push(company.industry.clone()); self.matched_companies.push(vec![company]); } } } fn extend_more(&mut self, len: usize) { self._companies .extend((0..len).map(|_| Rc::new(random_company()))); self.prepare(self.query.clone()); } fn selected_company(&self) -> Option> { let Some(ix) = self.selected_index else { return None; }; self.matched_companies .get(ix.section) .and_then(|c| c.get(ix.row)) .cloned() } } impl ListDelegate for CompanyListDelegate { type Item = CompanyListItem; fn sections_count(&self, _: &App) -> usize { self.industries.len() } fn items_count(&self, section: usize, _: &App) -> usize { if matches!(section, 0 | 2 | 3) { // Return some empty sections for testing. return 0; } self.matched_companies[section].len() } fn perform_search( &mut self, query: &str, _: &mut Window, _: &mut Context>, ) -> Task<()> { self.prepare(query.to_owned()); Task::ready(()) } fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { println!("Confirmed with secondary: {}", secondary); window.dispatch_action(Box::new(SelectedCompany), cx); } fn set_selected_index( &mut self, ix: Option, _: &mut Window, cx: &mut Context>, ) { self.selected_index = ix; cx.notify(); } fn set_right_clicked_index( &mut self, ix: Option, _: &mut Window, _: &mut Context>, ) { println!("right_clicked_index: {:?}", ix); } fn render_section_header( &mut self, section: usize, _: &mut Window, cx: &mut Context>, ) -> Option { let Some(industry) = self.industries.get(section) else { return None; }; Some( h_flex() .pb_1() .px_2() .gap_2() .text_sm() .text_color(cx.theme().muted_foreground) .child(Icon::new(IconName::Folder)) .child(industry.clone()) .child(format!("(section: {})", section)), ) } fn render_section_footer( &mut self, section: usize, _: &mut Window, cx: &mut Context>, ) -> Option { let Some(_) = self.industries.get(section) else { return None; }; Some( div() .pt_1() .pb_5() .px_2() .text_xs() .text_color(cx.theme().muted_foreground) .child(format!( "Total {} items in section.", self.matched_companies[section].len() )), ) } fn render_item( &mut self, ix: IndexPath, _: &mut Window, _: &mut Context>, ) -> Option { let selected = Some(ix) == self.selected_index || Some(ix) == self.confirmed_index; if let Some(company) = self.matched_companies[ix.section].get(ix.row) { return Some(CompanyListItem::new(ix, company.clone(), selected)); } None } fn loading(&self, _: &App) -> bool { self.loading } fn has_more(&self, _: &App) -> bool { if self.loading { return false; } return !self.eof; } fn load_more_threshold(&self) -> usize { 150 } fn load_more(&mut self, window: &mut Window, cx: &mut Context>) { if !self.lazy_load { return; } cx.spawn_in(window, async move |view, window| { // Simulate network request, delay 1s to load data. window .background_executor() .timer(Duration::from_secs(1)) .await; _ = view.update_in(window, move |view, window, cx| { let query = view.delegate().query.clone(); view.delegate_mut().extend_more(200); _ = view.delegate_mut().perform_search(&query, window, cx); view.delegate_mut().eof = view.delegate()._companies.len() >= 6000; }); }) .detach(); } } pub struct ListStory { focus_handle: FocusHandle, company_list: Entity>, selected_company: Option>, selectable: bool, searchable: bool, _subscriptions: Vec, } impl super::Story for ListStory { fn title() -> &'static str { "List" } fn description() -> &'static str { "A list displays a series of items." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl ListStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let mut delegate = CompanyListDelegate { industries: vec![], matched_companies: vec![vec![]], _companies: vec![], selected_index: Some(IndexPath::default()), confirmed_index: None, query: "".into(), loading: false, eof: false, lazy_load: false, }; delegate.extend_more(100); let company_list = cx.new(|cx| ListState::new(delegate, window, cx).searchable(true)); let _subscriptions = vec![ cx.subscribe(&company_list, |_, _, ev: &ListEvent, _| match ev { ListEvent::Select(ix) => { println!("List Selected: {:?}", ix); } ListEvent::Confirm(ix) => { println!("List Confirmed: {:?}", ix); } ListEvent::Cancel => { println!("List Cancelled"); } }), ]; // Spawn a background to random refresh the list cx.spawn(async move |this, cx| { this.update(cx, |this, cx| { this.company_list.update(cx, |picker, _| { picker .delegate_mut() ._companies .iter_mut() .for_each(|company| { let mut new_company = random_company(); new_company.name = company.name.clone(); new_company.industry = company.industry.clone(); *company = Rc::new(new_company); }); picker.delegate_mut().prepare(""); }); cx.notify(); }) .ok(); }) .detach(); Self { focus_handle: cx.focus_handle(), searchable: true, selectable: true, company_list, selected_company: None, _subscriptions, } } fn selected_company(&mut self, _: &SelectedCompany, _: &mut Window, cx: &mut Context) { let picker = self.company_list.read(cx); if let Some(company) = picker.delegate().selected_company() { self.selected_company = Some(company); } } fn toggle_selectable(&mut self, selectable: bool, _: &mut Window, cx: &mut Context) { self.selectable = selectable; self.company_list.update(cx, |list, cx| { list.set_selectable(self.selectable, cx); }) } fn toggle_searchable(&mut self, searchable: bool, _: &mut Window, cx: &mut Context) { self.searchable = searchable; self.company_list.update(cx, |list, cx| { list.set_searchable(self.searchable, cx); }) } } fn random_company() -> Company { let last_done = (0.0..999.0).fake::(); let prev_close = last_done * (-0.1..0.1).fake::(); Company { name: fake::faker::company::en::CompanyName() .fake::() .into(), industry: fake::faker::company::en::Industry().fake::().into(), last_done, prev_close, ..Default::default() } .prepare() } impl Focusable for ListStory { fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { self.focus_handle.clone() } } impl Render for ListStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let lazy_load = self.company_list.read(cx).delegate().lazy_load; v_flex() .track_focus(&self.focus_handle) .on_action(cx.listener(Self::selected_company)) .size_full() .gap_4() .child( h_flex() .gap_2() .child( Button::new("scroll-top") .outline() .child("Scroll to Top") .small() .on_click(cx.listener(|this, _, window, cx| { this.company_list.update(cx, |list, cx| { list.scroll_to_item( IndexPath::default(), ScrollStrategy::Top, window, cx, ); cx.notify(); }) })), ) .child( Button::new("scroll-selected") .outline() .child("Scroll to selected") .small() .on_click(cx.listener(|this, _, window, cx| { this.company_list.update(cx, |list, cx| { list.scroll_to_selected_item(window, cx); }) })), ) .child( Button::new("scroll-to-item") .outline() .child("Scroll to (5, 1)") .small() .on_click(cx.listener(|this, _, window, cx| { this.company_list.update(cx, |list, cx| { list.scroll_to_item( IndexPath::new(1).section(5), ScrollStrategy::Center, window, cx, ); }) })), ) .child( Button::new("scroll-bottom") .outline() .child("Scroll to Bottom") .small() .on_click(cx.listener(|this, _, window, cx| { this.company_list.update(cx, |list, cx| { let last_section = list.delegate().sections_count(cx).saturating_sub(1); list.scroll_to_item( IndexPath::default().section(last_section).row( list.delegate() .items_count(last_section, cx) .saturating_sub(1), ), ScrollStrategy::Top, window, cx, ); }) })), ) .child( Checkbox::new("selectable") .label("Selectable") .checked(self.selectable) .on_click(cx.listener(|this, check: &bool, window, cx| { this.toggle_selectable(*check, window, cx) })), ) .child( Checkbox::new("searchable") .label("Searchable") .checked(self.searchable) .on_click(cx.listener(|this, check: &bool, window, cx| { this.toggle_searchable(*check, window, cx) })), ) .child( Checkbox::new("loading") .label("Loading") .checked(self.company_list.read(cx).delegate().loading) .on_click(cx.listener(|this, check: &bool, _, cx| { this.company_list.update(cx, |this, cx| { this.delegate_mut().loading = *check; cx.notify(); }) })), ) .child( Checkbox::new("lazy_load") .label("Lazy Load") .checked(lazy_load) .on_click(cx.listener(|this, check: &bool, _, cx| { this.company_list.update(cx, |this, cx| { this.delegate_mut().lazy_load = *check; cx.notify(); }) })), ), ) .child( List::new(&self.company_list) .p(px(8.)) .flex_1() .w_full() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius), ) } } ================================================ FILE: crates/story/src/stories/menu_story.rs ================================================ use gpui::{ Action, App, AppContext, Context, Corner, Entity, InteractiveElement, IntoElement, KeyBinding, ParentElement as _, Render, SharedString, Styled as _, Window, actions, div, px, }; use gpui_component::{ ActiveTheme as _, IconName, Side, StyledExt, button::Button, h_flex, menu::{ContextMenuExt, DropdownMenu as _, PopupMenuItem}, v_flex, }; use serde::Deserialize; use crate::section; #[derive(Action, Clone, PartialEq, Deserialize)] #[action(namespace = menu_story, no_json)] struct Info(usize); actions!(menu_story, [Copy, Paste, Cut, SearchAll, ToggleCheck]); const CONTEXT: &str = "menu_story"; pub fn init(cx: &mut App) { cx.bind_keys([ #[cfg(target_os = "macos")] KeyBinding::new("cmd-c", Copy, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-v", Paste, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-x", Cut, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-shift-f", SearchAll, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-shift-f", SearchAll, Some(CONTEXT)), KeyBinding::new("ctrl-shift-alt-t", ToggleCheck, Some(CONTEXT)), ]) } pub struct MenuStory { check_side: Option, message: String, } impl super::Story for MenuStory { fn title() -> &'static str { "Menu" } fn description() -> &'static str { "Popup menu and context menu" } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl MenuStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, _: &mut Context) -> Self { Self { check_side: None, message: "".to_string(), } } fn on_copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { self.message = "You have clicked copy".to_string(); cx.notify() } fn on_cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context) { self.message = "You have clicked cut".to_string(); cx.notify() } fn on_paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context) { self.message = "You have clicked paste".to_string(); cx.notify() } fn on_search_all(&mut self, _: &SearchAll, _: &mut Window, cx: &mut Context) { self.message = "You have clicked search all".to_string(); cx.notify() } fn on_action_info(&mut self, info: &Info, _: &mut Window, cx: &mut Context) { self.message = format!("You have clicked info: {}", info.0); cx.notify() } fn on_action_toggle_check(&mut self, _: &ToggleCheck, _: &mut Window, cx: &mut Context) { self.check_side = if self.check_side == Some(Side::Left) { Some(Side::Right) } else if self.check_side == Some(Side::Right) { None } else { Some(Side::Left) }; self.message = format!("You have used check at side: {:?}", self.check_side); cx.notify() } } impl Render for MenuStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let check_side = self.check_side; let view = cx.entity(); v_flex() .key_context(CONTEXT) .on_action(cx.listener(Self::on_copy)) .on_action(cx.listener(Self::on_cut)) .on_action(cx.listener(Self::on_paste)) .on_action(cx.listener(Self::on_search_all)) .on_action(cx.listener(Self::on_action_info)) .on_action(cx.listener(Self::on_action_toggle_check)) .size_full() .min_h(px(400.)) .gap_6() .child( section("Popup Menu") .child( Button::new("popup-menu-1") .outline() .label("Edit") .dropdown_menu(move |this, window, cx| { this.link("About", "https://github.com/longbridge/gpui-component") .check_side(check_side.unwrap_or(Side::Left)) .separator() .item(PopupMenuItem::new("Handle Click").on_click( window.listener_for(&view, |this, _, _, cx| { this.message = "You have clicked Handle Click".to_string(); cx.notify(); }), )) .separator() .menu("Copy", Box::new(Copy)) .menu("Cut", Box::new(Cut)) .menu("Paste", Box::new(Paste)) .separator() .menu_with_check( format!("Check Side {:?}", check_side), check_side.is_some(), Box::new(ToggleCheck), ) .separator() .menu_with_icon("Search", IconName::Search, Box::new(SearchAll)) .separator() .item( PopupMenuItem::element(|_, cx| { v_flex().child("Custom Element").child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("This is sub-title"), ) }) .on_click( window.listener_for(&view, |this, _, _, cx| { this.message = "You have clicked on custom element" .to_string(); cx.notify(); }), ), ) .menu_element_with_check( check_side.is_some(), Box::new(ToggleCheck), |_, cx| { h_flex().gap_1().child("Custom Element").child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("checked"), ) }, ) .menu_element_with_icon( IconName::Info, Box::new(Info(0)), |_, cx| { h_flex().gap_1().child("Custom").child( div() .text_sm() .text_color(cx.theme().muted_foreground) .child("element"), ) }, ) .separator() .menu_with_disabled("Disabled Item", Box::new(Info(0)), true) .separator() .submenu("Links", window, cx, |menu, _, _| { menu.link_with_icon( "GPUI Component", IconName::Github, "https://github.com/longbridge/gpui-component", ) .separator() .link("GPUI", "https://gpui.rs") .link("Zed", "https://zed.dev") }) .separator() .submenu("Other Links", window, cx, |menu, _, _| { menu.link("Crates", "https://crates.io") .link("Rust Docs", "https://docs.rs") }) }), ) .child(self.message.clone()), ) .child( section("Context Menu") .v_flex() .gap_4() .child( v_flex() .w_full() .p_4() .items_center() .justify_center() .min_h_20() .rounded(cx.theme().radius_lg) .border_2() .border_dashed() .border_color(cx.theme().border) .child("Right click to open ContextMenu") .context_menu({ move |this, window, cx| { this.check_side(check_side.unwrap_or(Side::Left)) .external_link_icon(false) .link( "About", "https://github.com/longbridge/gpui-component", ) .separator() .menu("Cut", Box::new(Cut)) .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() .label("This is a label") .menu_with_check( format!("Check Side {:?}", check_side), check_side.is_some(), Box::new(ToggleCheck), ) .separator() .submenu("Settings", window, cx, move |menu, _, _| { menu.menu("Info 0", Box::new(Info(0))) .separator() .menu("Item 1", Box::new(Info(1))) .menu("Item 2", Box::new(Info(2))) }) .separator() .menu("Search All", Box::new(SearchAll)) .separator() } }) .child( div() .text_sm() .text_color(cx.theme().muted_foreground) .child( "You can right click anywhere in \ this area to open the context menu.", ), ), ) .child( div() .id("other") .flex() .w_full() .p_4() .items_center() .justify_center() .min_h_20() .rounded(cx.theme().radius_lg) .border_2() .border_dashed() .border_color(cx.theme().border) .child("Here is another area with context menu.") .context_menu({ move |this, _, _| { this.link( "About", "https://github.com/longbridge/gpui-component", ) .separator() .menu("Item 1", Box::new(Info(1))) } }), ) .child( div() .id("other1") .flex() .w_full() .p_4() .items_center() .justify_center() .min_h_20() .rounded(cx.theme().radius_lg) .border_2() .border_dashed() .border_color(cx.theme().border) .child("ContextMenu area 1") .context_menu({ move |this, _, _| { this.link( "About", "https://github.com/longbridge/gpui-component", ) .separator() .menu("Item 1", Box::new(Info(1))) } }), ), ) .child( section("Menu with scrollbar") .child( Button::new("dropdown-menu-scrollable-1") .outline() .label("Scrollable Menu (100 items)") .dropdown_menu_with_anchor(Corner::TopRight, move |this, _, _| { let mut this = this .scrollable(true) .max_h(px(300.)) .label(format!("Total {} items", 100)); for i in 0..100 { if i % 5 == 0 { this = this.separator(); } this = this.menu( SharedString::from(format!("Item {}", i)), Box::new(Info(i)), ) } this.min_w(px(100.)) }), ) .child( Button::new("dropdown-menu-scrollable-2") .outline() .label("Scrollable Menu (5 items)") .dropdown_menu_with_anchor(Corner::TopRight, move |this, _, _| { let mut this = this .scrollable(true) .max_h(px(300.)) .label(format!("Total {} items", 100)); for i in 0..5 { this = this.menu( SharedString::from(format!("Item {}", i)), Box::new(Info(i)), ) } this.min_w(px(100.)) }), ), ) } } ================================================ FILE: crates/story/src/stories/mod.rs ================================================ use gpui::{AnyView, App, AppContext as _, Entity, Hsla, Pixels, Render, Window, px}; use gpui_component::dock::PanelControl; mod accordion_story; mod alert_dialog_story; mod alert_story; mod avatar_story; mod badge_story; mod breadcrumb_story; mod button_story; mod calendar_story; mod chart_story; mod checkbox_story; mod clipboard_story; mod collapsible_story; mod color_picker_story; mod data_table_story; mod date_picker_story; mod description_list_story; mod dialog_story; mod divider_story; mod dropdown_button_story; mod editor_story; mod form_story; mod group_box_story; mod hover_card_story; mod icon_story; mod image_story; mod input_story; mod kbd_story; mod label_story; mod list_story; mod menu_story; mod notification_story; mod number_input_story; mod otp_input_story; mod pagination_story; mod popover_story; mod progress_story; mod radio_story; mod rating_story; mod resizable_story; mod scrollbar_story; mod select_story; mod settings_story; mod sheet_story; mod sidebar_story; mod skeleton_story; mod slider_story; mod spinner_story; mod stepper_story; mod switch_story; mod table_story; mod tabs_story; mod tag_story; mod textarea_story; mod theme_story; mod toggle_story; mod tooltip_story; mod tree_story; mod virtual_list_story; mod welcome_story; pub use accordion_story::AccordionStory; pub use alert_dialog_story::AlertDialogStory; pub use alert_story::AlertStory; pub use avatar_story::AvatarStory; pub use badge_story::BadgeStory; pub use breadcrumb_story::BreadcrumbStory; pub use button_story::ButtonStory; pub use calendar_story::CalendarStory; pub use chart_story::ChartStory; pub use checkbox_story::CheckboxStory; pub use clipboard_story::ClipboardStory; pub use collapsible_story::CollapsibleStory; pub use color_picker_story::ColorPickerStory; pub use data_table_story::DataTableStory; pub use date_picker_story::DatePickerStory; pub use description_list_story::DescriptionListStory; pub use dialog_story::DialogStory; pub use divider_story::DividerStory; pub use dropdown_button_story::DropdownButtonStory; pub use editor_story::EditorStory; pub use form_story::FormStory; pub use group_box_story::GroupBoxStory; pub use hover_card_story::HoverCardStory; pub use icon_story::IconStory; pub use image_story::ImageStory; pub use input_story::InputStory; pub use kbd_story::KbdStory; pub use label_story::LabelStory; pub use list_story::ListStory; pub use menu_story::MenuStory; pub use notification_story::NotificationStory; pub use number_input_story::NumberInputStory; pub use otp_input_story::OtpInputStory; pub use pagination_story::PaginationStory; pub use popover_story::PopoverStory; pub use progress_story::ProgressStory; pub use radio_story::RadioStory; pub use rating_story::RatingStory; pub use resizable_story::ResizableStory; pub use scrollbar_story::ScrollbarStory; pub use select_story::SelectStory; pub use settings_story::SettingsStory; pub use sheet_story::SheetStory; pub use sidebar_story::SidebarStory; pub use skeleton_story::SkeletonStory; pub use slider_story::SliderStory; pub use spinner_story::SpinnerStory; pub use stepper_story::StepperStory; pub use switch_story::SwitchStory; pub use table_story::TableStory; pub use tabs_story::TabsStory; pub use tag_story::TagStory; pub use textarea_story::TextareaStory; pub use theme_story::ThemeColorsStory; pub use toggle_story::ToggleStory; pub use tooltip_story::TooltipStory; pub use tree_story::TreeStory; pub use virtual_list_story::VirtualListStory; pub use welcome_story::WelcomeStory; pub(crate) fn init(cx: &mut App) { input_story::init(cx); rating_story::init(cx); number_input_story::init(cx); textarea_story::init(cx); select_story::init(cx); popover_story::init(cx); menu_story::init(cx); tooltip_story::init(cx); otp_input_story::init(cx); tree_story::init(cx); } pub trait Story: Render + Sized { fn klass() -> &'static str { std::any::type_name::().split("::").last().unwrap() } fn title() -> &'static str; fn description() -> &'static str { "" } fn closable() -> bool { true } fn zoomable() -> Option { Some(PanelControl::default()) } fn title_bg() -> Option { None } fn paddings() -> Pixels { px(16.) } fn new_view(window: &mut Window, cx: &mut App) -> Entity; fn on_active(&mut self, active: bool, window: &mut Window, cx: &mut App) { let _ = active; let _ = window; let _ = cx; } fn on_active_any(view: AnyView, active: bool, window: &mut Window, cx: &mut App) where Self: 'static, { if let Some(story) = view.downcast::().ok() { cx.update_entity(&story, |story, cx| { story.on_active(active, window, cx); }); } } } ================================================ FILE: crates/story/src/stories/notification_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, InteractiveElement as _, IntoElement, ParentElement, Render, Styled, Window, }; use gpui_component::{ ActiveTheme, Anchor, Theme, WindowExt as _, button::{Button, ButtonVariants}, h_flex, menu::{DropdownMenu as _, PopupMenuItem}, notification::{Notification, NotificationType}, text::markdown, v_flex, }; use crate::section; const NOTIFICATION_MARKDOWN: &str = r#" This is a custom notification. - List item 1 - List item 2 - [Click here](https://github.com/longbridge/gpui-component) "#; pub struct NotificationStory { focus_handle: FocusHandle, } impl super::Story for NotificationStory { fn title() -> &'static str { "Notification" } fn description() -> &'static str { "Push notifications to display a message at the top right of the window" } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl NotificationStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), } } } impl Focusable for NotificationStory { fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { self.focus_handle.clone() } } impl Render for NotificationStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { const ANCHORS: [Anchor; 6] = [ Anchor::TopLeft, Anchor::TopCenter, Anchor::TopRight, Anchor::BottomLeft, Anchor::BottomCenter, Anchor::BottomRight, ]; let view = cx.entity(); v_flex() .id("notification-story") .track_focus(&self.focus_handle) .size_full() .gap_3() .child( h_flex().gap_3().child( Button::new("placement") .outline() .label(cx.theme().notification.placement.to_string()) .dropdown_menu(move |menu, window, cx| { let menu = ANCHORS.into_iter().fold(menu, |menu, placement| { menu.item( PopupMenuItem::new(placement.to_string()) .checked(cx.theme().notification.placement == placement) .on_click(window.listener_for( &view, move |_, _, _, cx| { Theme::global_mut(cx).notification.placement = placement; cx.notify(); }, )), ) }); menu }), ), ) .child( section("Simple Notification").child( Button::new("show-notify-0") .outline() .label("Show Notification") .on_click(cx.listener(|_, _, window, cx| { window.push_notification("This is a notification.", cx) })), ), ) .child( section("Notification with Type") .child( Button::new("show-notify-info") .info() .label("Info") .on_click(cx.listener(|_, _, window, cx| { window.push_notification( ( NotificationType::Info, "You have been saved file successfully.", ), cx, ) })), ) .child( Button::new("show-notify-error") .danger() .label("Error") .on_click(cx.listener(|_, _, window, cx| { window.push_notification( ( NotificationType::Error, "There have some error occurred. Please try again later.", ), cx, ) })), ) .child( Button::new("show-notify-success") .success() .label("Success") .on_click(cx.listener(|_, _, window, cx| { window.push_notification( ( NotificationType::Success, "We have received your payment successfully.", ), cx, ) })), ) .child( Button::new("show-notify-warning") .warning() .label("Warning") .on_click(cx.listener(|_, _, window, cx| { window.push_notification( ( NotificationType::Warning, "The network is not stable, please check your connection.", ), cx, ) })), ), ) .child( section("Unique Notification").child( Button::new("show-notify-unique") .outline() .label("Unique Notification") .on_click(cx.listener(|_, _, window, cx| { window.push_notification( Notification::info("This is a unique notification.") .id::() .message("This is a unique notification."), cx, ) })), ), ) .child( section("Unique with Key").child( h_flex() .gap_3() .child( Button::new("show-notify-unique-key0") .outline() .label("A Notification") .on_click(cx.listener(|_, _, window, cx| { window.push_notification( Notification::info("This is A unique notification.") .id1::(1), cx, ) })), ) .child( Button::new("show-notify-unique-key1") .outline() .label("B Notification") .on_click(cx.listener(|_, _, window, cx| { window.push_notification( Notification::info("This is B unique notification.") .id1::(2), cx, ) })), ), ), ) .child( section("With title and action").child( Button::new("show-notify-with-title") .outline() .label("Notification with Title") .on_click(cx.listener(|_, _, window, cx| { struct TestNotification; window.push_notification( Notification::new() .id::() .title("Uh oh! Something went wrong.") .message("There was a problem with your request.") .action(|_, _, cx| { Button::new("try-again").primary().label("Retry").on_click( cx.listener(|this, _, window, cx| { println!("You have clicked the try again action."); this.dismiss(window, cx); }), ) }) .on_click(cx.listener(|_, _, _, cx| { println!("Notification clicked"); cx.notify(); })), cx, ) })), ), ) .child( section("Custom Notification").child( Button::new("show-notify-custom") .outline() .label("Show Custom Notification") .on_click(cx.listener(|_, _, window, cx| { window.push_notification( Notification::new().content(|_, _, _| { markdown(NOTIFICATION_MARKDOWN).into_any_element() }), cx, ) })), ), ) .child({ struct ManualOpenNotification; section("Manual Close Notification") .child( Button::new("manual-open-notify") .outline() .label("Show") .on_click(cx.listener(|_, _, window, cx| { window.push_notification( Notification::new() .id::() .message( "You can close this notification by \ clicking the Close button.", ) .autohide(false), cx, ); })), ) .child( Button::new("manual-close-notify") .outline() .label("Dismiss All") .on_click(cx.listener(|_, _, window, cx| { window.remove_notification::(cx); })), ) }) } } ================================================ FILE: crates/story/src/stories/number_input_story.rs ================================================ use gpui::{ App, AppContext as _, Context, Entity, Focusable, InteractiveElement, IntoElement, ParentElement as _, Render, Styled, Subscription, Window, px, }; use regex::Regex; use crate::section; use gpui_component::{ ActiveTheme, Disableable, IconName, Sizable, button::{Button, ButtonVariants}, input::{InputEvent, InputState, MaskPattern, NumberInput, NumberInputEvent, StepAction}, v_flex, }; pub fn init(_: &mut App) {} pub struct NumberInputStory { number_input1_value: i64, number_input1: Entity, number_input2: Entity, number_input2_value: u64, number_input3: Entity, number_input3_value: f64, number_input4: Entity, number_input4_value: f64, disabled_input: Entity, _subscriptions: Vec, } impl super::Story for NumberInputStory { fn title() -> &'static str { "NumberInput" } fn description() -> &'static str { "NumberInput design to support + - to adjust the input value." } fn closable() -> bool { false } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl NumberInputStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let number_input1_value = 1; let number_input1 = cx.new(|cx| { InputState::new(window, cx) .placeholder("Normal Integer") .default_value(number_input1_value.to_string()) }); let number_input2 = cx.new(|cx| { InputState::new(window, cx) .placeholder("Unsized Integer") .pattern(Regex::new(r"^\d+$").unwrap()) }); let number_input3 = cx.new(|cx| { InputState::new(window, cx) .placeholder("Mask pattern") .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), }) }); let number_input4 = cx.new(|cx| { InputState::new(window, cx) .placeholder("Styling") .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), }) }); let disabled_input = cx.new(|cx| { InputState::new(window, cx) .default_value("100") .placeholder("Disabled") }); let _subscriptions = vec![ cx.subscribe_in(&number_input1, window, Self::on_input_event), cx.subscribe_in(&number_input1, window, Self::on_number_input_event), cx.subscribe_in(&number_input2, window, Self::on_input_event), cx.subscribe_in(&number_input2, window, Self::on_number_input_event), cx.subscribe_in(&number_input3, window, Self::on_input_event), cx.subscribe_in(&number_input3, window, Self::on_number_input_event), cx.subscribe_in(&number_input4, window, Self::on_input_event), cx.subscribe_in(&number_input4, window, Self::on_number_input_event), cx.subscribe_in(&disabled_input, window, Self::on_input_event), cx.subscribe_in(&disabled_input, window, Self::on_number_input_event), ]; Self { number_input1, number_input1_value, number_input2, number_input2_value: 0, number_input3, number_input3_value: 0.0, number_input4, number_input4_value: 0.0, disabled_input, _subscriptions, } } fn on_input_event( &mut self, state: &Entity, event: &InputEvent, _: &mut Window, cx: &mut Context, ) { match event { InputEvent::Change => { let text = state.read(cx).value(); if state == &self.number_input1 { if let Ok(value) = text.parse::() { self.number_input1_value = value; } } else if state == &self.number_input2 { if let Ok(value) = text.parse::() { self.number_input2_value = value; } } else if state == &self.number_input3 { if let Ok(value) = text.parse::() { self.number_input3_value = value; } } println!("Change: {}", text); } InputEvent::PressEnter { secondary } => { println!("PressEnter secondary: {}", secondary) } InputEvent::Focus => println!("Focus"), InputEvent::Blur => println!("Blur"), } } fn on_number_input_event( &mut self, this: &Entity, event: &NumberInputEvent, window: &mut Window, cx: &mut Context, ) { match event { NumberInputEvent::Step(step_action) => match step_action { StepAction::Decrement => { if this == &self.number_input1 { self.number_input1_value = self.number_input1_value - 1; this.update(cx, |input, cx| { input.set_value(self.number_input1_value.to_string(), window, cx); }); } else if this == &self.number_input2 { self.number_input2_value = self.number_input2_value.saturating_sub(1); this.update(cx, |input, cx| { input.set_value(self.number_input2_value.to_string(), window, cx); }); } else if this == &self.number_input3 { self.number_input3_value = self.number_input3_value - 1.0; this.update(cx, |input, cx| { input.set_value(self.number_input3_value.to_string(), window, cx); }); } else if this == &self.number_input4 { self.number_input4_value = self.number_input4_value - 1.0; this.update(cx, |input, cx| { input.set_value(self.number_input4_value.to_string(), window, cx); }); } } StepAction::Increment => { if this == &self.number_input1 { self.number_input1_value = self.number_input1_value + 1; this.update(cx, |input, cx| { input.set_value(self.number_input1_value.to_string(), window, cx); }); } else if this == &self.number_input2 { self.number_input2_value = self.number_input2_value + 1; this.update(cx, |input, cx| { input.set_value(self.number_input2_value.to_string(), window, cx); }); } else if this == &self.number_input3 { self.number_input3_value = self.number_input3_value + 1.0; this.update(cx, |input, cx| { input.set_value(self.number_input3_value.to_string(), window, cx); }); } else if this == &self.number_input4 { self.number_input4_value = self.number_input4_value + 1.0; this.update(cx, |input, cx| { input.set_value(self.number_input4_value.to_string(), window, cx); }); } } }, } } } impl Focusable for NumberInputStory { fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { self.number_input1.focus_handle(cx) } } impl Render for NumberInputStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .id("input-story") .size_full() .justify_start() .gap_3() .child( section("Normal Size") .max_w(px(200.)) .child(NumberInput::new(&self.number_input1)), ) .child( section("Disabled") .max_w(px(200.)) .child(NumberInput::new(&self.disabled_input).disabled(true)), ) .child( section("Small Size with suffix").max_w(px(200.)).child( NumberInput::new(&self.number_input2) .small() .suffix(Button::new("info").ghost().icon(IconName::Info).xsmall()), ), ) .child( section("With mask pattern") .max_w(px(200.)) .child(NumberInput::new(&self.number_input3)), ) .child( section("Without appearance").max_w(px(200.)).child( NumberInput::new(&self.number_input4) .appearance(false) .bg(cx.theme().secondary), ), ) } } ================================================ FILE: crates/story/src/stories/otp_input_story.rs ================================================ use gpui::{ prelude::FluentBuilder as _, px, App, AppContext as _, Context, Entity, Focusable, InteractiveElement, IntoElement, ParentElement as _, Render, SharedString, Styled, Subscription, Window, }; use gpui_component::{ checkbox::Checkbox, h_flex, input::{InputEvent, OtpInput, OtpState}, v_flex, Disableable as _, Sizable, StyledExt, }; use crate::section; pub fn init(_: &mut App) {} pub struct OtpInputStory { otp_masked: bool, otp_state: Entity, otp_value: Option, otp_state_small: Entity, otp_state_large: Entity, otp_state_sized: Entity, otp_state_disabled: Entity, _subscriptions: Vec, } impl super::Story for OtpInputStory { fn title() -> &'static str { "OtpInput" } fn description() -> &'static str { "OTP Input uses to one-time password (OTP) input field or number password input field." } fn closable() -> bool { false } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl OtpInputStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let otp_state = cx.new(|cx| OtpState::new(6, window, cx).masked(true)); let _subscriptions = vec![ cx.subscribe(&otp_state, |this, state, ev: &InputEvent, cx| match ev { InputEvent::Change => { let text = state.read(cx).value(); this.otp_value = Some(text.clone()); cx.notify(); } _ => {} }), ]; Self { otp_masked: true, otp_state, otp_value: None, otp_state_small: cx.new(|cx| { OtpState::new(6, window, cx) .default_value("123456") .masked(true) }), otp_state_large: cx.new(|cx| { OtpState::new(6, window, cx) .default_value("012345") .masked(true) }), otp_state_sized: cx.new(|cx| { OtpState::new(4, window, cx) .masked(true) .default_value("654321") }), otp_state_disabled: cx.new(|cx| { OtpState::new(6, window, cx) .masked(true) .default_value("123456") }), _subscriptions, } } fn toggle_opt_masked(&mut self, _: &bool, window: &mut Window, cx: &mut Context) { self.otp_masked = !self.otp_masked; self.otp_state.update(cx, |state, cx| { state.set_masked(self.otp_masked, window, cx) }); self.otp_state_small.update(cx, |state, cx| { state.set_masked(self.otp_masked, window, cx) }); self.otp_state_large.update(cx, |state, cx| { state.set_masked(self.otp_masked, window, cx) }); self.otp_state_sized.update(cx, |state, cx| { state.set_masked(self.otp_masked, window, cx) }); self.otp_state_disabled.update(cx, |state, cx| { state.set_masked(self.otp_masked, window, cx) }); } } impl Focusable for OtpInputStory { fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { self.otp_state.focus_handle(cx) } } impl Render for OtpInputStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .id("otp-input-story") .size_full() .gap_5() .child( h_flex().items_center().child( Checkbox::new("otp-mask") .label("Masked") .checked(self.otp_masked) .on_click(cx.listener(Self::toggle_opt_masked)), ), ) .child( section("Normal") .v_flex() .child(OtpInput::new(&self.otp_state)) .when_some(self.otp_value.clone(), |this, otp| { this.child(format!("Your OTP: {}", otp)) }), ) .child(section("Small").child(OtpInput::new(&self.otp_state_small).groups(1).small())) .child(section("Large").child(OtpInput::new(&self.otp_state_large).groups(3).large())) .child( section("With Size").child( OtpInput::new(&self.otp_state_sized) .groups(1) .with_size(px(55.)), ), ) .child( section("Disabled").child(OtpInput::new(&self.otp_state_disabled).disabled(true)), ) } } ================================================ FILE: crates/story/src/stories/pagination_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Window, }; use gpui_component::{ Disableable, Selectable as _, Sizable, Size, button::{Button, ButtonGroup}, pagination::Pagination, v_flex, }; use crate::section; pub struct PaginationStory { basic_page: usize, many_pages_page: usize, compact_page: usize, focus_handle: FocusHandle, size: Size, } impl super::Story for PaginationStory { fn title() -> &'static str { "Pagination" } fn description() -> &'static str { "Pagination with page navigation, next and previous links." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl PaginationStory { pub fn view(_window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self { basic_page: 5, many_pages_page: 1, compact_page: 3, focus_handle: cx.focus_handle(), size: Size::default(), }) } fn set_size(&mut self, size: Size, _: &mut Window, cx: &mut Context) { self.size = size; cx.notify(); } } impl Focusable for PaginationStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for PaginationStory { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let entity = cx.entity(); v_flex() .gap_6() .child( ButtonGroup::new("toggle-size") .outline() .compact() .child( Button::new("xsmall") .label("XSmall") .selected(self.size == Size::XSmall), ) .child( Button::new("small") .label("Small") .selected(self.size == Size::Small), ) .child( Button::new("medium") .label("Medium") .selected(self.size == Size::Medium), ) .child( Button::new("large") .label("Large") .selected(self.size == Size::Large), ) .on_click(cx.listener(|this, selecteds: &Vec, window, cx| { let size = match selecteds[0] { 0 => Size::XSmall, 1 => Size::Small, 2 => Size::Medium, 3 => Size::Large, _ => Size::Medium, }; this.set_size(size, window, cx); })), ) .child( section("Basic").child( Pagination::new("basic-pagination") .current_page(self.basic_page) .total_pages(10) .with_size(self.size) .on_click({ let entity = entity.clone(); move |page, _, cx| { entity.update(cx, |this, cx| { this.basic_page = *page; cx.notify(); }); } }), ), ) .child( section("Pagination with 10 visible pages").child( Pagination::new("many-pages-pagination") .current_page(self.many_pages_page) .total_pages(50) .visible_pages(10) .with_size(self.size) .on_click({ let entity = entity.clone(); move |page, _, cx| { entity.update(cx, |this, cx| { this.many_pages_page = *page; cx.notify(); }); } }), ), ) .child( section("Compact Style").child( Pagination::new("compact-pagination") .compact() .current_page(self.compact_page) .total_pages(10) .with_size(self.size) .on_click({ let entity = entity.clone(); move |page, _, cx| { entity.update(cx, |this, cx| { this.compact_page = *page; cx.notify(); }); } }), ), ) .child( section("Disabled").child( Pagination::new("disabled-pagination") .current_page(4) .total_pages(10) .with_size(self.size) .disabled(true) .on_click(|_, _, _| {}), ), ) } } ================================================ FILE: crates/story/src/stories/popover_story.rs ================================================ use gpui::{ Action, App, AppContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement as _, Render, Styled as _, Window, actions, div, px, }; use gpui_component::{ ActiveTheme, Anchor, StyledExt, WindowExt, button::{Button, ButtonVariants as _}, divider::Divider, h_flex, input::{Input, InputState}, list::{List, ListDelegate, ListItem, ListState}, popover::Popover, v_flex, }; use serde::Deserialize; use crate::section; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = popover_story, no_json)] struct Info(usize); actions!(popover_story, [Copy, Paste, Cut, SearchAll, ToggleCheck]); const CONTEXT: &str = "popover-story"; pub fn init(cx: &mut App) { cx.bind_keys([ #[cfg(target_os = "macos")] KeyBinding::new("cmd-c", Copy, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-v", Paste, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-x", Cut, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-shift-f", SearchAll, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-shift-f", SearchAll, Some(CONTEXT)), ]) } struct Form { parent: Entity, input1: Entity, } impl Form { fn new(parent: Entity, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self { parent, input1: cx.new(|cx| InputState::new(window, cx)), }) } } impl Focusable for Form { fn focus_handle(&self, cx: &App) -> FocusHandle { self.input1.focus_handle(cx) } } struct DropdownListDelegate { parent: Entity, } impl ListDelegate for DropdownListDelegate { type Item = ListItem; fn items_count(&self, _: usize, _: &App) -> usize { 10 } fn render_item( &mut self, ix: gpui_component::IndexPath, _: &mut Window, _: &mut Context>, ) -> Option { Some(ListItem::new(ix).child(format!("Item {}", ix.row))) } fn set_selected_index( &mut self, _: Option, _: &mut Window, _: &mut Context>, ) { } fn confirm(&mut self, _: bool, _: &mut Window, cx: &mut Context>) { self.parent.update(cx, |this, cx| { this.list_popover_open = false; cx.notify(); }) } fn cancel(&mut self, _: &mut Window, cx: &mut Context>) { self.parent.update(cx, |this, cx| { this.list_popover_open = false; cx.notify(); }) } } impl EventEmitter for Form {} impl Render for Form { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let parent = self.parent.clone(); v_flex() .gap_2() .p_3() .size_full() .child("This is a form container.") .child("Click submit to dismiss the popover.") .child(Input::new(&self.input1)) .child( Button::new("submit") .label("Submit") .primary() .on_click(cx.listener(move |_, _, _, cx| { parent.update(cx, |this, cx| { this.form_popover_open = false; cx.notify(); }) })), ) } } pub struct PopoverStory { focus_handle: FocusHandle, form: Entity
, list: Entity>, form_popover_open: bool, list_popover_open: bool, checked: bool, message: String, } impl super::Story for PopoverStory { fn title() -> &'static str { "Popover" } fn description() -> &'static str { "A popup displays content on top of the main page." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl PopoverStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let form = Form::new(cx.entity(), window, cx); let parent = cx.entity(); let list = cx .new(|cx| ListState::new(DropdownListDelegate { parent }, window, cx).searchable(true)); cx.focus_self(window); Self { form, list, checked: true, form_popover_open: false, list_popover_open: false, focus_handle: cx.focus_handle(), message: "".to_string(), } } fn on_copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { self.message = "You have clicked copy".to_string(); cx.notify() } fn on_cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context) { self.message = "You have clicked cut".to_string(); cx.notify() } fn on_paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context) { self.message = "You have clicked paste".to_string(); cx.notify() } fn on_search_all(&mut self, _: &SearchAll, _: &mut Window, cx: &mut Context) { self.message = "You have clicked search all".to_string(); cx.notify() } fn on_action_info(&mut self, info: &Info, _: &mut Window, cx: &mut Context) { self.message = format!("You have clicked info: {}", info.0); cx.notify() } fn on_action_toggle_check(&mut self, _: &ToggleCheck, _: &mut Window, cx: &mut Context) { self.checked = !self.checked; self.message = format!("You have clicked toggle check: {}", self.checked); cx.notify() } } impl Focusable for PopoverStory { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for PopoverStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let form = self.form.clone(); v_flex() .key_context(CONTEXT) .track_focus(&self.focus_handle) .on_action(cx.listener(Self::on_copy)) .on_action(cx.listener(Self::on_cut)) .on_action(cx.listener(Self::on_paste)) .on_action(cx.listener(Self::on_search_all)) .on_action(cx.listener(Self::on_action_info)) .on_action(cx.listener(Self::on_action_toggle_check)) .size_full() .gap_6() .child( section("Basic Popover").child( Popover::new("popover-0") .max_w(px(600.)) .trigger(Button::new("btn").outline().label("Popover")) .gap_2() .text_sm() .w(px(400.)) .child("Hello, this is a Popover.") .child(Divider::horizontal()) .child( "You can put any content here, including text,\ buttons, forms, and more.", ), ), ) .child( section("Popover with Form").child( Popover::new("popover-form") .p_0() .text_sm() .trigger(Button::new("pop").outline().label("Popup Form")) .track_focus(&form.focus_handle(cx)) .open(self.form_popover_open) .on_open_change(cx.listener(move |this, open, _, cx| { println!("Popover form open changed: {}", open); this.form_popover_open = *open; cx.notify(); })) .child(form.clone()), ), ) .child( section("Popover with List").child( Popover::new("popover-list") .p_0() .text_sm() .open(self.list_popover_open) .on_open_change(cx.listener(move |this, open, _, cx| { this.list_popover_open = *open; cx.notify(); })) .trigger(Button::new("pop").outline().label("Popup List")) .track_focus(&self.list.focus_handle(cx)) .child(List::new(&self.list)) .w_64() .h(px(200.)), ), ) .child( section("Right click to open Popover").child( Popover::new("popover-right-click") .mouse_button(MouseButton::Right) .trigger(Button::new("btn").outline().label("Right Click Popover")) .max_w(px(600.)) .content(|_, _, cx| { v_flex() .gap_2() .child("Hello, this is a Popover on the Bottom Right.") .child(Divider::horizontal()) .child( Button::new("info1") .primary() .label("Dismiss") .w(px(80.)) .on_click(cx.listener(|_, _, window, cx| { window.push_notification( "You have clicked dismiss via DismissEvent.", cx, ); cx.emit(DismissEvent); })), ) }), ), ) .child( section("Styling Popover").child( Popover::new("popover-1") .trigger(Button::new("btn").outline().label("Style Popover")) .appearance(false) .py_1() .px_2() .bg(cx.theme().primary) .text_color(cx.theme().primary_foreground) .max_w(px(600.)) .rounded(cx.theme().radius.half()) .text_sm() .shadow_2xl() .child("A styled Popover with custom background and text color."), ), ) .child( section("Default Open").child( Popover::new("default-open-popover") .default_open(true) .trigger( Button::new("default-open-btn") .label("Default Open") .outline(), ) .child("This popover is open by default when first rendered."), ), ) .child( section("Popover Anchor") .min_h(px(360.)) .v_flex() .child( div().absolute().top_0().left_0().w_full().h_10().child( h_flex() .items_center() .justify_between() .child( Popover::new("anchor-top-left") .max_w(px(600.)) .anchor(Anchor::TopLeft) .trigger(Button::new("btn").outline().label("TopLeft")) .child("This is a Popover on the Top Left."), ) .child( Popover::new("anchor-top-center") .max_w(px(600.)) .anchor(Anchor::TopCenter) .trigger(Button::new("btn").outline().label("TopCenter")) .child("This is a Popover on the Top Center."), ) .child( Popover::new("anchor-top-right") .anchor(Anchor::TopRight) .trigger(Button::new("btn").outline().label("TopRight")) .child("This is a Popover on the Top Right."), ), ), ) .child( div().absolute().bottom_0().left_0().w_full().h_10().child( h_flex() .items_center() .justify_between() .child( Popover::new("anchor-bottom-left") .trigger(Button::new("btn").outline().label("BottomLeft")) .anchor(Anchor::BottomLeft) .child("This is a Popover on the Bottom Left."), ) .child( Popover::new("anchor-bottom-center") .trigger(Button::new("btn").outline().label("BottomCenter")) .anchor(Anchor::BottomCenter) .child("This is a Popover on the Bottom Center."), ) .child( Popover::new("anchor-bottom-right") .anchor(Anchor::BottomRight) .trigger(Button::new("btn").outline().label("BottomRight")) .child("This is a Popover on the Bottom Right."), ), ), ), ) } } ================================================ FILE: crates/story/src/stories/progress_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled, Task, Window, div, px, }; use gpui_component::{ ActiveTheme, IconName, Sizable, button::Button, h_flex, progress::{Progress, ProgressCircle}, v_flex, }; use std::time::Duration; use crate::section; pub struct ProgressStory { focus_handle: gpui::FocusHandle, value: f32, _task: Option>, } impl super::Story for ProgressStory { fn title() -> &'static str { "Progress" } fn description() -> &'static str { "Displays an indicator showing the completion progress of a task, typically displayed as a progress bar." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl ProgressStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), value: 25., _task: None, } } pub fn set_value(&mut self, value: f32) { self.value = value; } fn start_animation(&mut self, cx: &mut Context) { self.value = 0.; self._task = Some(cx.spawn({ let entity = cx.entity(); async move |_, cx| { loop { cx.background_executor() .timer(Duration::from_millis(15)) .await; let mut need_break = false; _ = entity.update(cx, |this, cx| { this.value = (this.value + 2.).min(100.); cx.notify(); if this.value >= 100. { this._task = None; need_break = true; } }); if need_break { break; } } } })); } } impl Focusable for ProgressStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for ProgressStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .w_full() .gap_3() .child( h_flex() .w_full() .gap_3() .justify_between() .child( h_flex() .gap_2() .child(Button::new("button-1").small().label("0%").on_click( cx.listener(|this, _, _, _| { this.set_value(0.); }), )) .child(Button::new("button-2").small().label("25%").on_click( cx.listener(|this, _, _, _| { this.set_value(25.); }), )) .child(Button::new("button-3").small().label("75%").on_click( cx.listener(|this, _, _, _| { this.set_value(75.); }), )) .child(Button::new("button-4").small().label("100%").on_click( cx.listener(|this, _, _, _| { this.set_value(100.); }), )) .child( Button::new("circle-animation-button") .small() .icon(IconName::Play) .on_click(cx.listener(|this, _, _, cx| { this.start_animation(cx); })), ), ) .child( h_flex() .gap_2() .child( Button::new("circle-button-5") .icon(IconName::Minus) .on_click(cx.listener(|this, _, _, _| { this.set_value((this.value - 1.).max(0.)); })), ) .child( Button::new("circle-button-6") .icon(IconName::Plus) .on_click(cx.listener(|this, _, _, _| { this.set_value((this.value + 1.).min(100.)); })), ), ), ) .child( section("Progress Bar") .max_w_md() .child(Progress::new("progress-1").value(self.value)), ) .child( section("Custom Style").max_w_md().child( Progress::new("progress-2") .value(32.) .h(px(16.)) .rounded(px(2.)) .color(cx.theme().green_light) .border_2() .border_color(cx.theme().green), ), ) .child( section("Circle Progress").max_w_md().child( ProgressCircle::new("circle-progress-1") .value(self.value) .size_20() .child( v_flex() .size_full() .items_center() .justify_center() .gap_1() .child( div() .child(format!("{}%", self.value)) .text_color(cx.theme().progress_bar), ) .child(div().child("Loading").text_xs()), ), ), ) .child( section("With size").max_w_md().child( h_flex() .gap_2() .child( ProgressCircle::new("circle-progress-1") .value(self.value) .large(), ) .child(ProgressCircle::new("circle-progress-1").value(self.value)) .child( ProgressCircle::new("circle-progress-1") .value(self.value) .small(), ) .child( ProgressCircle::new("circle-progress-1") .value(self.value) .xsmall(), ), ), ) .child( section("With Label").max_w_md().child( h_flex() .gap_2() .child( ProgressCircle::new("circle-progress-1") .color(cx.theme().primary) .value(self.value) .size_4(), ) .child("Downloading..."), ), ) .child( section("Circle with Color").max_w_md().child( ProgressCircle::new("circle-progress-1") .color(cx.theme().yellow) .value(self.value) .size_12(), ), ) } } ================================================ FILE: crates/story/src/stories/radio_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled, Window, div, px, }; use gpui_component::{ ActiveTheme, Sizable, h_flex, radio::{Radio, RadioGroup}, v_flex, }; use crate::section; pub struct RadioStory { focus_handle: gpui::FocusHandle, radio_check1: bool, radio_check2: bool, radio_group_checked: Option, } impl super::Story for RadioStory { fn title() -> &'static str { "Radio" } fn description() -> &'static str { "A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl RadioStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), radio_check1: false, radio_check2: true, radio_group_checked: Some(1), } } } impl Focusable for RadioStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for RadioStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_6() .child( section("Radio") .max_w_md() .child( Radio::new("radio1") .checked(self.radio_check1) .on_click(cx.listener(|this, checked, _, _| { this.radio_check1 = *checked; })), ) .child( Radio::new("radio2") .label("Radio 2") .checked(self.radio_check2) .on_click(cx.listener(|this, checked, _, _| { this.radio_check2 = *checked; })), ), ) .child( section("Disabled") .child(Radio::new("a").label("Disabled").disabled(true)) .child( Radio::new("b") .label("Disabled with Checked") .checked(true) .disabled(true), ), ) .child( section("Multi-line Label").child( Radio::new("radio3") .label("The long long label text.") .child( div() .text_color(cx.theme().muted_foreground) .child("This line should wrap when the text is too long."), ) .w(px(300.)) .checked(true) .disabled(true), ), ) .child( section("Sizeable").child( h_flex() .h_full() .gap_x_4() .child( Radio::new("xsmall") .label("Small") .xsmall() .checked(self.radio_check2) .on_click(cx.listener(|this, v, _, _| { this.radio_check2 = *v; })), ) .child( Radio::new("large") .label("Large") .large() .checked(self.radio_check2) .on_click(cx.listener(|this, v, _, _| { this.radio_check2 = *v; })), ), ), ) .child( section("Radio Group").max_w_md().child( v_flex().child( RadioGroup::horizontal("radio_group_1") .children(["One", "Two", "Three"]) .selected_index(self.radio_group_checked) .on_click(cx.listener(|this, selected_ix: &usize, _, cx| { this.radio_group_checked = Some(*selected_ix); cx.notify(); })), ), ), ) .child( section("Radio Group Vertical (With container style)") .max_w_md() .child( v_flex().items_center().content_center().child( RadioGroup::vertical("radio_group_2") .w(px(220.)) .p_2() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .disabled(true) .child(Radio::new("one1").label("United States")) .child(Radio::new("one2").label("Canada")) .child(Radio::new("one3").label("Mexico")) .selected_index(Some(1)), ), ), ) } } ================================================ FILE: crates/story/src/stories/rating_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled, Window, }; use gpui_component::{ ActiveTheme, IconName, Selectable as _, Sizable as _, Size, button::{Button, ButtonGroup}, h_flex, rating::Rating, v_flex, }; use crate::section; pub struct RatingStory { focus_handle: gpui::FocusHandle, size: Size, value: usize, } impl super::Story for RatingStory { fn title() -> &'static str { "Rating" } fn description() -> &'static str { "A simple interactive star rating component." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl RatingStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), size: Size::default(), value: 3, } } } impl Focusable for RatingStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } pub fn init(_cx: &mut App) { // No global init required for RatingStory } impl Render for RatingStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .w_full() .gap_3() .child( h_flex().w_full().gap_3().child( ButtonGroup::new("toggle-size") .outline() .compact() .child( Button::new("xsmall") .label("XSmall") .selected(self.size == Size::XSmall), ) .child( Button::new("small") .label("Small") .selected(self.size == Size::Small), ) .child( Button::new("medium") .label("Medium") .selected(self.size == Size::Medium), ) .child( Button::new("large") .label("Large") .selected(self.size == Size::Large), ) .on_click(cx.listener(|this, selecteds: &Vec, _, cx| { let size = match selecteds[0] { 0 => Size::XSmall, 1 => Size::Small, 2 => Size::Medium, 3 => Size::Large, _ => unreachable!(), }; this.size = size; cx.notify(); })), ), ) .child( section("Basic Rating").max_w_md().child( v_flex() .w_full() .gap_3() .justify_center() .items_center() .child( Rating::new("rating-1") .with_size(self.size) .value(self.value) .max(5) .on_click(cx.listener(|this, value: &usize, _, cx| { this.value = *value; cx.notify(); })), ) .child( h_flex() .gap_x_2() .child( Button::new("r-dec") .small() .outline() .icon(IconName::Minus) .on_click(cx.listener(|this, _, _, cx| { let v = this.value.saturating_sub(1); this.value = v; cx.notify(); })), ) .child( Button::new("r-inc") .small() .outline() .icon(IconName::Plus) .on_click(cx.listener(|this, _, _, cx| { let v = (this.value + 1).min(5); this.value = v; cx.notify(); })), ), ), ), ) .child( section("Disabled").max_w_md().child( Rating::new("rating-2") .with_size(self.size) .value(2) .color(cx.theme().green) .max(5) .disabled(true), ), ) .child( section("Custom Color").max_w_md().child( Rating::new("rating-3") .large() .value(self.value) .color(cx.theme().green) .max(5), ), ) } } ================================================ FILE: crates/story/src/stories/resizable_story.rs ================================================ use gpui::{ AnyElement, App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement as _, Pixels, Render, SharedString, Styled, Window, div, px, }; use gpui_component::{ ActiveTheme, resizable::{h_resizable, resizable_panel, v_resizable}, v_flex, }; pub struct ResizableStory { focus_handle: FocusHandle, } impl super::Story for ResizableStory { fn title() -> &'static str { "Resizable" } fn description() -> &'static str { "The resizable panels." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl Focusable for ResizableStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl ResizableStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut App) -> Self { Self { focus_handle: cx.focus_handle(), } } } fn panel_box(content: impl Into, _: &App) -> AnyElement { div() .p_4() .size_full() .child(content.into()) .into_any_element() } impl Render for ResizableStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .gap_6() .child( div() .h(px(600.)) .border_1() .border_color(cx.theme().border) .child( v_resizable("resizable-1") .on_resize(|state, _, cx| { println!("Resized: {:?}", state.read(cx).sizes()); }) .child( h_resizable("resizable-1.1") .size(px(150.)) .child( resizable_panel() .size(px(150.)) .size_range(px(120.)..px(300.)) .child(panel_box("Left (120px .. 300px)", cx)), ) .child(panel_box("Center", cx)) .child( resizable_panel() .size(px(300.)) .child(panel_box("Right", cx)), ), ) .child(panel_box("Center", cx)) .child( resizable_panel() .size(px(80.)) .size_range(px(80.)..Pixels::MAX) .child(panel_box("Bottom (80px .. 150px)", cx)), ), ), ) .child( div() .h(px(400.)) .border_1() .border_color(cx.theme().border) .child( h_resizable("resizable-3") .child( resizable_panel() .size(px(200.)) .size_range(px(200.)..px(400.)) .child(panel_box("Left 2", cx)), ) .child(panel_box("Right (Grow)", cx)), ), ) } } ================================================ FILE: crates/story/src/stories/scrollbar_story.rs ================================================ use std::rc::Rc; use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Size, Styled, UniformListScrollHandle, Window, div, px, size, uniform_list, }; use gpui_component::{ ActiveTheme as _, Selectable, button::{Button, ButtonGroup}, h_flex, scroll::ScrollableElement, v_flex, }; pub struct ScrollbarStory { focus_handle: FocusHandle, items: Rc>, item_sizes: Rc>>, test_width: Pixels, size_mode: usize, scroll_handle: UniformListScrollHandle, } const ITEM_HEIGHT: Pixels = px(50.); impl ScrollbarStory { fn new(_: &mut Window, cx: &mut Context) -> Self { let items: Rc> = Rc::new((0..5000).map(|i| format!("Item {}", i)).collect()); let test_width = px(3000.); let item_sizes = items .iter() .map(|_| size(test_width, ITEM_HEIGHT)) .collect::>(); Self { focus_handle: cx.focus_handle(), items, item_sizes: Rc::new(item_sizes), test_width, size_mode: 0, scroll_handle: UniformListScrollHandle::new(), } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } pub fn change_test_cases(&mut self, n: usize, cx: &mut Context) { self.size_mode = n; if n == 0 { self.items = Rc::new((0..5000).map(|i| format!("Item {}", i)).collect()); self.test_width = px(3000.); } else if n == 1 { self.items = Rc::new((0..100).map(|i| format!("Item {}", i)).collect()); self.test_width = px(10000.); } else if n == 2 { self.items = Rc::new((0..500000).map(|i| format!("Item {}", i)).collect()); self.test_width = px(10000.); } else { self.items = Rc::new((0..5).map(|i| format!("Item {}", i)).collect()); self.test_width = px(10000.); } self.item_sizes = self .items .iter() .map(|_| size(self.test_width, ITEM_HEIGHT)) .collect::>() .into(); cx.notify(); } fn render_buttons(&mut self, cx: &mut Context) -> impl IntoElement { h_flex().gap_2().justify_between().child( h_flex().gap_2().child( ButtonGroup::new("test-cases") .outline() .compact() .child( Button::new("test-0") .label("Size 0") .selected(self.size_mode == 0), ) .child( Button::new("test-1") .label("Size 1") .selected(self.size_mode == 1), ) .child( Button::new("test-2") .label("Size 2") .selected(self.size_mode == 2), ) .child( Button::new("test-3") .label("Size 3") .selected(self.size_mode == 3), ) .on_click(cx.listener(|view, clicks: &Vec, _, cx| { if clicks.contains(&0) { view.change_test_cases(0, cx) } else if clicks.contains(&1) { view.change_test_cases(1, cx) } else if clicks.contains(&2) { view.change_test_cases(2, cx) } else if clicks.contains(&3) { view.change_test_cases(3, cx) } })), ), ) } } impl super::Story for ScrollbarStory { fn title() -> &'static str { "Scrollbar" } fn description() -> &'static str { "Add scrollbar to a scrollable element." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl Focusable for ScrollbarStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for ScrollbarStory { fn render( &mut self, _: &mut gpui::Window, cx: &mut gpui::Context, ) -> impl gpui::IntoElement { v_flex() .size_full() .gap_4() .child(self.render_buttons(cx)) .child({ div() .relative() .border_1() .border_color(cx.theme().border) .flex_1() .child( uniform_list("list", self.items.len(), { let items = self.items.clone(); move |visible_range, _, cx| { let mut elements = Vec::with_capacity(visible_range.len()); for ix in visible_range { let item = &items[ix]; elements.push( div() .h(ITEM_HEIGHT) .pt_1() .items_center() .justify_center() .text_sm() .child( div() .p_2() .bg(cx.theme().secondary) .child(item.to_string()), ), ); } elements } }) .py_1() .px_3() .size_full() .track_scroll(&self.scroll_handle), ) .vertical_scrollbar(&self.scroll_handle) }) } } ================================================ FILE: crates/story/src/stories/select_story.rs ================================================ use gpui::*; use gpui_component::{button::*, checkbox::*, divider::*, input::*, select::*, *}; use itertools::Itertools as _; use serde::{Deserialize, Serialize}; use crate::section; pub fn init(_: &mut App) {} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] struct Country { name: SharedString, code: SharedString, } impl Country { pub fn letter_prefix(&self) -> char { self.name.chars().next().unwrap_or(' ') } } impl SelectItem for Country { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn display_title(&self) -> Option { Some(format!("{} ({})", self.name, self.code).into_any_element()) } fn value(&self) -> &Self::Value { &self.code } } pub struct SelectStory { disabled: bool, country_select: Entity>>>, fruit_select: Entity>>, simple_select1: Entity>>, simple_select2: Entity>>, simple_select3: Entity>>, disabled_select: Entity>>, appearance_select: Entity>>, input_state: Entity, } impl super::Story for SelectStory { fn title() -> &'static str { "Select" } fn description() -> &'static str { "Displays a list of options for the user to pick from—triggered by a button." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl Focusable for SelectStory { fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { self.fruit_select.focus_handle(cx) } } impl SelectStory { fn new(window: &mut Window, cx: &mut App) -> Entity { let countries = serde_json::from_str::>(include_str!("../fixtures/countries.json")) .unwrap(); let mut grouped_countries: SearchableVec> = SearchableVec::new(vec![]); for (prefix, items) in countries.iter().chunk_by(|c| c.letter_prefix()).into_iter() { let items = items.cloned().collect::>(); grouped_countries.push(SelectGroup::new(prefix.to_string()).items(items)); } let country_select = cx.new(|cx| { SelectState::new( grouped_countries, Some(IndexPath::default().row(8).section(2)), window, cx, ) .searchable(true) }); let appearance_select = cx.new(|cx| { SelectState::new( vec![ "CN".into(), "US".into(), "HK".into(), "JP".into(), "KR".into(), ], Some(IndexPath::default()), window, cx, ) }); let input_state = cx.new(|cx| InputState::new(window, cx).placeholder("Your phone number")); let fruits = SearchableVec::new(vec![ "Apple", "Orange", "Banana", "Grape", "Pineapple", "Watermelon & This is a long long long long long long long long long title", "Avocado", ]); let fruit_select = cx.new(|cx| SelectState::new(fruits, None, window, cx).searchable(true)); cx.new(|cx| { cx.subscribe_in(&country_select, window, Self::on_select_event) .detach(); Self { disabled: false, country_select, fruit_select, simple_select1: cx.new(|cx| { SelectState::new( vec![ "GPUI", "Iced", "egui", "Makepad", "Slint", "QT", "ImGui", "Cocoa", "WinUI", ], Some(IndexPath::default()), window, cx, ) }), simple_select2: cx.new(|cx| { let mut select = SelectState::new(SearchableVec::new(vec![]), None, window, cx) .searchable(true); select.set_items( SearchableVec::new(vec!["Rust", "Go", "C++", "JavaScript"]), window, cx, ); select }), simple_select3: cx .new(|cx| SelectState::new(Vec::::new(), None, window, cx)), disabled_select: cx .new(|cx| SelectState::new(Vec::::new(), None, window, cx)), appearance_select, input_state, } }) } pub fn view(window: &mut Window, cx: &mut App) -> Entity { Self::new(window, cx) } fn on_select_event( &mut self, _: &Entity>>>, event: &SelectEvent>>, _window: &mut Window, _cx: &mut Context, ) { match event { SelectEvent::Confirm(value) => println!("Selected country: {:?}", value), } } fn toggle_disabled(&mut self, disabled: bool, _: &mut Window, cx: &mut Context) { self.disabled = disabled; cx.notify(); } } impl Render for SelectStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .gap_4() .child( Checkbox::new("disable-selects") .label("Disabled") .checked(self.disabled) .on_click(cx.listener(|this, checked, window, cx| { this.toggle_disabled(*checked, window, cx); })), ) .child( section("Select").max_w_128().child( Select::new(&self.country_select) .search_placeholder("Search country by name or code") .cleanable(true) .disabled(self.disabled), ), ) .child( section("Searchable").max_w_128().child( Select::new(&self.fruit_select) .disabled(self.disabled) .icon(IconName::Search) .w(px(320.)) .menu_width(px(400.)), ), ) .child( section("Disabled") .max_w_128() .child(Select::new(&self.disabled_select).disabled(true)), ) .child( section("With preview label").max_w_128().child( Select::new(&self.simple_select1) .disabled(self.disabled) .small() .placeholder("UI") .title_prefix("UI: "), ), ) .child( section("Searchable Select").max_w_128().child( Select::new(&self.simple_select2) .disabled(self.disabled) .small() .placeholder("Language") .title_prefix("Language: "), ), ) .child( section("Empty Items").max_w_128().child( Select::new(&self.simple_select3) .disabled(self.disabled) .small() .empty( h_flex() .h_24() .justify_center() .text_color(cx.theme().muted_foreground) .child("No Data"), ), ), ) .child( section("Appearance false with Input").max_w_128().child( h_flex() .border_1() .border_color(cx.theme().input) .rounded(cx.theme().radius_lg) .text_color(cx.theme().secondary_foreground) .w_full() .gap_1() .child( div().w(px(140.)).child( Select::new(&self.appearance_select) .appearance(false) .py_2() .pl_3(), ), ) .child(Divider::vertical()) .child( div().flex_1().child( Input::new(&self.input_state) .appearance(false) .pr_3() .py_2(), ), ) .child( div() .p_2() .child(Button::new("send").small().ghost().label("Send")), ), ), ) .child( section("Selected Values").max_w_lg().child( v_flex() .gap_3() .child(format!( "Country: {:?}", self.country_select.read(cx).selected_value() )) .child(format!( "fruit: {:?}", self.fruit_select.read(cx).selected_value() )) .child(format!( "UI: {:?}", self.simple_select1.read(cx).selected_value() )) .child(format!( "Language: {:?}", self.simple_select2.read(cx).selected_value() )) .child("This is other text."), ), ) } } ================================================ FILE: crates/story/src/stories/settings_story.rs ================================================ use gpui::{ App, AppContext, Axis, Context, Element, Entity, FocusHandle, Focusable, Global, IntoElement, ParentElement as _, Render, SharedString, Styled, Window, px, }; use gpui_component::{ ActiveTheme, Icon, IconName, Sizable, Size, Theme, ThemeMode, button::Button, group_box::GroupBoxVariant, h_flex, label::Label, setting::{ NumberFieldOptions, RenderOptions, SettingField, SettingFieldElement, SettingGroup, SettingItem, SettingPage, Settings, }, text::markdown, v_flex, }; struct AppSettings { auto_switch_theme: bool, cli_path: SharedString, font_family: SharedString, font_size: f64, line_height: f64, notifications_enabled: bool, auto_update: bool, resettable: bool, } impl Default for AppSettings { fn default() -> Self { Self { auto_switch_theme: false, cli_path: "/usr/local/bin/bash".into(), font_family: "Arial".into(), font_size: 14.0, line_height: 12.0, notifications_enabled: true, auto_update: true, resettable: true, } } } impl Global for AppSettings {} impl AppSettings { fn global(cx: &App) -> &AppSettings { cx.global::() } pub fn global_mut(cx: &mut App) -> &mut AppSettings { cx.global_mut::() } } pub struct SettingsStory { focus_handle: FocusHandle, group_variant: GroupBoxVariant, size: Size, } struct OpenURLSettingField { label: SharedString, url: SharedString, } impl OpenURLSettingField { fn new(label: impl Into, url: impl Into) -> Self { Self { label: label.into(), url: url.into(), } } } impl SettingFieldElement for OpenURLSettingField { type Element = Button; fn render_field(&self, options: &RenderOptions, _: &mut Window, _: &mut App) -> Self::Element { let url = self.url.clone(); Button::new("open-url") .outline() .label(self.label.clone()) .with_size(options.size) .on_click(move |_, _window, cx| { cx.open_url(url.as_str()); }) } } impl super::Story for SettingsStory { fn title() -> &'static str { "Settings" } fn description() -> &'static str { "A collection of settings groups and items for the application." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn paddings() -> gpui::Pixels { px(0.) } } impl SettingsStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { cx.set_global::(AppSettings::default()); Self { focus_handle: cx.focus_handle(), group_variant: GroupBoxVariant::Outline, size: Size::default(), } } fn setting_pages(&self, _: &mut Window, cx: &mut Context) -> Vec { let view = cx.entity(); let default_settings = AppSettings::default(); let resettable = AppSettings::global(cx).resettable; vec![ SettingPage::new("General") .resettable(resettable) .default_open(true) .icon(Icon::new(IconName::Settings2)) .groups(vec![ SettingGroup::new().title("Appearance").items(vec![ SettingItem::new( "Dark Mode", SettingField::switch( |cx: &App| cx.theme().mode.is_dark(), |val: bool, cx: &mut App| { let mode = if val { ThemeMode::Dark } else { ThemeMode::Light }; Theme::global_mut(cx).mode = mode; Theme::change(mode, None, cx); }, ) .default_value(false), ) .description("Switch between light and dark themes."), SettingItem::new( "Auto Switch Theme", SettingField::checkbox( |cx: &App| AppSettings::global(cx).auto_switch_theme, |val: bool, cx: &mut App| { AppSettings::global_mut(cx).auto_switch_theme = val; }, ) .default_value(default_settings.auto_switch_theme), ) .description("Automatically switch theme based on system settings."), SettingItem::new( "resettable", SettingField::switch( |cx: &App| AppSettings::global(cx).resettable, |checked: bool, cx: &mut App| { AppSettings::global_mut(cx).resettable = checked }, ), ) .description("Enable/Disable reset button for settings."), SettingItem::new( "Group Variant", SettingField::dropdown( vec![ (GroupBoxVariant::Normal.as_str().into(), "Normal".into()), (GroupBoxVariant::Outline.as_str().into(), "Outline".into()), (GroupBoxVariant::Fill.as_str().into(), "Fill".into()), ], { let view = view.clone(); move |cx: &App| { SharedString::from( view.read(cx).group_variant.as_str().to_string(), ) } }, { let view = view.clone(); move |val: SharedString, cx: &mut App| { view.update(cx, |view, cx| { view.group_variant = GroupBoxVariant::from_str(val.as_str()); cx.notify(); }); } }, ) .default_value(GroupBoxVariant::Outline.as_str().to_string()), ) .description("Select the variant for setting groups."), SettingItem::new( "Group Size", SettingField::dropdown( vec![ (Size::Medium.as_str().into(), "Medium".into()), (Size::Small.as_str().into(), "Small".into()), (Size::XSmall.as_str().into(), "XSmall".into()), ], { let view = view.clone(); move |cx: &App| { SharedString::from(view.read(cx).size.as_str().to_string()) } }, { let view = view.clone(); move |val: SharedString, cx: &mut App| { view.update(cx, |view, cx| { view.size = Size::from_str(val.as_str()); cx.notify(); }); } }, ) .default_value(Size::default().as_str().to_string()), ) .description("Select the size for the setting group."), ]), SettingGroup::new() .title("Font") .item( SettingItem::new( "Font Family", SettingField::dropdown( vec![ ("Arial".into(), "Arial".into()), ("Helvetica".into(), "Helvetica".into()), ("Times New Roman".into(), "Times New Roman".into()), ("Courier New".into(), "Courier New".into()), ], |cx: &App| AppSettings::global(cx).font_family.clone(), |val: SharedString, cx: &mut App| { AppSettings::global_mut(cx).font_family = val; }, ) .default_value(default_settings.font_family), ) .description("Select the font family for the story."), ) .item( SettingItem::new( "Font Size", SettingField::number_input( NumberFieldOptions { min: 8.0, max: 72.0, ..Default::default() }, |cx: &App| AppSettings::global(cx).font_size, |val: f64, cx: &mut App| { AppSettings::global_mut(cx).font_size = val; }, ) .default_value(default_settings.font_size), ) .description( "Adjust the font size for better readability between 8 and 72.", ), ) .item( SettingItem::new( "Line Height", SettingField::number_input( NumberFieldOptions { min: 8.0, max: 32.0, ..Default::default() }, |cx: &App| AppSettings::global(cx).line_height, |val: f64, cx: &mut App| { AppSettings::global_mut(cx).line_height = val; }, ) .default_value(default_settings.line_height), ) .description( "Adjust the line height for better readability between 8 and 32.", ), ), SettingGroup::new().title("Other").items(vec![ SettingItem::render(|options, _, _| { h_flex() .w_full() .justify_between() .flex_wrap() .gap_3() .child("This is a custom element item by use SettingItem::element.") .child( Button::new("action") .icon(IconName::Globe) .label("Repository...") .outline() .with_size(options.size) .on_click(|_, _, cx| { cx.open_url( "https://github.com/longbridge/gpui-component", ); }), ) .into_any_element() }), SettingItem::new( "CLI Path", SettingField::input( |cx: &App| AppSettings::global(cx).cli_path.clone(), |val: SharedString, cx: &mut App| { println!("cli-path set value: {}", val); AppSettings::global_mut(cx).cli_path = val; }, ) .default_value(default_settings.cli_path), ) .layout(Axis::Vertical) .description( "Path to the CLI executable. \n\ This item uses Vertical layout. The title,\ description, and field are all aligned vertically with width 100%.", ), ]), ]), SettingPage::new("Software Update") .resettable(resettable) .icon(Icon::new(IconName::Cpu)) .groups(vec![SettingGroup::new().title("Updates").items(vec![ SettingItem::new( "Enable Notifications", SettingField::switch( |cx: &App| AppSettings::global(cx).notifications_enabled, |val: bool, cx: &mut App| { AppSettings::global_mut(cx).notifications_enabled = val; }, ) .default_value(default_settings.notifications_enabled), ) .description("Receive notifications about updates and news."), SettingItem::new( "Auto Update", SettingField::switch( |cx: &App| AppSettings::global(cx).auto_update, |val: bool, cx: &mut App| { AppSettings::global_mut(cx).auto_update = val; }, ) .default_value(default_settings.auto_update), ) .description("Automatically download and install updates."), ])]), SettingPage::new("About") .resettable(resettable) .icon(Icon::new(IconName::Info)) .group( SettingGroup::new().item(SettingItem::render(|_options, _, cx| { v_flex() .gap_3() .w_full() .items_center() .justify_center() .child(Icon::new(IconName::GalleryVerticalEnd).size_16()) .child("GPUI Component") .child( Label::new( "Rust GUI components for building fantastic cross-platform \ desktop application by using GPUI.", ) .text_sm() .text_color(cx.theme().muted_foreground), ) .into_any() })), ) .group(SettingGroup::new().title("Links").items(vec![ SettingItem::new( "GitHub Repository", SettingField::element(OpenURLSettingField::new( "Repository...", "https://github.com/longbridge/gpui-component", )), ) .description("Open the GitHub repository in your default browser."), SettingItem::new( "Documentation", SettingField::element(OpenURLSettingField::new( "Rust Docs...", "https://docs.rs/gpui-component" )), ) .description(markdown( "Rust doc for the `gpui-component` crate.", )), SettingItem::new( "Website", SettingField::render(|options, _window, _cx| { Button::new("open-url") .outline() .label("Website...") .with_size(options.size) .on_click(|_, _window, cx| { cx.open_url("https://longbridge.github.io/gpui-component/"); }) }), ) .description("Official website and documentation for the GPUI Component."), ])), ] } } impl Focusable for SettingsStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for SettingsStory { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { Settings::new("app-settings") .with_size(self.size) .with_group_variant(self.group_variant) .pages(self.setting_pages(window, cx)) } } ================================================ FILE: crates/story/src/stories/sheet_story.rs ================================================ use std::{sync::Arc, time::Duration}; use fake::Fake; use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, InteractiveElement as _, IntoElement, ParentElement, Render, SharedString, Styled, Task, WeakEntity, Window, div, prelude::FluentBuilder as _, px, }; use gpui_component::{ ActiveTheme as _, Icon, IconName, IndexPath, Placement, WindowExt, button::{Button, ButtonVariant, ButtonVariants as _}, checkbox::Checkbox, date_picker::{DatePicker, DatePickerState}, h_flex, input::{Input, InputState}, list::{List, ListDelegate, ListItem, ListState}, v_flex, }; use crate::TestAction; use crate::{Story, section}; pub struct ListItemDeletegate { story: WeakEntity, confirmed_index: Option, selected_index: Option, items: Vec>, matches: Vec>, } impl ListDelegate for ListItemDeletegate { type Item = ListItem; fn items_count(&self, _: usize, _: &App) -> usize { self.matches.len() } fn perform_search( &mut self, query: &str, _: &mut Window, cx: &mut Context>, ) -> Task<()> { let query = query.to_string(); cx.spawn(async move |this, cx| { // Simulate a slow search. let sleep = (0.05..0.1).fake(); cx.background_executor().timer(Duration::from_secs_f64(sleep)).await; this.update(cx, |this, cx| { this.delegate_mut().matches = this .delegate() .items .iter() .filter(|item| item.to_lowercase().contains(&query.to_lowercase())) .cloned() .collect(); cx.notify(); }) .ok(); }) } fn render_item( &mut self, ix: IndexPath, _: &mut Window, _: &mut Context>, ) -> Option { let confirmed = Some(ix.row) == self.confirmed_index; if let Some(item) = self.matches.get(ix.row) { let list_item = ListItem::new(("item", ix.row)) .check_icon(IconName::Check) .confirmed(confirmed) .child(h_flex().items_center().justify_between().child(item.to_string())) .suffix(|_, _| { Button::new("like") .tab_stop(false) .icon(IconName::Heart) .with_variant(ButtonVariant::Ghost) .size(px(18.)) .on_click(move |_, window, cx| { cx.stop_propagation(); window.prevent_default(); println!("You have clicked like."); }) }); Some(list_item) } else { None } } fn render_empty( &mut self, _: &mut Window, cx: &mut Context>, ) -> impl IntoElement { v_flex() .size_full() .child(Icon::new(IconName::Inbox).size(px(50.)).text_color(cx.theme().muted_foreground)) .child("No matches found") .items_center() .justify_center() .p_3() .bg(cx.theme().muted) .text_color(cx.theme().muted_foreground) } fn cancel(&mut self, window: &mut Window, cx: &mut Context>) { _ = self.story.update(cx, |this, cx| { this.close_sheet(window, cx); }); } fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut Context>) { _ = self.story.update(cx, |this, _| { self.confirmed_index = self.selected_index; if let Some(ix) = self.confirmed_index { if let Some(item) = self.matches.get(ix) { this.selected_value = Some(SharedString::from(item.to_string())); } } }); } fn set_selected_index( &mut self, ix: Option, _: &mut Window, cx: &mut Context>, ) { self.selected_index = ix.map(|ix| ix.row); if let Some(_) = ix { cx.notify(); } } } pub struct SheetStory { focus_handle: FocusHandle, placement: Option, selected_value: Option, list: Entity>, input1: Entity, input2: Entity, date: Entity, overlay: bool, overlay_closable: bool, } impl Story for SheetStory { fn title() -> &'static str { "Sheet" } fn description() -> &'static str { "Sheet for open a popup in the edge of the window" } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl SheetStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let items: Vec> = [ "Baguette (France)", "Baklava (Turkey)", "Beef Wellington (UK)", "Biryani (India)", "Borscht (Ukraine)", "Bratwurst (Germany)", "Bulgogi (Korea)", "Burrito (USA)", "Ceviche (Peru)", "Chicken Tikka Masala (India)", "Churrasco (Brazil)", "Couscous (North Africa)", "Croissant (France)", "Dim Sum (China)", "Empanada (Argentina)", "Fajitas (Mexico)", "Falafel (Middle East)", "Feijoada (Brazil)", "Fish and Chips (UK)", "Fondue (Switzerland)", "Goulash (Hungary)", "Haggis (Scotland)", "Kebab (Middle East)", "Kimchi (Korea)", "Lasagna (Italy)", "Maple Syrup Pancakes (Canada)", "Moussaka (Greece)", "Pad Thai (Thailand)", "Paella (Spain)", "Pancakes (USA)", "Pasta Carbonara (Italy)", "Pavlova (Australia)", "Peking Duck (China)", "Pho (Vietnam)", "Pierogi (Poland)", "Pizza (Italy)", "Poutine (Canada)", "Pretzel (Germany)", "Ramen (Japan)", "Rendang (Indonesia)", "Sashimi (Japan)", "Satay (Indonesia)", "Shepherd's Pie (Ireland)", "Sushi (Japan)", "Tacos (Mexico)", "Tandoori Chicken (India)", "Tortilla (Spain)", "Tzatziki (Greece)", "Wiener Schnitzel (Austria)", ] .iter() .map(|s| Arc::new(s.to_string())) .collect(); let story = cx.entity().downgrade(); let delegate = ListItemDeletegate { story, selected_index: None, confirmed_index: None, items: items.clone(), matches: items.clone(), }; let list = cx.new(|cx| ListState::new(delegate, window, cx).searchable(true)); let input1 = cx.new(|cx| InputState::new(window, cx).placeholder("Your Name")); let input2 = cx.new(|cx| { InputState::new(window, cx).placeholder("For test focus back on dialog close.") }); let date = cx.new(|cx| DatePickerState::new(window, cx)); Self { focus_handle: cx.focus_handle(), placement: None, selected_value: None, list, input1, input2, date, overlay: true, overlay_closable: true, } } fn open_sheet_at(&mut self, placement: Placement, window: &mut Window, cx: &mut Context) { let list = self.list.clone(); let drawer_h = match placement { Placement::Left | Placement::Right => px(400.), Placement::Top | Placement::Bottom => px(540.), }; let overlay = self.overlay; let overlay_closable = self.overlay_closable; let input1 = self.input1.clone(); let date = self.date.clone(); window.open_sheet_at(placement, cx, move |this, _, cx| { this.overlay(overlay) .overlay_closable(overlay_closable) .size(drawer_h) .title("Sheet Title") .child( v_flex() .size_full() .gap_3() .child(Input::new(&input1)) .child(DatePicker::new(&date).placeholder("Date of Birth")) .child( Button::new("send-notification").child("Test Notification").on_click( |_, window, cx| { window .push_notification("Hello this is message from Sheet.", cx) }, ), ) .child( Button::new("confirm-dialog-from-sheet") .child("Open Confirm Dialog") .on_click(|_, window, cx| { window.open_alert_dialog(cx, move |dialog, _, _| { dialog .child("Confirm dialog opened from sheet.") .on_ok(|_, window, cx| { window .push_notification("You have pressed ok.", cx); true }) .on_cancel(|_, window, cx| { window.push_notification( "You have pressed cancel.", cx, ); true }) }); }), ) .child( List::new(&list) .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius), ), ) .footer( h_flex() .gap_6() .items_center() .child(Button::new("confirm").primary().label("Confirm").on_click( |_, window, cx| { window.close_sheet(cx); }, )) .child(Button::new("cancel").label("Cancel").on_click(|_, window, cx| { window.close_sheet(cx); })), ) }); } fn close_sheet(&mut self, _: &mut Window, cx: &mut Context) { self.placement = None; cx.notify(); } fn on_action_test_action( &mut self, _: &TestAction, window: &mut Window, cx: &mut Context, ) { window.push_notification("You have clicked the TestAction.", cx); } } impl Focusable for SheetStory { fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { self.focus_handle.clone() } } impl Render for SheetStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .id("sheet-story") .track_focus(&self.focus_handle) .on_action(cx.listener(Self::on_action_test_action)) .size_full() .child( v_flex() .gap_6() .child( h_flex() .id("state") .items_center() .gap_3() .child( Checkbox::new("overlay") .label("Overlay") .checked(self.overlay) .on_click(cx.listener(|view, _, _, cx| { view.overlay = !view.overlay; cx.notify(); })), ) .child( Checkbox::new("closable") .label("Overlay Closable") .checked(self.overlay_closable) .on_click(cx.listener(|view, _, _, cx| { view.overlay_closable = !view.overlay_closable; cx.notify(); })), ), ) .child( section("Normal Sheet") .child( Button::new("show-sheet-left") .outline() .label("Left Sheet...") .on_click(cx.listener(|this, _, window, cx| { this.open_sheet_at(Placement::Left, window, cx) })), ) .child( Button::new("show-sheet-top") .outline() .label("Top Sheet...") .on_click(cx.listener(|this, _, window, cx| { this.open_sheet_at(Placement::Top, window, cx) })), ) .child( Button::new("show-sheet-right") .outline() .label("Right Sheet...") .on_click(cx.listener(|this, _, window, cx| { this.open_sheet_at(Placement::Right, window, cx) })), ) .child( Button::new("show-sheet-bottom") .outline() .label("Bottom Sheet...") .on_click(cx.listener(|this, _, window, cx| { this.open_sheet_at(Placement::Bottom, window, cx) })), ), ) .child( section("Scrollable Sheet").max_w_md().child( Button::new("show-scrollable-sheet") .outline() .label("Scrollable Sheet...") .on_click(cx.listener(|_, _, window, cx| { window.open_sheet_at( Placement::Right, cx, move |this, _, _| { this.title("Scrollable Sheet") .child("This is a scrollable sheet.\n".repeat(150)) }, ); })), ), ) .child( section("Focus back test") .max_w_md() .child(Input::new(&self.input2)) .child( Button::new("test-action") .outline() .label("Test Action") .flex_shrink_0() .on_click(|_, window, cx| { window.dispatch_action(Box::new(TestAction), cx); }) .tooltip( "This button for test dispatch action, \ to make sure when Dialog close,\ \nthis still can handle the action.", ), ), ) .when_some(self.selected_value.clone(), |this, selected_value| { this.child( h_flex().gap_1().child("You have selected:").child( div().child(selected_value.to_string()).text_color(gpui::red()), ), ) }), ) } } ================================================ FILE: crates/story/src/stories/sidebar_story.rs ================================================ use std::collections::HashMap; use gpui::{ Action, App, AppContext, ClickEvent, Context, Entity, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window, div, prelude::FluentBuilder, px, relative, }; use gpui_component::{ ActiveTheme, Icon, IconName, Side, Sizable, badge::Badge, breadcrumb::{Breadcrumb, BreadcrumbItem}, divider::Divider, h_flex, menu::DropdownMenu, sidebar::{ Sidebar, SidebarFooter, SidebarGroup, SidebarHeader, SidebarMenu, SidebarMenuItem, SidebarToggleButton, }, switch::Switch, v_flex, }; use serde::Deserialize; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = sidebar_story, no_json)] pub struct SelectCompany(SharedString); pub struct SidebarStory { active_items: HashMap, last_active_item: Item, active_subitem: Option, collapsed: bool, side: Side, click_to_open_submenu: bool, focus_handle: gpui::FocusHandle, checked: bool, } impl SidebarStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { let mut active_items = HashMap::new(); active_items.insert(Item::Playground, true); Self { active_items, last_active_item: Item::Playground, active_subitem: None, collapsed: false, side: Side::Left, focus_handle: cx.focus_handle(), checked: false, click_to_open_submenu: false, } } fn render_content(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex().gap_3().child( h_flex() .gap_3() .child( Switch::new("side") .label("Placement Right") .checked(self.side.is_right()) .on_click(cx.listener(|this, checked: &bool, _, cx| { this.side = if *checked { Side::Right } else { Side::Left }; cx.notify(); })), ) .child( Switch::new("click-to-open") .checked(self.click_to_open_submenu) .label("Click to open submenu") .on_click(cx.listener(|this, checked: &bool, _, cx| { this.click_to_open_submenu = *checked; cx.notify(); })), ), ) } fn switch_checked_handler( &mut self, checked: &bool, _: &mut Window, _: &mut Context, ) { self.checked = *checked; } } #[derive(Clone, Copy, PartialEq, Eq, Hash)] enum Item { Playground, Models, Documentation, Settings, DesignEngineering, SalesAndMarketing, Travel, } #[derive(Clone, Copy, PartialEq, Eq)] enum SubItem { History, Starred, General, Team, Billing, Limits, Settings, Genesis, Explorer, Quantum, Introduction, GetStarted, Tutorial, Changelog, } impl Item { pub fn label(&self) -> &'static str { match self { Self::Playground => "Playground", Self::Models => "Models", Self::Documentation => "Documentation", Self::Settings => "Settings", Self::DesignEngineering => "Design Engineering", Self::SalesAndMarketing => "Sales and Marketing", Self::Travel => "Travel", } } pub fn is_disabled(&self) -> bool { match self { Self::Travel => true, _ => false, } } pub fn icon(&self) -> IconName { match self { Self::Playground => IconName::SquareTerminal, Self::Models => IconName::Bot, Self::Documentation => IconName::BookOpen, Self::Settings => IconName::Settings2, Self::DesignEngineering => IconName::Frame, Self::SalesAndMarketing => IconName::ChartPie, Self::Travel => IconName::Map, } } pub fn handler( &self, ) -> impl Fn(&mut SidebarStory, &ClickEvent, &mut Window, &mut Context) + 'static { let item = *self; move |this, _, _, cx| { if this.active_items.contains_key(&item) { this.active_items.remove(&item); } else { this.active_items.insert(item, true); } this.last_active_item = item; this.active_subitem = None; cx.notify(); } } pub fn items(&self) -> Vec { match self { Self::Playground => vec![SubItem::History, SubItem::Starred, SubItem::Settings], Self::Models => vec![SubItem::Genesis, SubItem::Explorer, SubItem::Quantum], Self::Documentation => vec![ SubItem::Introduction, SubItem::GetStarted, SubItem::Tutorial, SubItem::Changelog, ], Self::Settings => vec![ SubItem::General, SubItem::Team, SubItem::Billing, SubItem::Limits, ], _ => Vec::new(), } } } impl SubItem { pub fn label(&self) -> &'static str { match self { Self::History => "History", Self::Starred => "Starred", Self::Settings => "Settings", Self::Genesis => "Genesis", Self::Explorer => "Explorer", Self::Quantum => "Quantum", Self::Introduction => "Introduction", Self::GetStarted => "Get Started", Self::Tutorial => "Tutorial", Self::Changelog => "Changelog", Self::Team => "Team", Self::Billing => "Billing", Self::Limits => "Limits", Self::General => "General", } } pub fn is_disabled(&self) -> bool { match self { Self::Quantum => true, _ => false, } } pub fn handler( &self, item: &Item, ) -> impl Fn(&mut SidebarStory, &ClickEvent, &mut Window, &mut Context) + 'static { let item = *item; let subitem = *self; move |this, _, _, cx| { println!( "Clicked on item: {}, child: {}", item.label(), subitem.label() ); this.active_items.insert(item, true); this.last_active_item = item; this.active_subitem = Some(subitem); cx.notify(); } } } impl super::Story for SidebarStory { fn title() -> &'static str { "Sidebar" } fn description() -> &'static str { "A composable, themeable and customizable sidebar component." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl Focusable for SidebarStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for SidebarStory { fn render( &mut self, window: &mut gpui::Window, cx: &mut gpui::Context, ) -> impl gpui::IntoElement { let groups: [Vec; 2] = [ vec![ Item::Playground, Item::Models, Item::Documentation, Item::Settings, ], vec![ Item::DesignEngineering, Item::SalesAndMarketing, Item::Travel, ], ]; h_flex() .rounded(cx.theme().radius) .border_1() .border_color(cx.theme().border) .h_full() .when(self.side.is_right(), |this| this.flex_row_reverse()) .child( Sidebar::new("sidebar-story") .side(self.side) .collapsed(self.collapsed) .w(px(220.)) .gap_0() .header( SidebarHeader::new() .child( div() .flex() .items_center() .justify_center() .rounded(cx.theme().radius) .bg(cx.theme().success) .text_color(cx.theme().success_foreground) .size_8() .flex_shrink_0() .when(!self.collapsed, |this| { this.child(Icon::new(IconName::GalleryVerticalEnd)) }) .when(self.collapsed, |this| { this.size_4() .bg(cx.theme().transparent) .text_color(cx.theme().foreground) .child(Icon::new(IconName::GalleryVerticalEnd)) }), ) .when(!self.collapsed, |this| { this.child( v_flex() .gap_0() .text_sm() .flex_1() .line_height(relative(1.25)) .overflow_hidden() .text_ellipsis() .child("Company Name") .child(div().child("Enterprise").text_xs()), ) }) .when(!self.collapsed, |this| { this.child( Icon::new(IconName::ChevronsUpDown).size_4().flex_shrink_0(), ) }) .dropdown_menu(|menu, _, _| { menu.menu( "Twitter Inc.", Box::new(SelectCompany(SharedString::from("twitter"))), ) .menu( "Meta Platforms", Box::new(SelectCompany(SharedString::from("meta"))), ) .menu( "Google Inc.", Box::new(SelectCompany(SharedString::from("google"))), ) }), ) .child( SidebarGroup::new("Platform").child(SidebarMenu::new().children( groups[0].iter().enumerate().map(|(ix, item)| { let is_active = self.last_active_item == *item && self.active_subitem == None; SidebarMenuItem::new(item.label()) .icon(item.icon()) .active(is_active) .default_open(ix == 0) .click_to_open(self.click_to_open_submenu) .when(ix == 0, |this| { this.context_menu({ move |this, _, _| { this.link( "About", "https://github.com/longbridge/gpui-component", ) } }) }) .children(item.items().into_iter().enumerate().map( |(ix, sub_item)| { SidebarMenuItem::new(sub_item.label()) .active(self.active_subitem == Some(sub_item)) .disable(sub_item.is_disabled()) .when(ix == 0, |this| { this.suffix({ let checked = self.checked; let view = cx.entity(); move |window, _| { Switch::new("switch") .xsmall() .checked(checked) .on_click(window.listener_for( &view, Self::switch_checked_handler, )) } }) .context_menu({ move |this, _, _| { this.label("This is a label") } }) }) .on_click(cx.listener(sub_item.handler(&item))) }, )) .on_click(cx.listener(item.handler())) }), )), ) .child( SidebarGroup::new("Projects").child(SidebarMenu::new().children( groups[1].iter().enumerate().map(|(ix, item)| { let is_active = self.last_active_item == *item && self.active_subitem == None; SidebarMenuItem::new(item.label()) .icon(item.icon()) .active(is_active) .disable(item.is_disabled()) .click_to_open(self.click_to_open_submenu) .when(ix == 0, |this| { this.suffix(|_, _| { Badge::new().dot().count(1).child( div().p_0p5().child(Icon::new(IconName::Bell)), ) }) }) .when(ix == 1, |this| { this.suffix(|_, _| Icon::new(IconName::Settings2)) }) .on_click(cx.listener(item.handler())) }), )), ) .footer( SidebarFooter::new() .justify_between() .child( h_flex() .gap_2() .child(IconName::CircleUser) .when(!self.collapsed, |this| this.child("Jason Lee")), ) .when(!self.collapsed, |this| { this.child(Icon::new(IconName::ChevronsUpDown).size_4()) }), ), ) .child( v_flex() .size_full() .gap_4() .p_4() .child( h_flex() .items_center() .gap_3() .when(self.side.is_right(), |this| { this.flex_row_reverse().justify_between() }) .child( SidebarToggleButton::new() .side(self.side) .collapsed(self.collapsed) .on_click(cx.listener(|this, _, _, cx| { this.collapsed = !this.collapsed; cx.notify(); })), ) .child(Divider::vertical().h_4()) .child( Breadcrumb::new() .child("Breadcrumb") .child(BreadcrumbItem::new("Home").on_click(cx.listener( |this, _, _, cx| { this.last_active_item = Item::Playground; cx.notify(); }, ))) .child( BreadcrumbItem::new(self.last_active_item.label()) .on_click(cx.listener(|this, _, _, cx| { this.active_subitem = None; cx.notify(); })), ) .when_some(self.active_subitem, |this, subitem| { this.child(BreadcrumbItem::new(subitem.label())) }), ), ) .child(self.render_content(window, cx)), ) } } ================================================ FILE: crates/story/src/stories/skeleton_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled, Window, px, }; use gpui_component::{ActiveTheme as _, skeleton::Skeleton, v_flex}; use crate::section; pub struct SkeletonStory { focus_handle: gpui::FocusHandle, value: f32, } impl super::Story for SkeletonStory { fn title() -> &'static str { "Skeleton" } fn description() -> &'static str { "Use to show a placeholder while content is loading." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl SkeletonStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), value: 50., } } pub fn set_value(&mut self, value: f32) { self.value = value; } } impl Focusable for SkeletonStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for SkeletonStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .w_full() .gap_3() .child( section("Skeleton") .max_w_md() .child(Skeleton::new().size_12().rounded_full()) .child( v_flex() .gap_2() .child(Skeleton::new().w(px(250.)).h_4().rounded(cx.theme().radius)) .child(Skeleton::new().w(px(200.)).h_4().rounded(cx.theme().radius)), ), ) .child( section("Card").max_w_md().child( v_flex() .gap_2() .child( Skeleton::new() .w(px(250.)) .h(px(125.)) .rounded(cx.theme().radius), ) .child( v_flex() .gap_2() .child(Skeleton::new().w(px(250.)).h_4().rounded(cx.theme().radius)) .child( Skeleton::new().w(px(200.)).h_4().rounded(cx.theme().radius), ), ), ), ) } } ================================================ FILE: crates/story/src/stories/slider_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, Hsla, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, hsla, px, }; use gpui_component::{ ActiveTheme, Colorize as _, StyledExt, WindowExt, checkbox::Checkbox, clipboard::Clipboard, h_flex, slider::{Slider, SliderEvent, SliderScale, SliderState}, v_flex, }; use crate::section; pub struct SliderStory { focus_handle: gpui::FocusHandle, slider1: Entity, slider1_value: f32, slider2: Entity, slider2_value: f32, slider3: Entity, slider_hsl: [Entity; 4], slider_hsl_value: Hsla, slider4: Entity, slider_logarithmic: Entity, disabled: bool, _subscritions: Vec, } impl super::Story for SliderStory { fn title() -> &'static str { "Slider" } fn description() -> &'static str { "Displays a slider control for selecting a value within a range." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl SliderStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { let slider1 = cx.new(|_| { SliderState::new() .min(-255.) .max(255.) .default_value(75.) .step(15.) }); let slider2 = cx.new(|_| { SliderState::new() .min(0.) .max(5.) .step(1.0) .default_value(2.) }); let slider_hsl = [ cx.new(|_| { SliderState::new() .min(0.) .max(1.) .step(0.01) .default_value(0.38) }), cx.new(|_| { SliderState::new() .min(0.) .max(1.) .step(0.01) .default_value(0.5) }), cx.new(|_| { SliderState::new() .min(0.) .max(1.) .step(0.01) .default_value(0.5) }), cx.new(|_| { SliderState::new() .min(0.) .max(1.) .step(0.01) .default_value(0.5) }), ]; let slider3 = cx.new(|_| { SliderState::new() .min(0.) .max(100.) .default_value(12.0..45.0) .step(1.) }); let slider4 = cx.new(|_| { SliderState::new() .min(0.) .max(360.) .default_value(100.0..300.0) .step(1.) }); let slider_logarithmic = cx.new(|_| { SliderState::new() .min(0.25) .max(4.0) .default_value(1.0) .step(0.05) .scale(SliderScale::Logarithmic) }); let mut _subscritions = vec![ cx.subscribe(&slider1, |this, _, event: &SliderEvent, cx| match event { SliderEvent::Change(value) => { this.slider1_value = value.start(); cx.notify(); } }), cx.subscribe(&slider2, |this, _, event: &SliderEvent, cx| match event { SliderEvent::Change(value) => { this.slider2_value = value.start(); cx.notify(); } }), ]; _subscritions.extend( slider_hsl .iter() .map(|slider| { cx.subscribe(slider, |this, _, event: &SliderEvent, cx| match event { SliderEvent::Change(_) => { this.slider_hsl_value = hsla( this.slider_hsl[0].read(cx).value().start(), this.slider_hsl[1].read(cx).value().start(), this.slider_hsl[2].read(cx).value().start(), this.slider_hsl[3].read(cx).value().start(), ); cx.notify(); } }) }) .collect::>(), ); slider_hsl[0].update(cx, |slider, cx| { cx.emit(SliderEvent::Change(slider.value())); }); Self { focus_handle: cx.focus_handle(), slider1_value: 0., slider2_value: 0., slider1, slider2, slider3, slider4, slider_hsl, slider_hsl_value: gpui::red(), slider_logarithmic, disabled: false, _subscritions, } } } impl Focusable for SliderStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for SliderStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let rgb = SharedString::from(self.slider_hsl_value.to_hex()); v_flex() .w_full() .gap_3() .child( h_flex().child( Checkbox::new("disabled") .checked(self.disabled) .label("Disabled") .on_click(cx.listener(|this, check: &bool, _, cx| { this.disabled = *check; cx.notify(); })), ), ) .child( section("Horizontal Slider") .max_w_md() .v_flex() .child(Slider::new(&self.slider1).disabled(self.disabled)) .child(format!("Value: {}", self.slider1_value)), ) .child( section("Slider (0 - 5) and with color") .max_w_md() .v_flex() .child( Slider::new(&self.slider2) .disabled(self.disabled) .bg(cx.theme().success) .text_color(cx.theme().success_foreground), ) .child(format!("Value: {}", self.slider2_value)), ) .child( section("Range Mode") .max_w_md() .v_flex() .child(Slider::new(&self.slider3).disabled(self.disabled)) .child(format!("Value: {}", self.slider3.read(cx).value())), ) .child( section("Vertical with Range") .max_w_md() .v_flex() .child( Slider::new(&self.slider4) .vertical() .h(px(200.)) .rounded(px(2.)) .disabled(self.disabled), ) .child(format!("Value: {}", self.slider4.read(cx).value())), ) .child( section("Color Picker") .sub_title( h_flex() .gap_2() .items_center() .child( h_flex() .text_color(self.slider_hsl_value) .child(rgb.clone()), ) .child(Clipboard::new("copy-hsl").value(rgb).on_copied( |_, window, cx| { window.push_notification("Color copied to clipboard.", cx) }, )), ) .max_w_md() .justify_around() .child( v_flex() .h_32() .gap_3() .items_center() .justify_center() .child( Slider::new(&self.slider_hsl[0]) .vertical() .disabled(self.disabled), ) .child( v_flex() .items_center() .child("Hue") .child(format!("{:.0}", self.slider_hsl_value.h * 360.)), ), ) .child( v_flex() .h_32() .gap_3() .items_center() .justify_center() .child( Slider::new(&self.slider_hsl[1]) .vertical() .disabled(self.disabled), ) .child( v_flex() .items_center() .child("Saturation") .child(format!("{:.0}", self.slider_hsl_value.s * 100.)), ), ) .child( v_flex() .h_32() .gap_3() .items_center() .justify_center() .child( Slider::new(&self.slider_hsl[2]) .vertical() .disabled(self.disabled), ) .child( v_flex() .items_center() .child("Lightness") .child(format!("{:.0}", self.slider_hsl_value.l * 100.)), ), ) .child( v_flex() .h_32() .gap_3() .items_center() .justify_center() .child( Slider::new(&self.slider_hsl[3]) .vertical() .disabled(self.disabled), ) .child( v_flex() .items_center() .child("Alpha") .child(format!("{:.0}", self.slider_hsl_value.a * 100.)), ), ), ) .child( section("Logarithmic Slider") .max_w_md() .v_flex() .child( Slider::new(&self.slider_logarithmic) .horizontal() .disabled(self.disabled), ) .child(format!( "Playback Speed: {:.2}", self.slider_logarithmic.read(cx).value().start() )), ) } } ================================================ FILE: crates/story/src/stories/spinner_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled, Window, px, }; use gpui_component::{ActiveTheme as _, IconName, Sizable, spinner::Spinner, v_flex}; use crate::section; pub struct SpinnerStory { focus_handle: gpui::FocusHandle, value: f32, } impl super::Story for SpinnerStory { fn title() -> &'static str { "Spinner" } fn description() -> &'static str { "Displays an spinner showing the completion progress of a task." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl SpinnerStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), value: 50., } } pub fn set_value(&mut self, value: f32) { self.value = value; } } impl Focusable for SpinnerStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for SpinnerStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .w_full() .gap_3() .child(section("Spinner").gap_x_2().child(Spinner::new())) .child( section("Spinner with color") .gap_x_2() .child(Spinner::new().color(cx.theme().blue)) .child(Spinner::new().color(cx.theme().green)), ) .child( section("Spinner with size") .gap_x_2() .child(Spinner::new().with_size(px(64.))) .child(Spinner::new().large()) .child(Spinner::new()) .child(Spinner::new().small()) .child(Spinner::new().xsmall()), ) .child( section("Spinner with Icon") .gap_x_2() .child(Spinner::new().icon(IconName::LoaderCircle)) .child( Spinner::new() .icon(IconName::LoaderCircle) .large() .color(cx.theme().cyan), ), ) } } ================================================ FILE: crates/story/src/stories/stepper_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, Styled, Subscription, Window, }; use gpui_component::{ IconName, Selectable as _, Sizable, Size, StyledExt, button::{Button, ButtonGroup}, checkbox::Checkbox, h_flex, stepper::{Stepper, StepperItem}, v_flex, }; use crate::section; pub struct StepperStory { focus_handle: gpui::FocusHandle, size: Size, stepper0_step: usize, stepper1_step: usize, stepper2_step: usize, stepper3_step: usize, disabled: bool, _subscritions: Vec, } impl super::Story for StepperStory { fn title() -> &'static str { "Stepper" } fn description() -> &'static str { "A step-by-step process for users to navigate through a series of steps." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl StepperStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), size: Size::default(), stepper0_step: 1, stepper1_step: 0, stepper2_step: 2, stepper3_step: 0, disabled: false, _subscritions: vec![], } } } impl Focusable for StepperStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for StepperStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .w_full() .gap_3() .child( h_flex() .gap_3() .child( ButtonGroup::new("toggle-size") .outline() .compact() .child( Button::new("xsmall") .label("XSmall") .selected(self.size == Size::XSmall), ) .child( Button::new("small") .label("Small") .selected(self.size == Size::Small), ) .child( Button::new("medium") .label("Medium") .selected(self.size == Size::Medium), ) .child( Button::new("large") .label("Large") .selected(self.size == Size::Large), ) .on_click(cx.listener(|this, selecteds: &Vec, _, cx| { let size = match selecteds[0] { 0 => Size::XSmall, 1 => Size::Small, 2 => Size::Medium, 3 => Size::Large, _ => unreachable!(), }; this.size = size; cx.notify(); })), ) .child( Checkbox::new("disabled") .checked(self.disabled) .label("Disabled") .on_click(cx.listener(|this, check: &bool, _, cx| { this.disabled = *check; cx.notify(); })), ), ) .child( section("Horizontal Stepper").max_w_md().v_flex().child( Stepper::new("stepper0") .w_full() .with_size(self.size) .disabled(self.disabled) .selected_index(self.stepper0_step) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), StepperItem::new().child("Step 3"), ]) .on_click(cx.listener(|this, step, _, cx| { this.stepper0_step = *step; cx.notify(); })), ), ) .child( section("Icon Stepper").max_w_md().v_flex().child( Stepper::new("stepper1") .w_full() .with_size(self.size) .disabled(self.disabled) .selected_index(self.stepper1_step) .items([ StepperItem::new() .icon(IconName::Calendar) .child("Order Details"), StepperItem::new().icon(IconName::Inbox).child("Shipping"), StepperItem::new().icon(IconName::Frame).child("Preview"), StepperItem::new().icon(IconName::Info).child("Finish"), ]) .on_click(cx.listener(|this, step, _, cx| { this.stepper1_step = *step; cx.notify(); })), ), ) .child( section("Vertical Stepper").max_w_md().v_flex().child( Stepper::new("stepper3") .vertical() .with_size(self.size) .disabled(self.disabled) .selected_index(self.stepper2_step) .items_center() .items([ StepperItem::new() .pb_8() .icon(IconName::Building2) .child(v_flex().child("Step 1").child("Description for step 1.")), StepperItem::new() .pb_8() .icon(IconName::Asterisk) .child(v_flex().child("Step 2").child("Description for step 2.")), StepperItem::new() .pb_8() .icon(IconName::Folder) .child(v_flex().child("Step 3").child("Description for step 3.")), StepperItem::new() .icon(IconName::CircleCheck) .child(v_flex().child("Step 4").child("Description for step 4.")), ]) .on_click(cx.listener(|this, step, _, cx| { this.stepper2_step = *step; cx.notify(); })), ), ) .child( section("Text Center").max_w_md().v_flex().child( Stepper::new("stepper4") .with_size(self.size) .disabled(self.disabled) .selected_index(self.stepper3_step) .text_center(true) .items([ StepperItem::new().child( v_flex() .items_center() .child("Step 1") .child("Desc for step 1."), ), StepperItem::new().child( v_flex() .items_center() .child("Step 2") .child("Desc for step 2."), ), StepperItem::new().child( v_flex() .items_center() .child("Step 3") .child("Desc for step 3."), ), ]) .on_click(cx.listener(|this, step, _, cx| { this.stepper3_step = *step; cx.notify(); })), ), ) } } ================================================ FILE: crates/story/src/stories/switch_story.rs ================================================ use gpui::{ App, AppContext, Context, Div, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window, px, }; use gpui_component::{ ActiveTheme, Disableable as _, Sizable, h_flex, label::Label, switch::Switch, v_flex, }; use crate::section; pub struct SwitchStory { focus_handle: FocusHandle, switch1: bool, switch2: bool, switch3: bool, } impl super::Story for SwitchStory { fn title() -> &'static str { "Switch" } fn description() -> &'static str { "A control that allows the user to toggle between checked and not checked." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl SwitchStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), switch1: true, switch2: false, switch3: true, } } } impl Focusable for SwitchStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for SwitchStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = cx.theme(); fn title(title: impl Into) -> Div { v_flex().flex_1().gap_2().child(Label::new(title).text_xl()) } fn card(cx: &Context) -> Div { h_flex() .items_center() .gap_4() .p_4() .w_full() .rounded(cx.theme().radius) .border_1() .border_color(cx.theme().border) } v_flex() .w_full() .gap_3() .child( card(cx) .child( title("Marketing emails").child( Label::new("Receive emails about new products, features, and more.") .text_color(theme.muted_foreground), ), ) .child( h_flex().gap_2().child("Subscribe").child( Switch::new("switch1") .checked(self.switch1) .on_click(cx.listener(move |view, checked, _, cx| { view.switch1 = *checked; cx.notify(); })), ), ), ) .child( card(cx) .child( title("Security emails").child( Label::new( "Receive emails about your account security. \ When turn off, you never receive email again.", ) .text_color(theme.muted_foreground), ), ) .child( Switch::new("switch2") .checked(self.switch2) .on_click(cx.listener(move |view, checked, _, cx| { view.switch2 = *checked; cx.notify(); })), ), ) .child( section("Disabled") .child(Switch::new("switch3").disabled(true).on_click(|v, _, _| { println!("Switch value changed: {:?}", v); })) .child( Switch::new("switch3_1") .w(px(200.)) .label("Airplane Mode") .checked(true) .disabled(true) .on_click(|ev, _, _| { println!("Switch value changed: {:?}", ev); }), ), ) .child( section("Small Size").child( Switch::new("switch3") .checked(self.switch3) .label("Small Size") .small() .on_click(cx.listener(move |view, checked, _, cx| { view.switch3 = *checked; cx.notify(); })), ), ) } } ================================================ FILE: crates/story/src/stories/table_story.rs ================================================ use gpui::{ App, AppContext as _, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Window, prelude::FluentBuilder as _, px, }; use gpui_component::{ ActiveTheme, Selectable as _, Sizable, Size, button::{Button, ButtonGroup}, h_flex, table::{ Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, }, tag::Tag, v_flex, }; use crate::section; pub struct TableStory { focus_handle: FocusHandle, size: Size, } impl TableStory { fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), size: Size::default(), } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn set_size(&mut self, size: Size, _: &mut Window, cx: &mut Context) { self.size = size; cx.notify(); } } impl super::Story for TableStory { fn title() -> &'static str { "Table" } fn description() -> &'static str { "A basic table component for directly rendering tabular data." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl Focusable for TableStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } fn status_tag(status: &str) -> Tag { match status { "Paid" => Tag::success().outline().child(status.to_string()), "Pending" => Tag::warning().outline().child(status.to_string()), "Unpaid" => Tag::danger().outline().child(status.to_string()), _ => Tag::new().child(status.to_string()), } .xsmall() } impl Render for TableStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let invoices: Vec<(&str, &str, &str, &str, &str)> = vec![ ("INV001", "Paid", "Credit Card", "$250.00", "2024-01-15"), ("INV002", "Pending", "PayPal", "$150.00", "2024-02-01"), ("INV003", "Unpaid", "Bank Transfer", "$350.00", "2024-02-15"), ( "INV004", "Paid", "Credit Card\nMaster Card / Visa", "$450.00", "2024-03-01", ), ("INV005", "Paid", "PayPal", "$550.00", "2024-03-15"), ( "INV006", "Pending", "Bank Transfer", "$200.00", "2024-04-01", ), ("INV007", "Unpaid", "Credit Card", "$300.00", "2024-04-15"), ]; v_flex() .size_full() .gap_6() .child( h_flex().gap_3().child( ButtonGroup::new("toggle-size") .outline() .compact() .child( Button::new("xsmall") .label("XSmall") .selected(self.size == Size::XSmall), ) .child( Button::new("small") .label("Small") .selected(self.size == Size::Small), ) .child( Button::new("medium") .label("Medium") .selected(self.size == Size::Medium), ) .child( Button::new("large") .label("Large") .selected(self.size == Size::Large), ) .on_click(cx.listener(|this, selecteds: &Vec, window, cx| { let size = match selecteds[0] { 0 => Size::XSmall, 1 => Size::Small, 2 => Size::Medium, 3 => Size::Large, _ => unreachable!(), }; this.set_size(size, window, cx); })), ), ) .child( section("Table").child( Table::new() .with_size(self.size) .child( TableHeader::new().child( TableRow::new() .child(TableHead::new().w(px(150.)).child("Invoice")) .child(TableHead::new().col_span(2).child("Status")) .child(TableHead::new().text_right().child("Amount")) .child(TableHead::new().text_right().child("Date")), ), ) .child(TableBody::new().children(invoices.iter().map( |(invoice, status, method, amount, date)| { TableRow::new() .child(TableCell::new().w(px(150.)).child(invoice.to_string())) .child(TableCell::new().child(status_tag(status))) .child(TableCell::new().child(method.to_string())) .child(TableCell::new().text_right().child(amount.to_string())) .child(TableCell::new().text_right().child(date.to_string())) }, ))) .child( TableFooter::new().child( TableRow::new() .child(TableCell::new().col_span(3).child("Total")) .child( TableCell::new() .col_span(2) .text_right() .child("$2,250.00"), ), ), ) .child(TableCaption::new().child("A list of your recent invoices.")), ), ) .child( section("With Border").child( Table::new() .with_size(self.size) .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .child( TableHeader::new().child( TableRow::new() .child(TableHead::new().w(px(100.)).child("Invoice")) .child(TableHead::new().child("Method")) .child(TableHead::new().text_right().child("Amount")) .child(TableHead::new().text_right().child("Date")), ), ) .child( TableBody::new().children(invoices.iter().enumerate().take(6).map( |(ix, (invoice, _, method, amount, date))| { TableRow::new() .when(ix % 2 != 0, |this| this.bg(cx.theme().table_even)) .child( TableCell::new().w(px(100.)).child(invoice.to_string()), ) .child(TableCell::new().child(method.to_string())) .child( TableCell::new().text_right().child(amount.to_string()), ) .child( TableCell::new().text_right().child(date.to_string()), ) }, )), ), ), ) } } ================================================ FILE: crates/story/src/stories/tabs_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Window, }; use gpui_component::{ ActiveTheme as _, IconName, Selectable as _, Sizable, Size, button::{Button, ButtonGroup, ButtonVariants}, checkbox::Checkbox, h_flex, tab::{Tab, TabBar}, v_flex, }; use crate::section; pub struct TabsStory { focus_handle: FocusHandle, active_tab_ix: usize, size: Size, menu: bool, } impl super::Story for TabsStory { fn title() -> &'static str { "Tabs" } fn description() -> &'static str { "A set of layered sections of content—known as tab panels—that are displayed one at a time." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl TabsStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), active_tab_ix: 0, size: Size::default(), menu: false, } } fn set_active_tab(&mut self, ix: usize, _: &mut Window, cx: &mut Context) { self.active_tab_ix = ix; cx.notify(); } fn set_size(&mut self, size: Size, _: &mut Window, cx: &mut Context) { self.size = size; cx.notify(); } } impl Focusable for TabsStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for TabsStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .w_full() .gap_3() .child( h_flex() .gap_3() .child( ButtonGroup::new("toggle-size") .outline() .compact() .child( Button::new("xsmall") .label("XSmall") .selected(self.size == Size::XSmall), ) .child( Button::new("small") .label("Small") .selected(self.size == Size::Small), ) .child( Button::new("medium") .label("Medium") .selected(self.size == Size::Medium), ) .child( Button::new("large") .label("Large") .selected(self.size == Size::Large), ) .on_click(cx.listener(|this, selecteds: &Vec, window, cx| { let size = match selecteds[0] { 0 => Size::XSmall, 1 => Size::Small, 2 => Size::Medium, 3 => Size::Large, _ => unreachable!(), }; this.set_size(size, window, cx); })), ) .child( Checkbox::new("show-menu") .label("More menu") .checked(self.menu) .on_click(cx.listener(|this, _, _, cx| { this.menu = !this.menu; cx.notify(); })), ), ) .child( section("Tabs").max_w_md().child( TabBar::new("tabs") .w_full() .with_size(self.size) .menu(self.menu) .selected_index(self.active_tab_ix) .on_click(cx.listener(|this, ix: &usize, window, cx| { this.set_active_tab(*ix, window, cx); })) .border_t_1() .border_color(cx.theme().border) .prefix( h_flex() .mx_1() .child( Button::new("back") .ghost() .xsmall() .icon(IconName::ArrowLeft), ) .child( Button::new("forward") .ghost() .xsmall() .icon(IconName::ArrowRight), ), ) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile").disabled(true)) .child(Tab::new().label("Documents")) .child(Tab::new().label("Mail")) .child(Tab::new().label("Appearance")) .child(Tab::new().label("Settings")) .child(Tab::new().label("About")) .child(Tab::new().label("License")) .suffix( h_flex() .mx_1() .child(Button::new("inbox").ghost().xsmall().icon(IconName::Inbox)) .child( Button::new("more") .ghost() .xsmall() .icon(IconName::Ellipsis), ), ), ), ) .child( section("Underline Tabs").max_w_md().child( TabBar::new("underline") .w_full() .underline() .with_size(self.size) .menu(self.menu) .selected_index(self.active_tab_ix) .on_click(cx.listener(|this, ix: &usize, window, cx| { this.set_active_tab(*ix, window, cx); })) .child("Account") .child("Profile") .child("Documents") .child("Mail") .child("Appearance") .child("Settings") .child("About") .child("License"), ), ) .child( section("Pill Tabs").max_w_md().child( TabBar::new("pill") .w_full() .pill() .with_size(self.size) .menu(self.menu) .selected_index(self.active_tab_ix) .on_click(cx.listener(|this, ix: &usize, window, cx| { this.set_active_tab(*ix, window, cx); })) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile").disabled(true)) .child(Tab::new().label("Documents & Files")) .child(Tab::new().label("Mail")) .child(Tab::new().label("Appearance")) .child(Tab::new().label("Settings")) .child(Tab::new().label("About")) .child(Tab::new().label("License")), ), ) .child( section("Outline Tabs").max_w_md().child( TabBar::new("outline") .w_full() .outline() .with_size(self.size) .menu(self.menu) .selected_index(self.active_tab_ix) .on_click(cx.listener(|this, ix: &usize, window, cx| { this.set_active_tab(*ix, window, cx); })) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile").disabled(true)) .child(Tab::new().label("Documents & Files")) .child(Tab::new().label("Mail")) .child(Tab::new().label("Appearance")) .child(Tab::new().label("Settings")) .child(Tab::new().label("About")) .child(Tab::new().label("License")), ), ) .child( section("Segmented Tabs").max_w_md().child( TabBar::new("segmented") .w_full() .segmented() .with_size(self.size) .menu(self.menu) .selected_index(self.active_tab_ix) .on_click(cx.listener(|this, ix: &usize, window, cx| { this.set_active_tab(*ix, window, cx); })) .child(IconName::Bot) .child(IconName::Calendar) .child(IconName::Map) .children(vec!["Appearance", "Settings", "About", "License"]), ), ) .child( section("Segmented Tabs (With filling space)") .max_w_md() .child( TabBar::new("flex tabs") .w_full() .segmented() .with_size(self.size) .selected_index(self.active_tab_ix) .on_click(cx.listener(|this, ix: &usize, window, cx| { this.set_active_tab(*ix, window, cx); })) .child(Tab::new().flex_1().label("About")) .child(Tab::new().flex_1().label("Profile")), ), ) } } ================================================ FILE: crates/story/src/stories/tag_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Window, px, }; use gpui_component::{ColorName, Sizable, h_flex, indigo_50, indigo_500, tag::Tag, v_flex}; use crate::section; pub struct TagStory { focus_handle: FocusHandle, } impl super::Story for TagStory { fn title() -> &'static str { "Tag" } fn description() -> &'static str { "A short item that can be used to categorize or label content." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl TagStory { pub(crate) fn new(_: &mut Window, cx: &mut App) -> Self { Self { focus_handle: cx.focus_handle(), } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } } impl Focusable for TagStory { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for TagStory { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .w_full() .gap_3() .child( section("Tag (default)").child( h_flex() .gap_2() .child(Tag::primary().child("Tag")) .child(Tag::secondary().child("Secondary")) .child(Tag::danger().child("Danger")) .child(Tag::success().child("Success")) .child(Tag::warning().child("Warning")) .child(Tag::info().child("Info")) .child( Tag::custom(indigo_500(), indigo_50(), indigo_500()).child("Custom"), ), ), ) .child( section("Tag (outline)").child( h_flex() .gap_2() .child(Tag::primary().outline().child("Tag")) .child(Tag::secondary().outline().child("Secondary")) .child(Tag::danger().outline().child("Danger")) .child(Tag::success().outline().child("Success")) .child(Tag::warning().outline().child("Warning")) .child(Tag::info().outline().child("Info")) .child( Tag::custom(indigo_500(), indigo_500(), indigo_500()) .outline() .child("Custom"), ), ), ) .child( section("Tag (small)").child( h_flex() .gap_2() .child(Tag::primary().small().child("Tag")) .child(Tag::secondary().small().child("Secondary")) .child(Tag::danger().small().child("Danger")) .child(Tag::success().small().child("Success")) .child(Tag::warning().small().child("Warning")) .child(Tag::info().small().child("Info")), ), ) .child( section("Tag (rounded full)").child( h_flex() .gap_2() .child(Tag::primary().rounded_full().child("Tag")) .child(Tag::secondary().rounded_full().child("Secondary")) .child(Tag::danger().rounded_full().child("Danger")) .child(Tag::success().rounded_full().child("Success")) .child(Tag::warning().rounded_full().child("Warning")) .child(Tag::info().rounded_full().child("Info")), ), ) .child( section("Tag (small with rounded full)").child( h_flex() .gap_2() .child(Tag::primary().small().rounded_full().child("Tag")) .child(Tag::secondary().small().rounded_full().child("Secondary")) .child(Tag::danger().small().rounded_full().child("Danger")) .child(Tag::success().small().rounded_full().child("Success")) .child(Tag::warning().small().rounded_full().child("Warning")) .child(Tag::info().small().rounded_full().child("Info")), ), ) .child( section("Tag (rounded 0px)").child( h_flex() .gap_2() .child(Tag::primary().small().rounded(px(0.)).child("Tag")) .child(Tag::secondary().small().rounded(px(0.)).child("Secondary")) .child(Tag::danger().small().rounded(px(0.)).child("Danger")) .child(Tag::success().small().rounded(px(0.)).child("Success")) .child(Tag::warning().small().rounded(px(0.)).child("Warning")) .child(Tag::info().small().rounded(px(0.)).child("Info")), ), ) .child( section("Color Tags").child( v_flex().gap_4().child( h_flex().gap_2().flex_wrap().children( ColorName::all() .into_iter() .filter(|color| *color != ColorName::Gray) .map(|color| Tag::color(color).child(color.to_string())), ), ), ), ) } } ================================================ FILE: crates/story/src/stories/textarea_story.rs ================================================ use gpui::{ App, AppContext as _, ClickEvent, Context, Entity, Focusable, IntoElement, ParentElement as _, Render, Styled, Window, px, }; use crate::section; use gpui_component::{ Sizable, button::Button, h_flex, input::{Input, InputState}, v_flex, }; pub fn init(_: &mut App) {} pub struct TextareaStory { textarea: Entity, textarea_auto_grow: Entity, textarea_no_wrap: Entity, textarea_auto_grow_no_wrap: Entity, } impl super::Story for TextareaStory { fn title() -> &'static str { "Textarea" } fn description() -> &'static str { "Input with multi-line mode." } fn closable() -> bool { false } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl TextareaStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { let textarea = cx.new(|cx| { InputState::new(window, cx) .multi_line(true) .rows(10) .placeholder("Enter text here...") .searchable(true) .default_value( unindent::unindent( r#"Hello 世界,this is GPUI component. The GPUI Component is a collection of UI components for GPUI framework, including. Button, Input, Checkbox, Radio, Dropdown, Tab, and more... Here is an application that is built by using GPUI Component. > This application is still under development, not published yet. ![image](https://github.com/user-attachments/assets/559a648d-19df-4b5a-b563-b78cc79c8894) ![image](https://github.com/user-attachments/assets/5e06ad5d-7ea0-43db-8d13-86a240da4c8d) ## Demo If you want to see the demo, here is a some demo applications. "#, ) ) }); let textarea_no_wrap = cx.new(|cx| { InputState::new(window, cx) .multi_line(true) .rows(6) .soft_wrap(false) .default_value("This is a very long line of text to test if the horizontal scrolling function is working properly, and it should not wrap automatically but display a horizontal scrollbar.\nThe second line is also very long text, used to test the horizontal scrolling effect under multiple lines, and you can input more content to test.\nThe third line: Here you can input other long text content that requires horizontal scrolling.\n") }); let textarea_auto_grow = cx.new(|cx| { InputState::new(window, cx) .auto_grow(1, 5) .placeholder("Enter text here...") .default_value( "Hello 世界 this is a very long line of text \ to test if the horizontal scrolling function is working \ properly, and it should not wrap automatically but display \ a horizontal scrollbar.\n\ The second line is also very long text, used to test the \ horizontal scrolling effect under multiple lines, and you \ can input more content to test.\nThe third line: Here you \ can input other long text content that requires \ horizontal scrolling.\n", ) }); let textarea_auto_grow_no_wrap = cx.new(|cx| { InputState::new(window, cx) .auto_grow(1, 5) .soft_wrap(false) .placeholder("Enter text here...") .default_value("Hello 世界,this is GPUI component.") }); Self { textarea, textarea_auto_grow, textarea_no_wrap, textarea_auto_grow_no_wrap, } } fn on_insert_text_to_textarea( &mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context, ) { self.textarea.update(cx, |input, cx| { input.insert("Hello 你好", window, cx); }); } fn on_replace_text_to_textarea( &mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context, ) { self.textarea.update(cx, |input, cx| { input.replace("Hello 你好", window, cx); }); } } impl Focusable for TextareaStory { fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { self.textarea.focus_handle(cx) } } impl Render for TextareaStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let loc = self.textarea.read(cx).cursor_position(); v_flex() .w_full() .gap_3() .child( section("Textarea").child( v_flex() .gap_2() .w_full() .child(Input::new(&self.textarea).h(px(320.))) .child( h_flex() .justify_between() .child( h_flex() .gap_2() .child( Button::new("btn-insert-text") .outline() .xsmall() .label("Insert Text") .on_click( cx.listener(Self::on_insert_text_to_textarea), ), ) .child( Button::new("btn-replace-text") .outline() .xsmall() .label("Replace Text") .on_click( cx.listener(Self::on_replace_text_to_textarea), ), ), ) .child(format!("{}:{}", loc.line, loc.character)), ), ), ) .child( section("No Wrap") .max_w_md() .child(Input::new(&self.textarea_no_wrap).h(px(200.))), ) .child( section("Auto Grow") .max_w_md() .child(Input::new(&self.textarea_auto_grow)), ) .child( section("Auto Grow with No Wrap") .max_w_md() .child(Input::new(&self.textarea_auto_grow_no_wrap)), ) } } ================================================ FILE: crates/story/src/stories/theme_story/checkerboard.rs ================================================ use gpui::*; use gpui_component::ActiveTheme as _; #[derive(IntoElement)] pub struct Checkerboard { children: Vec, is_dark: bool, } impl Checkerboard { pub fn new(is_dark: bool) -> Self { Self { children: Vec::new(), is_dark, } } } impl ParentElement for Checkerboard { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } impl RenderOnce for Checkerboard { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let square_size = px(12.); // Use a subtle difference for the checkerboard let (c1, c2) = if self.is_dark { // Dark mode: dark grey and slightly lighter grey (hsla(0., 0., 0.1, 1.), hsla(0., 0., 0.13, 1.)) } else { // Light mode: white and light grey (hsla(0., 0., 1.0, 1.), hsla(0., 0., 0.95, 1.)) }; div() .bg(c1) .rounded(cx.theme().radius_lg) .overflow_hidden() .size_full() .child( gpui::canvas( move |_, _, _| (), move |bounds, _, window, _| { let size = square_size; let rows = (bounds.size.height / size).ceil() as i32; let cols = (bounds.size.width / size).ceil() as i32; for row in 0..rows { for col in 0..cols { if (row + col) % 2 == 0 { let origin = bounds.origin + gpui::point(size * (col as f32), size * (row as f32)); window.paint_quad(gpui::PaintQuad { bounds: gpui::Bounds { origin, size: gpui::size(size, size), }, corner_radii: gpui::Corners::default(), background: c2.into(), border_widths: gpui::Edges::default(), border_color: gpui::transparent_black(), border_style: gpui::BorderStyle::default(), }); } } } }, ) .absolute() .size_full(), ) .children(self.children) } } ================================================ FILE: crates/story/src/stories/theme_story/color_theme_story.rs ================================================ use gpui::{prelude::FluentBuilder, *}; use gpui_component::{ ActiveTheme as _, Icon, IconName, IndexPath, StyledExt as _, ThemeColor, button::{Button, ButtonVariants as _}, h_flex, input::{Input, InputEvent, InputState}, menu::PopupMenuItem, scroll::ScrollableElement, select::{Select, SelectEvent, SelectItem, SelectState}, sidebar::{Sidebar, SidebarMenu, SidebarMenuItem}, switch::Switch, v_flex, }; use crate::stories::theme_story::checkerboard::Checkerboard; use std::collections::BTreeMap; use std::rc::Rc; #[derive(Clone)] struct ColorEntry { name: String, color: Hsla, hex: String, is_explicit: bool, } #[derive(Clone)] struct ColorCategory { name: String, entries: Vec, } #[derive(Clone, PartialEq)] struct ThemeItem { name: SharedString, is_active: bool, } impl ThemeItem { fn new(name: impl Into, is_active: bool) -> Self { Self { name: name.into(), is_active, } } } impl SelectItem for ThemeItem { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn value(&self) -> &Self::Value { &self.name } fn render(&self, _window: &mut Window, cx: &mut App) -> impl IntoElement { h_flex() .w_full() .items_center() .gap_2() .child( div() .size(rems(1.0)) .flex_shrink_0() .when(self.is_active, |this| { this.child( Icon::new(IconName::Check) .size(rems(1.0)) .text_color(cx.theme().primary), ) }), ) .child(self.name.clone()) } } pub struct ThemeColorsStory { select_state: Entity>>, selected_theme_name: SharedString, show_all_colors: bool, sidebar_render_key: usize, force_open_state: Option, filter_by_value: Option, filter_input: Entity, all_categories: Vec, categories: Vec, } impl crate::stories::Story for ThemeColorsStory { fn title() -> &'static str { "Theme Colors" } fn description() -> &'static str { "A color theme viewer to explore colors organized by categories." // Themes are loaded by applying user-defined color overrides to a default base theme, // with inherited colors marked by an indicator dot. } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl ThemeColorsStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { use gpui_component::ThemeRegistry; let registry = ThemeRegistry::global(cx); let mut themes = registry.sorted_themes(); themes.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); let active_theme_name = cx.theme().theme_name().clone(); let items: Vec = themes .iter() .map(|theme| ThemeItem::new(theme.name.clone(), theme.name == active_theme_name)) .collect(); let current_theme = active_theme_name.clone(); let selected_index = items.iter().position(|item| item.name == current_theme); let selected_path = selected_index.map(|idx| IndexPath::default().row(idx)); let select_state = cx.new(|cx| SelectState::new(items, selected_path, window, cx)); let mut this = Self { select_state: select_state.clone(), selected_theme_name: current_theme, show_all_colors: false, sidebar_render_key: 0, force_open_state: None, filter_by_value: None, filter_input: cx.new(|cx| InputState::new(window, cx).placeholder("Search...")), all_categories: Vec::new(), categories: Vec::new(), }; cx.subscribe( &select_state, |this, _, event: &SelectEvent>, cx| { let SelectEvent::Confirm(theme_name) = event; if let Some(theme_name) = theme_name { this.selected_theme_name = theme_name.clone(); this.filter_by_value = None; this.all_categories.clear(); this.compute_categories(cx); cx.notify(); } }, ) .detach(); cx.subscribe(&this.filter_input, |this, _, event, cx| { if let InputEvent::Change = event { this.compute_categories(cx); cx.notify(); } }) .detach(); this.compute_categories(cx); this } fn get_theme_colors(&self, cx: &Context) -> ThemeColor { use gpui_component::{Theme as UITheme, ThemeRegistry}; if let Some(theme_config) = ThemeRegistry::global(cx) .themes() .get(&self.selected_theme_name) .cloned() { let mut temp_theme = if theme_config.mode.is_dark() { UITheme::from(ThemeColor::dark().as_ref()) } else { UITheme::from(ThemeColor::light().as_ref()) }; // Apply the config to get proper colors using the public API temp_theme.apply_config(&theme_config); temp_theme.colors } else { // Fallback to current theme if selected theme not found **cx.theme() } } fn get_isolated_theme(&self, cx: &App) -> (ThemeColor, bool) { use gpui_component::{Theme as UITheme, ThemeRegistry}; let registry = ThemeRegistry::global(cx); // Look up the selected theme configuration let selected_theme_config = registry.themes().get(&self.selected_theme_name); let is_dark = if let Some(config) = selected_theme_config { config.mode.is_dark() } else { // Fallback to system appearance if selected theme lookup fails let appearance = cx.window_appearance(); appearance == WindowAppearance::Dark || appearance == WindowAppearance::VibrantDark }; let theme_config = if is_dark { registry.default_dark_theme() } else { registry.default_light_theme() }; let mut temp_theme = if theme_config.mode.is_dark() { UITheme::from(ThemeColor::dark().as_ref()) } else { UITheme::from(ThemeColor::light().as_ref()) }; temp_theme.apply_config(theme_config); (temp_theme.colors, is_dark) } fn compute_categories(&mut self, cx: &Context) { use gpui_component::ThemeRegistry; if self.all_categories.is_empty() { let theme = self.get_theme_colors(cx); let registry = ThemeRegistry::global(cx); let theme_config = registry.themes().get(&self.selected_theme_name).cloned(); self.all_categories = format_colors(&theme, theme_config.as_ref().map(|c| &c.colors)); } let mut categories = self.all_categories.clone(); if let Some(filter_value) = self.filter_by_value { categories = filter_categories(categories, |entry| { colors_equal_u8(entry.color, filter_value) }); } else if !self.show_all_colors { categories = filter_categories(categories, |entry| entry.is_explicit); } let query = self.filter_input.read(cx).value().trim().to_lowercase(); if !query.is_empty() { let normalized_query = query.strip_prefix('#').unwrap_or(&query); categories = categories .into_iter() .filter_map( |ColorCategory { name: category, entries: colors, }| { let category_matches = category.to_lowercase().contains(&query); let filtered_colors: Vec<_> = colors .into_iter() .filter(|entry| { if category_matches || entry.name.to_lowercase().contains(&query) { return true; } // Hex matching entry.hex.starts_with(normalized_query) }) .collect(); if filtered_colors.is_empty() { None } else { Some(ColorCategory { name: category, entries: filtered_colors, }) } }, ) .collect(); } self.categories = categories; } fn render_color_swatch( name: String, color: Hsla, hex: String, is_explicit: bool, isolated_theme: &ThemeColor, cx: &App, ) -> impl IntoElement { use gpui_component::{WindowExt as _, clipboard::Clipboard}; let rgb_str = format!("#{}", hex); let swatch_group = format!("swatch-{}", name); h_flex() .group(swatch_group.clone()) .gap_3() .items_center() .child( div() .size_16() .rounded(cx.theme().radius) .bg(color) .border_1() .border_color(isolated_theme.border) .flex_shrink_0(), ) .child( v_flex() .gap_1() .flex_1() .child( h_flex() .gap_2() .items_center() .when(!is_explicit, |this| { this.child( div() .size_1p5() .rounded_full() .bg(isolated_theme.foreground) .flex_shrink_0(), ) }) .child( div() .text_sm() .font_medium() .when(!is_explicit, |this: Div| { this.text_color(isolated_theme.muted_foreground) }) .when(is_explicit, |this| { this.text_color(isolated_theme.foreground) }) .child(name.clone()), ), ) .child( h_flex() .gap_1() .items_center() .child( div() .text_sm() .text_color(isolated_theme.muted_foreground) .child(rgb_str.clone()), ) .child( div() .invisible() .group_hover(swatch_group, |this| this.visible()) .child( Clipboard::new(format!("copy-{}", name)) .value(rgb_str) .on_copied(move |value, window, cx| { window.push_notification( format!("Copied {} to clipboard", value), cx, ) }), ), ), ), ) } fn render_left_panel(&self, _: &mut Window, cx: &mut Context) -> Sidebar { let categories = &self.categories; let is_filtering = self.filter_by_value.is_some(); let entity_ref = cx.entity().clone(); let expand_all = Rc::new(cx.listener( |this: &mut Self, _: &ClickEvent, _: &mut Window, cx: &mut Context| { this.sidebar_render_key += 1; this.force_open_state = Some(true); cx.notify(); }, )); let collapse_all = Rc::new(cx.listener( |this: &mut Self, _: &ClickEvent, _: &mut Window, cx: &mut Context| { this.sidebar_render_key += 1; this.force_open_state = Some(false); cx.notify(); }, )); Sidebar::new(format!("color-theme-sidebar-{}", self.sidebar_render_key)) .w(px(300.)) .border_0() .header(Input::new(&self.filter_input).prefix(IconName::Search)) .child( SidebarMenu::new().children(categories.iter().enumerate().map( |( idx, ColorCategory { name: category_name, entries: colors, }, )| { let is_open = self.force_open_state.unwrap_or_else(|| idx == 0); SidebarMenuItem::new(category_name.clone()) .default_open(is_open) .click_to_open(true) .context_menu({ let expand_all = expand_all.clone(); let collapse_all = collapse_all.clone(); move |menu, _, _| { menu.item(PopupMenuItem::new("Expand All").on_click({ let expand_all = expand_all.clone(); move |ev, window, cx| (expand_all)(ev, window, cx) })) .item( PopupMenuItem::new("Collapse All").on_click({ let collapse_all = collapse_all.clone(); move |ev, window, cx| (collapse_all)(ev, window, cx) }), ) } }) .children(colors.iter().map(|entry| { let color_value = entry.color; let is_explicit = entry.is_explicit; let color_view = entity_ref.clone(); SidebarMenuItem::new(entry.name.clone()) .suffix(move |_, cx| { h_flex() .gap_2() .items_center() .when(!is_explicit, |this| { this.child( div() .size_1p5() .rounded_full() .bg(cx.theme().foreground), ) }) .child( div() .size_4() .rounded(cx.theme().radius.half()) .bg(color_value) .border_1() .border_color(cx.theme().border) .flex_shrink_0(), ) }) .context_menu(move |menu, _, _| { let menu_view = color_view.clone(); if is_filtering { menu.item( PopupMenuItem::new("Show All Values").on_click( move |_, _, cx| { menu_view.update(cx, |this, cx| { this.filter_by_value = None; this.compute_categories(cx); cx.notify(); }) }, ), ) } else { menu.item( PopupMenuItem::new("Filter By Value").on_click( move |_, _, cx| { menu_view.update(cx, |this, cx| { this.filter_by_value = Some(color_value); this.compute_categories(cx); cx.notify(); }) }, ), ) } }) })) }, )), ) } fn render_right_panel(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let (isolated_theme, is_dark) = self.get_isolated_theme(cx); let categories = self.categories.clone(); let categories_count = categories.len(); let list_state = window .use_keyed_state("color-theme-right-panel-list-state", cx, |_, _| { ListState::new(19, ListAlignment::Top, px(1000.)) }) .read(cx) .clone(); if list_state.item_count() != categories_count { list_state.reset(categories_count); } div() .border_1() .border_color(isolated_theme.border) .rounded(cx.theme().radius_lg) .size_full() .overflow_hidden() .child( Checkerboard::new(is_dark).child( v_flex() .size_full() .overflow_hidden() .rounded(cx.theme().radius_lg) .px_4() .child( list(list_state.clone(), { move |ix, _, cx| { let ColorCategory { name: category_name, entries: colors, } = categories[ix].clone(); let is_last = categories_count > 0 && ix == categories_count.saturating_sub(1); v_flex() .w_full() .gap_3() .pt_4() .when(is_last, |this| this.pb_4()) .child( div() .text_base() .font_semibold() .pb_2() .border_b_1() .border_color(isolated_theme.border) .text_color(isolated_theme.foreground) .child(category_name.clone()), ) .child(div().flex().flex_wrap().gap_4().children( colors.iter().map(|entry| { div().w(px(220.)).child(Self::render_color_swatch( entry.name.to_string(), entry.color, entry.hex.clone(), entry.is_explicit, &isolated_theme, cx, )) }), )) .into_any_element() } }) .size_full(), ) .vertical_scrollbar(&list_state), ), ) } } fn format_colors( theme: &ThemeColor, config: Option<&gpui_component::theme::ThemeConfigColors>, ) -> Vec { let json_theme = serde_json::to_value(theme).unwrap_or(serde_json::Value::Null); let mut categories: BTreeMap> = BTreeMap::new(); // Create a set of keys present in the config (if available) let config_keys: Option> = config.map(|c| { let json_config = serde_json::to_value(c).unwrap_or(serde_json::Value::Null); if let serde_json::Value::Object(map) = json_config { map.into_iter() .filter(|(_, v)| !v.is_null()) .map(|(k, _)| k) .collect() } else { std::collections::HashSet::new() } }); if let serde_json::Value::Object(map) = json_theme { for (key, value) in map { if let Ok(color) = serde_json::from_value::(value) { let parsed = super::mapper::parse_theme_key(&key); let category = parsed.category; let name = parsed.name; // Check if this key is explicit in the user config let is_explicit = config_keys .as_ref() .map_or(false, |k| k.contains(&parsed.canonical_key)); categories.entry(category).or_default().push(ColorEntry { name, color, hex: hsla_to_hex(color), is_explicit, }); } } } for colors in categories.values_mut() { colors.sort_by(|a, b| a.name.cmp(&b.name)); } let mut categories_vec: Vec<_> = categories .into_iter() .map(|(name, entries)| ColorCategory { name, entries }) .collect(); // Custom sort: Global first, then Primary, then others categories_vec.sort_by(|a, b| { let priority_order = [ "Global", "Primary", "Secondary", "Accent", "Base", "Background", "Foreground", "Structure", ]; let a_priority = priority_order.iter().position(|&x| x == a.name.as_str()); let b_priority = priority_order.iter().position(|&x| x == b.name.as_str()); match (a_priority, b_priority) { (Some(a_pos), Some(b_pos)) => a_pos.cmp(&b_pos), (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => a.name.cmp(&b.name), } }); categories_vec } fn hsla_to_hex(color: Hsla) -> String { let rgb = color.to_rgb(); if color.a < 1.0 { format!( "{:02x}{:02x}{:02x}{:02x}", (rgb.r * 255.0) as u8, (rgb.g * 255.0) as u8, (rgb.b * 255.0) as u8, (color.a * 255.0) as u8 ) } else { format!( "{:02x}{:02x}{:02x}", (rgb.r * 255.0) as u8, (rgb.g * 255.0) as u8, (rgb.b * 255.0) as u8 ) } } /// Compares two HSLA colors for equality at 8-bit precision. fn colors_equal_u8(c1: Hsla, c2: Hsla) -> bool { let rgb1 = c1.to_rgb(); let rgb2 = c2.to_rgb(); let eq = |a: f32, b: f32| (a * 255.0).round() as u8 == (b * 255.0).round() as u8; eq(rgb1.r, rgb2.r) && eq(rgb1.g, rgb2.g) && eq(rgb1.b, rgb2.b) && eq(c1.a, c2.a) } /// Filters categories by a predicate on color entries, removing empty categories. fn filter_categories( categories: Vec, predicate: impl Fn(&ColorEntry) -> bool, ) -> Vec { categories .into_iter() .filter_map( |ColorCategory { name: category, entries: colors, }| { let filtered: Vec<_> = colors.into_iter().filter(|e| predicate(e)).collect(); if filtered.is_empty() { None } else { Some(ColorCategory { name: category, entries: filtered, }) } }, ) .collect() } impl Render for ThemeColorsStory { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .size_full() .overflow_hidden() .child( // Theme selector at the top h_flex() .gap_x_3() .child(div().w(px(300.)).child(Select::new(&self.select_state))) .child( Button::new("set_theme") .primary() .label("Set Theme") .on_click(cx.listener(|this, _, window, cx| { use gpui_component::{Theme, ThemeRegistry}; let registry = ThemeRegistry::global(cx); if let Some(theme_config) = registry.themes().get(&this.selected_theme_name).cloned() { let mode = theme_config.mode; let theme = Theme::global_mut(cx); if mode.is_dark() { theme.dark_theme = theme_config; } else { theme.light_theme = theme_config; } Theme::change(mode, None, cx); cx.refresh_windows(); // Refresh the select items to update the active checkmark let active_theme_name = cx.theme().theme_name().clone(); let themes = ThemeRegistry::global(cx).sorted_themes(); // Re-create items with new active state let mut items: Vec = themes .iter() .map(|theme| { ThemeItem::new( theme.name.clone(), // Note: we need to handle case sensitivity if names differ, // but usually accurate. theme.name == active_theme_name, ) }) .collect(); // Sort again to be safe/consistent items.sort_by(|a, b| { a.name.to_lowercase().cmp(&b.name.to_lowercase()) }); // Update the select state this.select_state.update(cx, |state, cx| { state.set_items(items, window, cx); }); } })), ) .child( Switch::new("show_all_colors") .checked(self.show_all_colors) .label("Show Inherited Colors") .on_click(cx.listener(|this, checked: &bool, _window, cx| { this.show_all_colors = *checked; this.compute_categories(cx); cx.notify(); })), ) .child( Switch::new("expand_collapse_switch") .checked(self.force_open_state == Some(true)) .label(if self.force_open_state == Some(true) { "Collapse All" } else { "Expand All" }) .on_click(cx.listener(|this, checked: &bool, _window, cx| { this.sidebar_render_key += 1; this.force_open_state = Some(*checked); cx.notify(); })), ), ) .child( h_flex() .flex_1() .items_start() .gap_4() .child(self.render_left_panel(window, cx)) .child(self.render_right_panel(window, cx)), ) } } ================================================ FILE: crates/story/src/stories/theme_story/mapper.rs ================================================ /// A compatibility bridge for mapping theme color keys. /// /// This module provides a way to translate between the legacy snake_case keys /// used in `ThemeColor` and the logical categories/names expected by the /// Color Theme Viewer. /// /// ### How to eliminate this mapper /// /// If the project decides to unify the theme schema project-wide (using dot-notation), /// follow these steps to remove this "temporary bridge": /// /// 1. **Update `ThemeColor`**: /// In `crates/ui/src/theme/theme_color.rs`, add `#[serde(rename = "...")]` attributes /// to all fields of the `ThemeColor` struct to match their canonical dot-notation /// names (e.g., `accent_foreground` -> `#[serde(rename = "accent.foreground")]`). /// /// 2. **Update JSON Themes**: /// Ensure `crates/ui/src/theme/default-theme.json` and any files in `themes/` /// strictly use the dot-notation keys. /// /// 3. **Refactor the Viewer**: /// In `crates/story/src/stories/theme_story/color_theme_story.rs`, remove the call /// to `super::mapper::parse_theme_key` and replace it with a simple split on the '.' character. /// /// 4. **Delete this file**: /// Remove `mapper.rs` and its module declaration in `mod.rs`. /// /// Represents a parsed theme key with a category, a display name, and a canonical dot-notation key. pub struct ParsedKey { pub category: String, pub name: String, pub canonical_key: String, } /// Parses a theme key (either snake_case or dot-notation) into a logical category and name. pub fn parse_theme_key(key: &str) -> ParsedKey { // 1. Check for dot-notation (e.g., "accent.background") if key.contains('.') { let parts: Vec<&str> = key.splitn(2, '.').collect(); return ParsedKey { category: to_title_case_full(parts[0]), name: to_title_case_full(parts[1]), canonical_key: key.to_string(), }; } // 2. Handle legacy snake_case remapping (e.g., "accent_foreground" -> "Accent" / "Foreground") // This list attempts to reconstruct the hierarchy from the flat ThemeColor struct. let (category, name, canonical) = match key { // Accent "accent" => ("Accent", "Background", "accent.background"), "accent_foreground" => ("Accent", "Foreground", "accent.foreground"), // Primary "primary" => ("Primary", "Background", "primary.background"), "primary_active" => ("Primary", "Active Background", "primary.active.background"), "primary_foreground" => ("Primary", "Foreground", "primary.foreground"), "primary_hover" => ("Primary", "Hover Background", "primary.hover.background"), // Secondary "secondary" => ("Secondary", "Background", "secondary.background"), "secondary_active" => ( "Secondary", "Active Background", "secondary.active.background", ), "secondary_foreground" => ("Secondary", "Foreground", "secondary.foreground"), "secondary_hover" => ( "Secondary", "Hover Background", "secondary.hover.background", ), // Sidebar "sidebar" => ("Sidebar", "Background", "sidebar.background"), "sidebar_accent" => ("Sidebar", "Accent Background", "sidebar.accent.background"), "sidebar_accent_foreground" => { ("Sidebar", "Accent Foreground", "sidebar.accent.foreground") } "sidebar_border" => ("Sidebar", "Border", "sidebar.border"), "sidebar_foreground" => ("Sidebar", "Foreground", "sidebar.foreground"), "sidebar_primary" => ( "Sidebar", "Primary Background", "sidebar.primary.background", ), "sidebar_primary_foreground" => ( "Sidebar", "Primary Foreground", "sidebar.primary.foreground", ), // List "list" => ("List", "Background", "list.background"), "list_active" => ("List", "Active Background", "list.active.background"), "list_active_border" => ("List", "Active Border", "list.active.border"), "list_even" => ("List", "Even Background", "list.even.background"), "list_head" => ("List", "Head Background", "list.head.background"), "list_hover" => ("List", "Hover Background", "list.hover.background"), // Table "table" => ("Table", "Background", "table.background"), "table_active" => ("Table", "Active Background", "table.active.background"), "table_active_border" => ("Table", "Active Border", "table.active.border"), "table_even" => ("Table", "Even Background", "table.even.background"), "table_head" => ("Table", "Head Background", "table.head.background"), "table_head_foreground" => ("Table", "Head Foreground", "table.head.foreground"), "table_hover" => ("Table", "Hover Background", "table.hover.background"), "table_row_border" => ("Table", "Row Border", "table.row.border"), // Tabs "tab" => ("Tab", "Background", "tab.background"), "tab_active" => ("Tab", "Active Background", "tab.active.background"), "tab_active_foreground" => ("Tab", "Active Foreground", "tab.active.foreground"), "tab_bar" => ("Tab Bar", "Background", "tab_bar.background"), "tab_bar_segmented" => ( "Tab Bar", "Segmented Background", "tab_bar.segmented.background", ), "tab_foreground" => ("Tab", "Foreground", "tab.foreground"), // Input "input" => ("Input", "Border", "input.border"), "caret" => ("Input", "Caret", "caret"), "selection" => ("Input", "Selection", "selection.background"), // Slider / Switch "slider_bar" => ("Slider", "Bar", "slider.background"), "slider_thumb" => ("Slider", "Thumb", "slider.thumb.background"), "switch" => ("Switch", "Background", "switch.background"), "switch_thumb" => ("Switch", "Thumb", "switch.thumb.background"), // Muted / Skeleton "muted" => ("Muted", "Background", "muted.background"), "muted_foreground" => ("Muted", "Foreground", "muted.foreground"), "skeleton" => ("Skeleton", "Background", "skeleton.background"), // Charts "chart_1" => ("Chart", "Color 1", "chart.1"), "chart_2" => ("Chart", "Color 2", "chart.2"), "chart_3" => ("Chart", "Color 3", "chart.3"), "chart_4" => ("Chart", "Color 4", "chart.4"), "chart_5" => ("Chart", "Color 5", "chart.5"), // Danger / Success / Warning / Info "danger" => ("Danger", "Background", "danger.background"), "danger_active" => ("Danger", "Active", "danger.active.background"), "danger_foreground" => ("Danger", "Foreground", "danger.foreground"), "danger_hover" => ("Danger", "Hover", "danger.hover.background"), "success" => ("Success", "Background", "success.background"), "success_active" => ("Success", "Active", "success.active.background"), "success_foreground" => ("Success", "Foreground", "success.foreground"), "success_hover" => ("Success", "Hover", "success.hover.background"), "warning" => ("Warning", "Background", "warning.background"), "warning_active" => ("Warning", "Active", "warning.active.background"), "warning_foreground" => ("Warning", "Foreground", "warning.foreground"), "warning_hover" => ("Warning", "Hover", "warning.hover.background"), "info" => ("Info", "Background", "info.background"), "info_active" => ("Info", "Active", "info.active.background"), "info_foreground" => ("Info", "Foreground", "info.foreground"), "info_hover" => ("Info", "Hover", "info.hover.background"), // Base Colors "red" => ("Base", "Red", "base.red"), "red_light" => ("Base", "Red Light", "base.red.light"), "green" => ("Base", "Green", "base.green"), "green_light" => ("Base", "Green Light", "base.green.light"), "blue" => ("Base", "Blue", "base.blue"), "blue_light" => ("Base", "Blue Light", "base.blue.light"), "yellow" => ("Base", "Yellow", "base.yellow"), "yellow_light" => ("Base", "Yellow Light", "base.yellow.light"), "magenta" => ("Base", "Magenta", "base.magenta"), "magenta_light" => ("Base", "Magenta Light", "base.magenta.light"), "cyan" => ("Base", "Cyan", "base.cyan"), "cyan_light" => ("Base", "Cyan Light", "base.cyan.light"), // Everything else remains in Global or attempts a split _ => { if key.contains('_') { let parts: Vec<&str> = key.splitn(2, '_').collect(); (parts[0], parts[1], key) } else { ("Global", key, key) } } }; ParsedKey { category: to_title_case_full(category), name: to_title_case_full(name), canonical_key: canonical.to_string(), } } fn to_title_case(s: &str) -> String { let mut c = s.chars(); match c.next() { None => String::new(), Some(f) => f.to_uppercase().collect::() + c.as_str(), } } fn to_title_case_full(s: &str) -> String { s.split(|c| c == '_' || c == '.') .map(to_title_case) .collect::>() .join(" ") } ================================================ FILE: crates/story/src/stories/theme_story/mod.rs ================================================ mod checkerboard; mod color_theme_story; mod mapper; pub use color_theme_story::*; ================================================ FILE: crates/story/src/stories/toggle_story.rs ================================================ use gpui::{ App, AppContext as _, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement as _, Render, Styled as _, Window, }; use gpui_component::{ IconName, Sizable, StyledExt, button::{Toggle, ToggleGroup, ToggleVariants}, v_flex, }; use crate::section; pub struct ToggleStory { focus_handle: FocusHandle, single_toggle: usize, checked: Vec, } impl ToggleStory { pub fn view(_: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self { focus_handle: cx.focus_handle(), single_toggle: 0, checked: vec![false; 20], }) } } impl super::Story for ToggleStory { fn title() -> &'static str { "ToggleButton" } fn description() -> &'static str { "" } fn closable() -> bool { false } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl Focusable for ToggleStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for ToggleStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .w_full() .gap_3() .child( section("Toggle") .child( Toggle::new("item1") .label("Single Toggle Item 1") .large() .checked(self.single_toggle == 1) .on_click(cx.listener(|view, checked, _, cx| { if *checked { view.single_toggle = 1; } cx.notify(); })), ) .child( Toggle::new("item2") .label("Single Toggle Item 2") .large() .checked(self.single_toggle == 2) .on_click(cx.listener(|view, checked, _, cx| { if *checked { view.single_toggle = 2; } cx.notify(); })), ) .child( Toggle::new("item3") .icon(IconName::Eye) .large() .checked(self.single_toggle == 3) .on_click(cx.listener(|view, checked, _, cx| { if *checked { view.single_toggle = 3; } cx.notify(); })), ), ) .child( section("Toggle Group with Ghost Style") .v_flex() .gap_4() .child( ToggleGroup::new("toggle-button-group1") .child(Toggle::new(0).icon(IconName::Bell).checked(self.checked[0])) .child(Toggle::new(1).icon(IconName::Bot).checked(self.checked[1])) .child( Toggle::new(2) .icon(IconName::Inbox) .checked(self.checked[2]), ) .child( Toggle::new(3) .icon(IconName::Check) .checked(self.checked[3]), ) .child(Toggle::new(4).label("Other").checked(self.checked[4])) .on_click(cx.listener(|view, checkeds: &Vec, _, cx| { view.checked[0] = checkeds[0]; view.checked[1] = checkeds[1]; view.checked[2] = checkeds[2]; view.checked[3] = checkeds[3]; view.checked[4] = checkeds[4]; cx.notify(); })), ) .child( ToggleGroup::new("toggle-button-group1-sm") .small() .child(Toggle::new(0).icon(IconName::Bell).checked(self.checked[0])) .child(Toggle::new(1).icon(IconName::Bot).checked(self.checked[1])) .child( Toggle::new(2) .icon(IconName::Inbox) .checked(self.checked[2]), ) .child( Toggle::new(3) .icon(IconName::Check) .checked(self.checked[3]), ) .child(Toggle::new(4).label("Other").checked(self.checked[4])) .on_click(cx.listener(|view, checkeds: &Vec, _, cx| { view.checked[0] = checkeds[0]; view.checked[1] = checkeds[1]; view.checked[2] = checkeds[2]; view.checked[3] = checkeds[3]; view.checked[4] = checkeds[4]; cx.notify(); })), ) .child( ToggleGroup::new("toggle-button-group1-xs") .xsmall() .child(Toggle::new(0).icon(IconName::Bell).checked(self.checked[0])) .child(Toggle::new(1).icon(IconName::Bot).checked(self.checked[1])) .child( Toggle::new(2) .icon(IconName::Inbox) .checked(self.checked[2]), ) .child( Toggle::new(3) .icon(IconName::Check) .checked(self.checked[3]), ) .child(Toggle::new(4).label("Other").checked(self.checked[4])) .on_click(cx.listener(|view, checkeds: &Vec, _, cx| { view.checked[0] = checkeds[0]; view.checked[1] = checkeds[1]; view.checked[2] = checkeds[2]; view.checked[3] = checkeds[3]; view.checked[4] = checkeds[4]; cx.notify(); })), ), ) .child( section("Toggle Group with Outline Style") .v_flex() .gap_4() .child( ToggleGroup::new("toggle-button-group2") .outline() .child(Toggle::new(0).icon(IconName::Bell).checked(self.checked[0])) .child(Toggle::new(1).icon(IconName::Bot).checked(self.checked[1])) .child( Toggle::new(2) .icon(IconName::Inbox) .checked(self.checked[2]), ) .child( Toggle::new(3) .icon(IconName::Check) .checked(self.checked[3]), ) .child(Toggle::new(4).label("Other").checked(self.checked[4])) .on_click(cx.listener(|view, checkeds: &Vec, _, cx| { view.checked[0] = checkeds[0]; view.checked[1] = checkeds[1]; view.checked[2] = checkeds[2]; view.checked[3] = checkeds[3]; view.checked[4] = checkeds[4]; cx.notify(); })), ) .child( ToggleGroup::new("toggle-button-group2-sm") .outline() .small() .child(Toggle::new(0).icon(IconName::Bell).checked(self.checked[0])) .child(Toggle::new(1).icon(IconName::Bot).checked(self.checked[1])) .child( Toggle::new(2) .icon(IconName::Inbox) .checked(self.checked[2]), ) .child( Toggle::new(3) .icon(IconName::Check) .checked(self.checked[3]), ) .child(Toggle::new(4).label("Other").checked(self.checked[4])) .on_click(cx.listener(|view, checkeds: &Vec, _, cx| { view.checked[0] = checkeds[0]; view.checked[1] = checkeds[1]; view.checked[2] = checkeds[2]; view.checked[3] = checkeds[3]; view.checked[4] = checkeds[4]; cx.notify(); })), ) .child( ToggleGroup::new("toggle-button-group2-xs") .outline() .xsmall() .child(Toggle::new(0).icon(IconName::Bell).checked(self.checked[0])) .child(Toggle::new(1).icon(IconName::Bot).checked(self.checked[1])) .child( Toggle::new(2) .icon(IconName::Inbox) .checked(self.checked[2]), ) .child( Toggle::new(3) .icon(IconName::Check) .checked(self.checked[3]), ) .child(Toggle::new(4).label("Other").checked(self.checked[4])) .on_click(cx.listener(|view, checkeds: &Vec, _, cx| { view.checked[0] = checkeds[0]; view.checked[1] = checkeds[1]; view.checked[2] = checkeds[2]; view.checked[3] = checkeds[3]; view.checked[4] = checkeds[4]; cx.notify(); })), ), ) } } ================================================ FILE: crates/story/src/stories/tooltip_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, Focusable, InteractiveElement, KeyBinding, ParentElement, Render, StatefulInteractiveElement, Styled, Window, actions, div, }; use gpui_component::{ ActiveTheme, IconName, button::{Button, ButtonVariant, ButtonVariants}, checkbox::Checkbox, dock::PanelControl, h_flex, radio::Radio, switch::Switch, tooltip::Tooltip, v_flex, }; use crate::{Story, section}; actions!(tooltip_story, [Info]); pub fn init(cx: &mut App) { cx.bind_keys([KeyBinding::new("ctrl-shift-delete", Info, Some("Tooltip"))]); } pub struct TooltipStory { focus_handle: gpui::FocusHandle, } impl TooltipStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), } } } impl Story for TooltipStory { fn title() -> &'static str { "Tooltip" } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn zoomable() -> Option { None } } impl Focusable for TooltipStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for TooltipStory { fn render( &mut self, _: &mut gpui::Window, _cx: &mut gpui::Context, ) -> impl gpui::IntoElement { v_flex() .w_full() .gap_3() .child( section("Tooltip for Button") .child( Button::new("btn0") .label("Search") .with_variant(ButtonVariant::Primary) .tooltip("This is a search Button."), ) .child(Button::new("btn1").label("Info").tooltip_with_action( "This is a tooltip with Action for display keybinding.", &Info, Some("Tooltip"), )) .child( div() .child(Button::new("btn3").label("Hover me")) .id("tooltip-4") .tooltip(|window, cx| { Tooltip::element(|_, cx| { h_flex() .gap_x_1() .child(IconName::Info) .child( div() .child("Muted Foreground") .text_color(cx.theme().muted_foreground), ) .child(div().child("Danger").text_color(cx.theme().danger)) .child(IconName::ArrowUp) }) .build(window, cx) }), ), ) .child( section("Label Tooltip").child(div().child("Hover me").id("tooltip-2").tooltip( |window, cx| { Tooltip::new("This is a Label") .action(&Info, Some("Tooltip")) .build(window, cx) }, )), ) .child( section("Checkbox Tooltip").child( Checkbox::new("check") .label("Remember me") .checked(true) .tooltip(|window, cx| Tooltip::new("This is a checkbox").build(window, cx)), ), ) .child( section("Radio Tooltip").child( Radio::new("radio") .label("Radio with tooltip") .checked(true) .tooltip(|window, cx| { Tooltip::new("This is a radio button").build(window, cx) }), ), ) .child( section("Switch Tooltip").child( Switch::new("switch") .checked(true) .tooltip("This is a switch"), ), ) } } ================================================ FILE: crates/story/src/stories/tree_story.rs ================================================ use std::path::PathBuf; use autocorrect::ignorer::Ignorer; use gpui::{ App, AppContext, Context, Entity, InteractiveElement, KeyBinding, ParentElement, Render, Styled, Window, actions, px, }; use gpui_component::{ ActiveTheme as _, IconName, StyledExt as _, button::Button, dock::PanelControl, h_flex, label::Label, list::ListItem, tree::{TreeItem, TreeState, tree}, v_flex, }; use rand::seq::SliceRandom as _; use crate::{Story, section}; actions!(story, [Rename]); const CONTEXT: &str = "TreeStory"; pub(crate) fn init(cx: &mut App) { cx.bind_keys([KeyBinding::new("enter", Rename, Some(CONTEXT))]); } pub struct TreeStory { tree_state: Entity, items: Vec, } fn build_file_items(ignorer: &Ignorer, root: &PathBuf, path: &PathBuf) -> Vec { let mut items = Vec::new(); if let Ok(entries) = std::fs::read_dir(path) { for entry in entries.flatten() { let path = entry.path(); let relative_path = path.strip_prefix(root).unwrap_or(&path); if ignorer.is_ignored(&relative_path.to_string_lossy()) || relative_path.ends_with(".git") { continue; } let file_name = path .file_name() .and_then(|n| n.to_str()) .unwrap_or("Unknown") .to_string(); let id = path.to_string_lossy().to_string(); if path.is_dir() { let children = build_file_items(ignorer, &root, &path); items.push(TreeItem::new(id, file_name).children(children)); } else { items.push(TreeItem::new(id, file_name)); } } } items.sort_by(|a, b| { b.is_folder() .cmp(&a.is_folder()) .then(a.label.cmp(&b.label)) }); items } impl TreeStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn load_files(state: Entity, path: PathBuf, cx: &mut Context) { cx.spawn(async move |weak_self, cx| { let ignorer = Ignorer::new(&path.to_string_lossy()); let items = build_file_items(&ignorer, &path, &path); _ = state.update(cx, |state, cx| { state.set_items(items.clone(), cx); }); _ = weak_self.update(cx, |this, cx| { this.items = items; cx.notify(); }) }) .detach(); } fn new(_: &mut Window, cx: &mut Context) -> Self { let tree_state = cx.new(|cx| TreeState::new(cx)); Self::load_files(tree_state.clone(), PathBuf::from("./"), cx); Self { tree_state, items: Vec::new(), } } fn on_action_rename(&mut self, _: &Rename, _: &mut Window, cx: &mut gpui::Context) { if let Some(entry) = self.tree_state.read(cx).selected_entry() { let item = entry.item(); println!("Renaming item: {} ({})", item.label, item.id); // Here you could implement actual renaming logic } } } impl Story for TreeStory { fn title() -> &'static str { "Tree" } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn zoomable() -> Option { None } } impl Render for TreeStory { fn render( &mut self, _: &mut gpui::Window, cx: &mut gpui::Context, ) -> impl gpui::IntoElement { let view = cx.entity(); v_flex() .w_full() .gap_3() .id("tree-story") .key_context(CONTEXT) .on_action(cx.listener(Self::on_action_rename)) .child( h_flex().gap_3().child( Button::new("select-item") .outline() .label("Select Item") .on_click(cx.listener(|this, _, _, cx| { if let Some(random_item) = this.items.choose(&mut rand::thread_rng()) { this.tree_state.update(cx, |state, cx| { state.set_selected_item(Some(random_item), cx); }); } })), ), ) .child( section("File tree") .sub_title("Press `space` to select, `enter` to rename.") .v_flex() .max_w_md() .child( tree( &self.tree_state, move |ix, entry, _selected, _window, cx| { view.update(cx, |_, cx| { let item = entry.item(); let icon = if !entry.is_folder() { IconName::File } else if entry.is_expanded() { IconName::FolderOpen } else { IconName::Folder }; ListItem::new(ix) .w_full() .rounded(cx.theme().radius) .px_3() .pl(px(16.) * entry.depth() + px(12.)) .child( h_flex().gap_2().child(icon).child(item.label.clone()), ) .on_click(cx.listener({ let item = item.clone(); move |_, _, _window, _| { println!( "Clicked on item: {} ({})", item.label, item.id ); } })) }) }, ) .p_1() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .h(px(540.)), ) .child( h_flex() .w_full() .justify_between() .gap_3() .children( self.tree_state .read(cx) .selected_index() .map(|ix| format!("Selected Index: {}", ix)), ) .children( self.tree_state .read(cx) .selected_item() .map(|item| Label::new("Selected:").secondary(item.id.clone())), ), ), ) } } ================================================ FILE: crates/story/src/stories/virtual_list_story.rs ================================================ use std::{ops::Range, rc::Rc}; use gpui::{ App, AppContext, Context, Div, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Render, ScrollStrategy, Size, Styled, Window, div, px, size, }; use gpui_component::{ ActiveTheme as _, Selectable, Sizable, VirtualListScrollHandle, button::{Button, ButtonGroup}, divider::Divider, h_flex, scroll::{ScrollableElement, ScrollbarAxis}, v_flex, v_virtual_list, }; pub struct VirtualListStory { focus_handle: FocusHandle, scroll_handle: VirtualListScrollHandle, items: Vec, item_sizes: Rc>>, columns_count: usize, axis: ScrollbarAxis, size_mode: usize, visible_range: Range, } const ITEM_SIZE: Size = size(px(100.), px(30.)); impl VirtualListStory { fn new(_: &mut Window, cx: &mut Context) -> Self { let items = (0..5000).map(|i| format!("Item {}", i)).collect::>(); let item_sizes = items.iter().map(|_| ITEM_SIZE).collect::>(); Self { focus_handle: cx.focus_handle(), scroll_handle: VirtualListScrollHandle::new(), items, item_sizes: Rc::new(item_sizes), columns_count: 100, axis: ScrollbarAxis::Both, size_mode: 0, visible_range: (0..0), } } pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } pub fn change_test_cases(&mut self, n: usize, cx: &mut Context) { self.size_mode = n; if n == 0 { self.items = (0..5000).map(|i| format!("Item {}", i)).collect::>(); self.columns_count = 30; } else if n == 1 { self.items = (0..100).map(|i| format!("Item {}", i)).collect::>(); self.columns_count = 100; } else if n == 2 { self.items = (0..500000) .map(|i| format!("Item {}", i)) .collect::>(); self.columns_count = 100; } else { self.items = (0..5).map(|i| format!("Item {}", i)).collect::>(); self.columns_count = 10; } self.item_sizes = Rc::new(self.items.iter().map(|_| ITEM_SIZE).collect()); cx.notify(); } pub fn change_axis(&mut self, axis: ScrollbarAxis, cx: &mut Context) { self.axis = axis; cx.notify(); } fn render_buttons(&mut self, cx: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child( h_flex() .gap_2() .justify_between() .child( h_flex() .gap_2() .child( ButtonGroup::new("test-cases") .outline() .compact() .child( Button::new("test-0") .label("Size 0") .selected(self.size_mode == 0), ) .child( Button::new("test-1") .label("Size 1") .selected(self.size_mode == 1), ) .child( Button::new("test-2") .label("Size 2") .selected(self.size_mode == 2), ) .child( Button::new("test-3") .label("Size 3") .selected(self.size_mode == 3), ) .on_click(cx.listener(|view, clicks: &Vec, _, cx| { if clicks.contains(&0) { view.change_test_cases(0, cx) } else if clicks.contains(&1) { view.change_test_cases(1, cx) } else if clicks.contains(&2) { view.change_test_cases(2, cx) } else if clicks.contains(&3) { view.change_test_cases(3, cx) } })), ) .child(Divider::vertical().px_2()) .child( ButtonGroup::new("scrollbars") .outline() .compact() .child( Button::new("test-axis-both") .label("Both Scrollbar") .selected(self.axis.is_both()), ) .child( Button::new("test-axis-vertical") .label("Vertical") .selected(self.axis.is_vertical()), ) .child( Button::new("test-axis-horizontal") .label("Horizontal") .selected(self.axis.is_horizontal()), ) .on_click(cx.listener(|view, clicks: &Vec, _, cx| { if clicks.contains(&0) { view.change_axis(ScrollbarAxis::Both, cx) } else if clicks.contains(&1) { view.change_axis(ScrollbarAxis::Vertical, cx) } else if clicks.contains(&2) { view.change_axis(ScrollbarAxis::Horizontal, cx) } })), ), ) .child(format!("visible_range: {:?}", self.visible_range)), ) .child( h_flex() .gap_2() .child( Button::new("scroll-to0") .small() .outline() .label("Scroll to Top") .on_click(cx.listener(|this, _, _, cx| { this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top); cx.notify(); })), ) .child( Button::new("scroll-to1") .small() .outline() .label("Scroll to 50") .on_click(cx.listener(|this, _, _, cx| { this.scroll_handle.scroll_to_item(50, ScrollStrategy::Top); cx.notify(); })), ) .child( Button::new("scroll-to2") .small() .outline() .label("Scroll to 25 (center)") .on_click(cx.listener(|this, _, _, cx| { this.scroll_handle .scroll_to_item(25, ScrollStrategy::Center); cx.notify(); })), ) .child( Button::new("scroll-to-bottom") .small() .outline() .label("Scroll to Bottom") .on_click(cx.listener(|this, _, _, cx| { this.scroll_handle.scroll_to_bottom(); cx.notify(); })), ), ) } } impl super::Story for VirtualListStory { fn title() -> &'static str { "VirtualList" } fn description() -> &'static str { "Add vertical or horizontal, or both scrollbars to a container, \ and use `virtual_list` to render a large number of items." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } } impl Focusable for VirtualListStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for VirtualListStory { fn render( &mut self, _: &mut gpui::Window, cx: &mut gpui::Context, ) -> impl gpui::IntoElement { let columns_count = self.columns_count; fn render_item(cx: &App) -> Div { div() .flex() .h_full() .items_center() .justify_center() .text_sm() .w(ITEM_SIZE.width) .h(ITEM_SIZE.height) .bg(cx.theme().secondary) } v_flex() .size_full() .gap_4() .child(self.render_buttons(cx)) .child( div().w_full().flex_1().min_h_64().child( div().relative().size_full().child( v_flex() .id("list") .relative() .size_full() .child( v_virtual_list( cx.entity().clone(), "items", self.item_sizes.clone(), move |story, visible_range, _, cx| { story.visible_range = visible_range.clone(); visible_range .map(|ix| { h_flex().gap_1().items_center().children( (0..columns_count).map(|i| { render_item(cx).child(if i == 0 { format!("row: {}", ix) } else { format!("{}", i) }) }), ) }) .collect() }, ) .track_scroll(&self.scroll_handle) .p_4() .border_1() .border_color(cx.theme().border) .gap_1(), ) .scrollbar(&self.scroll_handle, self.axis), ), ), ) } } ================================================ FILE: crates/story/src/stories/welcome_story.rs ================================================ use gpui::{ App, AppContext, Context, Entity, FocusHandle, Focusable, Render, Styled as _, Window, px, }; use gpui_component::{dock::PanelControl, text::markdown}; use crate::Story; pub struct WelcomeStory { focus_handle: FocusHandle, } impl WelcomeStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), } } } impl Story for WelcomeStory { fn title() -> &'static str { "Introduction" } fn description() -> &'static str { "UI components for building fantastic desktop application by using GPUI." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn zoomable() -> Option { None } fn paddings() -> gpui::Pixels { px(0.) } } impl Focusable for WelcomeStory { fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for WelcomeStory { fn render( &mut self, _: &mut gpui::Window, _: &mut gpui::Context, ) -> impl gpui::IntoElement { markdown(include_str!("../../../../README.md")) .px_4() .scrollable(true) .selectable(true) } } ================================================ FILE: crates/story/src/themes.rs ================================================ use gpui::{Action, App, SharedString}; use gpui_component::{Theme, ThemeMode, ThemeRegistry, scroll::ScrollbarShow}; use serde::{Deserialize, Serialize}; #[cfg(not(target_family = "wasm"))] use gpui_component::ActiveTheme; #[cfg(target_family = "wasm")] use crate::embedded_themes; const STATE_FILE: &str = "target/state.json"; #[derive(Debug, Clone, Serialize, Deserialize)] struct State { theme: SharedString, scrollbar_show: Option, } impl Default for State { fn default() -> Self { Self { theme: "Default Light".into(), scrollbar_show: None, } } } pub fn init(cx: &mut App) { #[cfg(target_family = "wasm")] { tracing::info!("Loading embedded themes for WASM..."); let embedded = embedded_themes::embedded_themes(); let registry = ThemeRegistry::global_mut(cx); for (name, content) in embedded { if let Err(e) = registry.load_themes_from_str(content) { tracing::error!("Failed to load embedded theme {}: {}", name, e); } else { tracing::info!("Loaded embedded theme: {}", name); } } } let state = if cfg!(not(target_family = "wasm")) { let json = std::fs::read_to_string(STATE_FILE).unwrap_or(String::default()); serde_json::from_str::(&json).unwrap_or_default() } else { State::default() }; #[cfg(not(target_family = "wasm"))] if let Err(err) = ThemeRegistry::watch_dir(std::path::PathBuf::from("./themes"), cx, move |cx| { if let Some(theme) = ThemeRegistry::global(cx) .themes() .get(&state.theme) .cloned() { Theme::global_mut(cx).apply_config(&theme); } }) { tracing::error!("Failed to watch themes directory: {}", err); } if let Some(scrollbar_show) = state.scrollbar_show { Theme::global_mut(cx).scrollbar_show = scrollbar_show; } cx.refresh_windows(); #[cfg(not(target_family = "wasm"))] cx.observe_global::(|cx| { let state = State { theme: cx.theme().theme_name().clone(), scrollbar_show: Some(cx.theme().scrollbar_show), }; if let Ok(json) = serde_json::to_string_pretty(&state) { // Ignore write errors - if STATE_FILE doesn't exist or can't be written, do nothing let _ = std::fs::write(STATE_FILE, json); } }) .detach(); cx.on_action(|switch: &SwitchTheme, cx| { let theme_name = switch.0.clone(); if let Some(theme_config) = ThemeRegistry::global(cx).themes().get(&theme_name).cloned() { Theme::global_mut(cx).apply_config(&theme_config); } cx.refresh_windows(); }); cx.on_action(|switch: &SwitchThemeMode, cx| { let mode = switch.0; Theme::change(mode, None, cx); cx.refresh_windows(); }); } #[derive(Action, Clone, PartialEq)] #[action(namespace = themes, no_json)] pub(crate) struct SwitchTheme(pub(crate) SharedString); #[derive(Action, Clone, PartialEq)] #[action(namespace = themes, no_json)] pub(crate) struct SwitchThemeMode(pub(crate) ThemeMode); ================================================ FILE: crates/story/src/title_bar.rs ================================================ use std::rc::Rc; use gpui::{ AnyElement, App, AppContext, Context, Corner, Entity, FocusHandle, InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Render, SharedString, Styled as _, Subscription, Window, div, px, }; use gpui_component::{ ActiveTheme as _, IconName, Side, Sizable as _, Theme, TitleBar, WindowExt as _, badge::Badge, button::{Button, ButtonVariants as _}, label::Label, menu::{AppMenuBar, DropdownMenu as _}, scroll::ScrollbarShow, }; use crate::{SelectFont, SelectRadius, SelectScrollbarShow, ToggleListActiveHighlight, app_menus}; pub struct AppTitleBar { app_menu_bar: Entity, font_size_selector: Entity, child: Rc AnyElement>, _subscriptions: Vec, } impl AppTitleBar { pub fn new( title: impl Into, window: &mut Window, cx: &mut Context, ) -> Self { let app_menu_bar = app_menus::init(title, cx); let font_size_selector = cx.new(|cx| FontSizeSelector::new(window, cx)); Self { app_menu_bar, font_size_selector, child: Rc::new(|_, _| div().into_any_element()), _subscriptions: vec![], } } pub fn child(mut self, f: F) -> Self where E: IntoElement, F: Fn(&mut Window, &mut App) -> E + 'static, { self.child = Rc::new(move |window, cx| f(window, cx).into_any_element()); self } } impl Render for AppTitleBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let notifications_count = window.notifications(cx).len(); TitleBar::new() // left side .child(div().flex().items_center().child(self.app_menu_bar.clone())) .child( div() .flex() .items_center() .justify_end() .px_2() .gap_2() .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .child((self.child.clone())(window, cx)) .child( Label::new("theme:") .secondary(cx.theme().theme_name()) .text_sm(), ) .child(self.font_size_selector.clone()) .child( Button::new("github") .icon(IconName::Github) .small() .ghost() .on_click(|_, _, cx| { cx.open_url("https://github.com/longbridge/gpui-component") }), ) .child( div().relative().child( Badge::new().count(notifications_count).max(99).child( Button::new("bell") .small() .ghost() .compact() .icon(IconName::Bell), ), ), ), ) } } struct FontSizeSelector { focus_handle: FocusHandle, } impl FontSizeSelector { pub fn new(_: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), } } fn on_select_font( &mut self, font_size: &SelectFont, window: &mut Window, cx: &mut Context, ) { Theme::global_mut(cx).font_size = px(font_size.0 as f32); window.refresh(); } fn on_select_radius( &mut self, radius: &SelectRadius, window: &mut Window, cx: &mut Context, ) { Theme::global_mut(cx).radius = px(radius.0 as f32); Theme::global_mut(cx).radius_lg = if cx.theme().radius > px(0.) { cx.theme().radius + px(2.) } else { px(0.) }; window.refresh(); } fn on_select_scrollbar_show( &mut self, show: &SelectScrollbarShow, window: &mut Window, cx: &mut Context, ) { Theme::global_mut(cx).scrollbar_show = show.0; window.refresh(); } fn on_toggle_list_active_highlight( &mut self, _: &ToggleListActiveHighlight, window: &mut Window, cx: &mut Context, ) { let theme = Theme::global_mut(cx); theme.list.active_highlight = !theme.list.active_highlight; window.refresh(); } } impl Render for FontSizeSelector { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle.clone(); let font_size = cx.theme().font_size.as_f32() as i32; let radius = cx.theme().radius.as_f32() as i32; let scroll_show = cx.theme().scrollbar_show; div() .id("font-size-selector") .track_focus(&focus_handle) .on_action(cx.listener(Self::on_select_font)) .on_action(cx.listener(Self::on_select_radius)) .on_action(cx.listener(Self::on_select_scrollbar_show)) .on_action(cx.listener(Self::on_toggle_list_active_highlight)) .child( Button::new("btn") .small() .ghost() .icon(IconName::Settings2) .dropdown_menu(move |this, _, cx| { this.scrollable(true) .check_side(Side::Right) .max_h(px(480.)) .label("Font Size") .menu_with_check("Large", font_size == 18, Box::new(SelectFont(18))) .menu_with_check( "Medium (default)", font_size == 16, Box::new(SelectFont(16)), ) .menu_with_check("Small", font_size == 14, Box::new(SelectFont(14))) .separator() .label("Border Radius") .menu_with_check("8px", radius == 8, Box::new(SelectRadius(8))) .menu_with_check( "6px (default)", radius == 6, Box::new(SelectRadius(6)), ) .menu_with_check("4px", radius == 4, Box::new(SelectRadius(4))) .menu_with_check("0px", radius == 0, Box::new(SelectRadius(0))) .separator() .label("Scrollbar") .menu_with_check( "Scrolling to show", scroll_show == ScrollbarShow::Scrolling, Box::new(SelectScrollbarShow(ScrollbarShow::Scrolling)), ) .menu_with_check( "Hover to show", scroll_show == ScrollbarShow::Hover, Box::new(SelectScrollbarShow(ScrollbarShow::Hover)), ) .menu_with_check( "Always show", scroll_show == ScrollbarShow::Always, Box::new(SelectScrollbarShow(ScrollbarShow::Always)), ) .separator() .menu_with_check( "List Active Highlight", cx.theme().list.active_highlight, Box::new(ToggleListActiveHighlight), ) }) .anchor(Corner::TopRight), ) } } ================================================ FILE: crates/story-web/.cargo/config.toml ================================================ [target.wasm32-unknown-unknown] rustflags = [] ================================================ FILE: crates/story-web/Cargo.toml ================================================ [package] name = "gpui-component-story-web" version = "0.5.1" publish = false edition.workspace = true [lib] crate-type = ["cdylib", "rlib"] [dependencies] gpui.workspace = true gpui_platform.workspace = true gpui-component = { path = "../ui", default-features = false } gpui-component-assets.workspace = true gpui-component-story = { path = "../story", default-features = false } # Web specific dependencies console_error_panic_hook = "0.1" log.workspace = true tracing-wasm = "0.2" console_log = "1.0" wasm-bindgen = "0.2" [lints] workspace = true ================================================ FILE: crates/story-web/Makefile ================================================ .PHONY: help dev build-wasm build-web build clean install help: ## Show help information @echo "GPUI Component Story Web - Available commands:" @echo "" @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' install: ## Install all dependencies @echo "Checking Rust WASM target..." @rustup target add wasm32-unknown-unknown || true @echo "Checking wasm-bindgen-cli..." @cargo install wasm-bindgen-cli || true @echo "Installing frontend dependencies..." @cd www && bun install build-wasm: ## Build WASM (release mode) @./scripts/build-wasm.sh --release build-wasm-dev: ## Build WASM (debug mode) @./scripts/build-wasm.sh build-web: ## Build frontend @cd www && bun run build build-web-prod: ## Build frontend for production (GitHub Pages) @cd www && bun install && NODE_ENV=production bun run build build: build-wasm build-web ## Build complete project (WASM + frontend) build-prod: build-wasm build-web-prod ## Build complete project for production dev: build-wasm-dev ## Start development server @cd www && bun install && bun run dev preview: ## Preview production build @cd www && bun run preview clean: ## Clean build artifacts @echo "Cleaning build artifacts..." @rm -rf www/dist @rm -rf www/src/wasm/*.js www/src/wasm/*.wasm @cargo clean watch-wasm: ## Watch Rust code changes and auto-rebuild WASM @echo "Watching WASM changes..." @cargo watch -x 'build --target wasm32-unknown-unknown' -s './scripts/build-wasm.sh' ================================================ FILE: crates/story-web/README.md ================================================ # GPUI Component Story Web Web-based component gallery for GPUI Component library. **Live Demo**: https://longbridge.github.io/gpui-component/gallery/ ## Prerequisites - Rust toolchain with `wasm32-unknown-unknown` target - [Bun](https://bun.sh) (recommended) or Node.js - wasm-bindgen-cli ### Install Dependencies ```bash # Add WASM target rustup target add wasm32-unknown-unknown # Install wasm-bindgen-cli cargo install wasm-bindgen-cli # Install Bun (macOS/Linux) curl -fsSL https://bun.sh/install | bash ``` ## Development ### Start Development Server ```bash make dev ``` This will: 1. Build WASM in debug mode 2. Generate JavaScript bindings 3. Start Vite dev server on http://localhost:3000 ## Production Build ### Build for Production ```bash make build-prod ``` This builds the project with: - Release mode WASM - Production optimizations - Base path set to `/gpui-component/gallery/` for GitHub Pages The output will be in `www/dist/` directory. ## Deployment The gallery is automatically deployed to GitHub Pages at `/gpui-component/gallery/` when docs are released. The deployment is handled by `.github/workflows/release-docs.yml` which: 1. Builds WASM in release mode 2. Builds frontend with production settings 3. Copies output to `docs/.vitepress/dist/gallery/` 4. Deploys to GitHub Pages ================================================ FILE: crates/story-web/rust-toolchain.toml ================================================ [toolchain] channel = "nightly" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] ================================================ FILE: crates/story-web/scripts/build-wasm.sh ================================================ #!/bin/bash set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color echo -e "${GREEN}Building GPUI Component Story Web...${NC}" # Get the script directory SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" PROJECT_ROOT="$SCRIPT_DIR/.." # Parse arguments RELEASE_FLAG="" if [[ "$1" == "--release" ]]; then RELEASE_FLAG="--release" echo -e "${YELLOW}Building in release mode${NC}" fi # Step 1: Build WASM echo -e "${GREEN}Step 1: Building WASM...${NC}" cd "$PROJECT_ROOT" cargo build --target wasm32-unknown-unknown $RELEASE_FLAG # Determine the build directory if [[ "$RELEASE_FLAG" == "--release" ]]; then BUILD_MODE="release" else BUILD_MODE="debug" fi # WASM file is in workspace target directory WORKSPACE_ROOT="$PROJECT_ROOT/../.." WASM_PATH="$WORKSPACE_ROOT/target/wasm32-unknown-unknown/$BUILD_MODE/gpui_component_story_web.wasm" # Check if WASM file exists if [[ ! -f "$WASM_PATH" ]]; then echo -e "${RED}Error: WASM file not found at: $WASM_PATH${NC}" exit 1 fi # Step 2: Generate JavaScript bindings echo -e "${GREEN}Step 2: Generating JavaScript bindings...${NC}" wasm-bindgen "$WASM_PATH" \ --out-dir "$PROJECT_ROOT/www/src/wasm" \ --target web \ --no-typescript echo -e "${GREEN}✓ Build completed successfully!${NC}" echo -e "${YELLOW}Next steps:${NC}" echo -e " cd www" echo -e " bun install" echo -e " bun run dev" ================================================ FILE: crates/story-web/src/lib.rs ================================================ use gpui::{prelude::*, *}; use gpui_component::Root; use gpui_component_assets::Assets; use gpui_component_story::{Gallery, StoryRoot}; use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn run() -> Result<(), JsValue> { console_error_panic_hook::set_once(); // Initialize logging to browser console console_log::init_with_level(log::Level::Info).expect("Failed to initialize logger"); // Also initialize tracing for WASM tracing_wasm::set_as_global_default(); #[cfg(target_family = "wasm")] gpui_platform::web_init(); #[cfg(not(target_family = "wasm"))] let app = gpui_platform::application(); #[cfg(target_family = "wasm")] let app = gpui_platform::single_threaded_web(); app.with_assets(Assets::new( "https://longbridge.github.io/gpui-component/gallery/", )) .run(|cx: &mut App| { gpui_component_story::init(cx); cx.open_window(WindowOptions::default(), |window, cx| { let view = Gallery::view(None, window, cx); let story_root = cx.new(|cx| StoryRoot::new("GPUI Component", view, window, cx)); cx.new(|cx| Root::new(story_root, window, cx)) }) .expect("Failed to open window"); cx.activate(true); }); Ok(()) } ================================================ FILE: crates/story-web/www/.gitignore ================================================ # Dependencies node_modules/ bun.lockb # Build outputs dist/ *.local # WASM generated files src/wasm/ !src/wasm/.gitkeep # Environment .env .env.local .env.*.local # Editor .vscode/ .idea/ *.swp *.swo *~ # OS .DS_Store Thumbs.db ================================================ FILE: crates/story-web/www/.prettierrc ================================================ { "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "printWidth": 100, "arrowParens": "avoid" } ================================================ FILE: crates/story-web/www/index.html ================================================ GPUI Component Story Gallery

Loading GPUI Component Story Gallery...

================================================ FILE: crates/story-web/www/package.json ================================================ { "name": "gpui-component-story-web", "version": "0.5.1", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "wasm": "cargo build --target wasm32-unknown-unknown --release && wasm-bindgen ../../../target/wasm32-unknown-unknown/release/gpui_component_story_web.wasm --out-dir ./src/wasm --target web --no-typescript" }, "devDependencies": { "vite": "^8", "vite-plugin-static-copy": "^3.2.0", "vite-plugin-wasm": "^3.3.0" } } ================================================ FILE: crates/story-web/www/src/main.js ================================================ async function init() { const loadingEl = document.getElementById('loading'); const appEl = document.getElementById('app'); try { // Import the WASM module const wasm = await import('./wasm/gpui_component_story_web.js'); await wasm.default(); // Initialize the story gallery await wasm.run(); // Hide loading indicator if (loadingEl) { loadingEl.remove(); } } catch (error) { console.error('Failed to initialize:', error); // Show error message if (loadingEl) { loadingEl.innerHTML = `

Failed to load the application

${error.message || error}

Please check the console for more details.

`; } } } init(); ================================================ FILE: crates/story-web/www/vite.config.js ================================================ import { defineConfig } from 'vite'; import wasm from 'vite-plugin-wasm'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ plugins: [ wasm(), viteStaticCopy({ targets: [ { src: path.resolve(__dirname, '../../assets/assets/icons'), dest: 'assets', }, ], }), { name: 'serve-assets', configureServer(server) { server.middlewares.use('/gpui-component/gallery/assets', (req, res, next) => { const assetsPath = path.resolve(__dirname, '../../assets/assets'); const filePath = path.join(assetsPath, req.url.replace('/assets', '')); // Try to serve the file import('fs').then(({ default: fs }) => { if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { res.setHeader('Access-Control-Allow-Origin', '*'); if (filePath.endsWith('.svg')) { res.setHeader('Content-Type', 'image/svg+xml'); } fs.createReadStream(filePath).pipe(res); } else { next(); } }); }); }, }, ], build: { target: 'esnext', minify: true, sourcemap: false, rollupOptions: { output: { manualChunks: undefined, }, }, }, server: { port: 3000, open: true, fs: { strict: false, allow: ['..'], }, headers: { 'Cross-Origin-Embedder-Policy': 'require-corp', 'Cross-Origin-Opener-Policy': 'same-origin', }, }, optimizeDeps: { exclude: ['./src/wasm'], }, base: '/gpui-component/gallery/', }); ================================================ FILE: crates/ui/Cargo.toml ================================================ [package] description = "UI components for building fantastic desktop application by using GPUI." keywords = ["desktop", "gpui", "shadcn", "ui", "uikit"] license = "Apache-2.0" name = "gpui-component" documentation = "https://docs.rs/gpui-component" homepage = "https://longbridge.github.io/gpui-component" repository = "https://github.com/longbridge/gpui-component" readme = "../../README.md" version = "0.5.1" publish = true edition.workspace = true [lib] doctest = false [features] decimal = ["dep:rust_decimal"] inspector = ["gpui_macros/inspector", "gpui/inspector"] # For syntax highlighting in Markdown and CodeEditor. tree-sitter-languages = [ "dep:tree-sitter-astro-next", "dep:tree-sitter-bash", "dep:tree-sitter-c", "dep:tree-sitter-c-sharp", "dep:tree-sitter-cmake", "dep:tree-sitter-cpp", "dep:tree-sitter-css", "dep:tree-sitter-diff", "dep:tree-sitter-elixir", "dep:tree-sitter-embedded-template", "dep:tree-sitter-go", "dep:tree-sitter-graphql", "dep:tree-sitter-html", "dep:tree-sitter-java", "dep:tree-sitter-javascript", "dep:tree-sitter-jsdoc", "dep:tree-sitter-kotlin-sg", "dep:tree-sitter-lua", "dep:tree-sitter-make", "dep:tree-sitter-md", "dep:tree-sitter-php", "dep:tree-sitter-proto", "dep:tree-sitter-python", "dep:tree-sitter-ruby", "dep:tree-sitter-rust", "dep:tree-sitter-scala", "dep:tree-sitter-sequel", "dep:tree-sitter-swift", "dep:tree-sitter-toml-ng", "dep:tree-sitter-typescript", "dep:tree-sitter-svelte-next", "dep:tree-sitter-yaml", "dep:tree-sitter-zig", ] [dependencies] anyhow.workspace = true gpui = { workspace = true } gpui_macros.workspace = true gpui-component-macros = { workspace = true } notify.workspace = true ropey.workspace = true rust-i18n.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true serde_repr.workspace = true smallvec.workspace = true sum-tree.workspace = true tracing.workspace = true log.workspace = true enum-iterator = "2.1.0" itertools = "0.13.0" once_cell = "1.19.0" paste = "1" regex = "1" unicode-segmentation = "1.12.0" uuid = "1.10" # Async primitives (cross-platform, including WASM) async-channel = "2.3.1" futures = "0.3" # Chart num-traits = "0.2" rust_decimal = { version = "1.37.0", optional = true } # Markdown Parser markdown = { version = "1.0.0", features = ["serde"] } # HTML Parser html5ever = "0.27" markup5ever_rcdom = "0.3.0" # Calendar chrono = "0.4.38" # Code Editor aho-corasick = "1.1.3" lsp-types.workspace = true # Cross-platform time APIs (zero-cost on native, uses web APIs on WASM) instant = { version = "0.1", features = ["wasm-bindgen"] } # Native-only dependencies (not available on WASM) [target.'cfg(not(target_family = "wasm"))'.dependencies] smol.workspace = true tree-sitter = "0.25.4" tree-sitter-astro-next = { version="0.1.1", optional = true } tree-sitter-bash = { version = "0.23.3", optional = true } tree-sitter-c = { version = "0.24.1", optional = true } tree-sitter-c-sharp = { version = "0.23.1", optional = true } tree-sitter-cmake = { version = "0.7.1", optional = true } tree-sitter-cpp = { version = "0.23.4", optional = true } tree-sitter-css = { version = "0.23.2", optional = true } tree-sitter-diff = { version = "0.1.0", optional = true } tree-sitter-elixir = { version = "0.3", optional = true } tree-sitter-embedded-template = { version = "0.23.0", optional = true } tree-sitter-go = { version = "0.23.4", optional = true } tree-sitter-graphql = { version = "0.1.0", optional = true } tree-sitter-html = { version = "0.23.2", optional = true } tree-sitter-java = { version = "0.23.5", optional = true } tree-sitter-javascript = { version = "0.23.1", optional = true } tree-sitter-jsdoc = { version = "0.23.2", optional = true } tree-sitter-json = "0.24.8" tree-sitter-kotlin-sg = { version = "0.4.0", optional = true } tree-sitter-lua = { version = "0.4.1", optional = true } tree-sitter-make = { version = "1.1.1", optional = true } tree-sitter-md = { version = "0.5.1", optional = true } tree-sitter-php = { version = "0.24.2", optional = true } tree-sitter-proto = { version = "0.2.0", optional = true } tree-sitter-python = { version = "0.23.6", optional = true } tree-sitter-ruby = { version = "0.23.1", optional = true } tree-sitter-rust = { version = "0.24.0", optional = true } tree-sitter-scala = { version = "0.23.4", optional = true } tree-sitter-sequel = { version = "0.3.8", optional = true } tree-sitter-svelte-next = { version = "0.1.1", optional = true } tree-sitter-swift = { version = "0.7.0", optional = true } tree-sitter-toml-ng = { version = "0.7.0", optional = true } tree-sitter-typescript = { version = "0.23.2", optional = true } tree-sitter-yaml = { version = "0.7.1", optional = true } tree-sitter-zig = { version = "1.1.2", optional = true } [target.'cfg(target_os = "macos")'.dependencies] core-text = "=21.0.0" [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } indoc = "2" [lints] workspace = true ================================================ FILE: crates/ui/LICENSE-APACHE ================================================ Copyright 2024 - 2025 Longbridge Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: crates/ui/build.rs ================================================ use std::{fs, path::Path}; fn main() { // Tell Cargo to rerun this build script if any SVG file in assets/icons changes. let icons_dir = Path::new("../assets/assets/icons"); // Watch the icons directory itself. println!("cargo:rerun-if-changed=../assets/assets/icons"); // Watch each SVG file in the directory. if let Ok(entries) = fs::read_dir(icons_dir) { for entry in entries.flatten() { let path = entry.path(); if path.extension().and_then(|s| s.to_str()) == Some("svg") { println!("cargo:rerun-if-changed={}", path.display()); } } } } ================================================ FILE: crates/ui/locales/ui.yml ================================================ _version: 2 Calendar: week.0: en: Su zh-CN: 日 zh-HK: 日 it: Do week.1: en: Mo zh-CN: 一 zh-HK: 一 it: Lu week.2: en: Tu zh-CN: 二 zh-HK: 二 it: Ma week.3: en: We zh-CN: 三 zh-HK: 三 it: Me week.4: en: Th zh-CN: 四 zh-HK: 四 it: Gi week.5: en: Fr zh-CN: 五 zh-HK: 五 it: Ve week.6: en: Sa zh-CN: 六 zh-HK: 六 it: Sa month.January: en: January zh-CN: 一月 zh-HK: 一月 it: Gennaio month.February: en: February zh-CN: 二月 zh-HK: 二月 it: Febbraio month.March: en: March zh-CN: 三月 zh-HK: 三月 it: Marzo month.April: en: April zh-CN: 四月 zh-HK: 四月 it: Aprile month.May: en: May zh-CN: 五月 zh-HK: 五月 it: Maggio month.June: en: June zh-CN: 六月 zh-HK: 六月 it: Giugno month.July: en: July zh-CN: 七月 zh-HK: 七月 it: Luglio month.August: en: August zh-CN: 八月 zh-HK: 八月 it: Agosto month.September: en: September zh-CN: 九月 zh-HK: 九月 it: Settembre month.October: en: October zh-CN: 十月 zh-HK: 十月 it: Ottobre month.November: en: November zh-CN: 十一月 zh-HK: 十一月 it: Novembre month.December: en: December zh-CN: 十二月 zh-HK: 十二月 it: Dicembre DatePicker: placeholder: en: "Select date" zh-CN: 选择日期 zh-HK: 選擇日期 it: "Seleziona data" Select: placeholder: en: "Please select" zh-CN: "请选择" zh-HK: "請選擇" it: Seleziona Dock: Unnamed: en: Unnamed zh-CN: 未命名 zh-HK: 未命名 it: "Senza nome" Close: en: Close zh-CN: 关闭 zh-HK: 關閉 it: Chiudi Zoom In: en: Zoom In zh-CN: 放大 zh-HK: 放大 it: Zoom In Zoom Out: en: Zoom Out zh-CN: 缩小 zh-HK: 縮小 it: Zoom Out Collapse: en: Collapse zh-CN: 隐藏 zh-HK: 隱藏 it: Nascondi Expand: en: Expand zh-CN: 展开 zh-HK: 展開 it: Espandi ColorPicker: Palette: en: Palette zh-CN: 调色板 zh-HK: 調色板 it: Tavolozza HSLA: en: HSLA zh-CN: HSLA zh-HK: HSLA it: HSLA Hue: en: Hue zh-CN: 色相 zh-HK: 色相 it: Tonalità Saturation: en: Saturation zh-CN: 饱和度 zh-HK: 飽和度 it: Saturazione Lightness: en: Lightness zh-CN: 亮度 zh-HK: 亮度 it: Luminosità Alpha: en: Alpha zh-CN: 透明度 zh-HK: 透明度 it: Alfa Dialog: ok: en: OK zh-CN: 确定 zh-HK: 確定 it: OK cancel: en: Cancel zh-CN: 取消 zh-HK: 取消 it: Annulla List: search_placeholder: en: Search... zh-CN: 搜索... zh-HK: 搜索... it: Ricerca... Input: Replace: en: Replace zh-CN: 替换 zh-HK: 替換 Replace All: en: Replace All zh-CN: 全部替换 zh-HK: 全部替換 Cut: en: Cut zh-CN: 剪切 zh-HK: 剪切 Copy: en: Copy zh-CN: 复制 zh-HK: 複製 Paste: en: Paste zh-CN: 粘贴 zh-HK: 貼上 Select All: en: Select All zh-CN: 全选 zh-HK: 全選 Go to Definition: en: Go to Definition zh-CN: 跳转到定义 zh-HK: 跳轉到定義 Show Code Actions: en: Show Code Actions zh-CN: 显示代码操作 zh-HK: 顯示代碼操作 Settings: search_placeholder: en: Search... zh-CN: 搜索... zh-HK: 搜索... it: Ricerca... Reset All: en: Reset All zh-CN: 重置全部 zh-HK: 重置全部 it: Resetta Tutto Pagination: previous: en: Previous zh-CN: 上一页 zh-HK: 上一頁 next: en: Next zh-CN: 下一页 zh-HK: 下一頁 ================================================ FILE: crates/ui/src/accordion.rs ================================================ use std::{cell::RefCell, collections::HashSet, rc::Rc, sync::Arc}; use gpui::{ AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, Window, div, prelude::FluentBuilder as _, rems, }; use crate::{ActiveTheme as _, Icon, IconName, Sizable, Size, h_flex, v_flex}; /// Accordion element. #[derive(IntoElement)] pub struct Accordion { id: ElementId, multiple: bool, size: Size, bordered: bool, disabled: bool, children: Vec, on_toggle_click: Option>, } impl Accordion { /// Create a new Accordion with the given ID. pub fn new(id: impl Into) -> Self { Self { id: id.into(), multiple: false, size: Size::default(), bordered: true, children: Vec::new(), disabled: false, on_toggle_click: None, } } /// Set whether multiple accordion items can be opened simultaneously, default: false pub fn multiple(mut self, multiple: bool) -> Self { self.multiple = multiple; self } /// Set whether the accordion items have borders, default: true pub fn bordered(mut self, bordered: bool) -> Self { self.bordered = bordered; self } /// Set whether the accordion is disabled, default: false pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Adds an AccordionItem to the Accordion. pub fn item(mut self, child: F) -> Self where F: FnOnce(AccordionItem) -> AccordionItem, { let item = child(AccordionItem::new()); self.children.push(item); self } /// Sets the on_toggle_click callback for the AccordionGroup. /// /// The first argument `Vec` is the indices of the open accordions. pub fn on_toggle_click( mut self, on_toggle_click: impl Fn(&[usize], &mut Window, &mut App) + Send + Sync + 'static, ) -> Self { self.on_toggle_click = Some(Arc::new(on_toggle_click)); self } } impl Sizable for Accordion { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); self } } impl RenderOnce for Accordion { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let open_ixs = Rc::new(RefCell::new(HashSet::new())); let is_multiple = self.multiple; v_flex() .id(self.id) .size_full() .when(self.bordered, |this| this.gap_y_2()) .children( self.children .into_iter() .enumerate() .map(|(ix, accordion)| { if accordion.open { open_ixs.borrow_mut().insert(ix); } accordion .index(ix) .with_size(self.size) .bordered(self.bordered) .disabled(self.disabled) .on_toggle_click({ let open_ixs = Rc::clone(&open_ixs); move |open, _, _| { let mut open_ixs = open_ixs.borrow_mut(); if *open { if !is_multiple { open_ixs.clear(); } open_ixs.insert(ix); } else { open_ixs.remove(&ix); } } }) }), ) .when_some( self.on_toggle_click.filter(|_| !self.disabled), move |this, on_toggle_click| { let open_ixs = Rc::clone(&open_ixs); this.on_click(move |_, window, cx| { let open_ixs: Vec = open_ixs.borrow().iter().map(|&ix| ix).collect(); on_toggle_click(&open_ixs, window, cx); }) }, ) } } /// An Accordion is a vertically stacked list of items, each of which can be expanded to reveal the content associated with it. #[derive(IntoElement)] pub struct AccordionItem { index: usize, icon: Option, title: AnyElement, children: Vec, open: bool, size: Size, bordered: bool, disabled: bool, on_toggle_click: Option>, } impl AccordionItem { /// Create a new AccordionItem. pub fn new() -> Self { Self { index: 0, icon: None, title: SharedString::default().into_any_element(), children: Vec::new(), open: false, disabled: false, on_toggle_click: None, size: Size::default(), bordered: true, } } /// Set the icon for the accordion item. pub fn icon(mut self, icon: impl Into) -> Self { self.icon = Some(icon.into()); self } /// Set the title for the accordion item. pub fn title(mut self, title: impl IntoElement) -> Self { self.title = title.into_any_element(); self } pub fn bordered(mut self, bordered: bool) -> Self { self.bordered = bordered; self } pub fn open(mut self, open: bool) -> Self { self.open = open; self } pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } fn index(mut self, index: usize) -> Self { self.index = index; self } fn on_toggle_click( mut self, on_toggle_click: impl Fn(&bool, &mut Window, &mut App) + 'static, ) -> Self { self.on_toggle_click = Some(Arc::new(on_toggle_click)); self } } impl ParentElement for AccordionItem { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } impl Sizable for AccordionItem { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); self } } impl RenderOnce for AccordionItem { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let text_size = match self.size { Size::XSmall => rems(0.875), Size::Small => rems(0.875), _ => rems(1.0), }; div().flex_1().child( v_flex() .w_full() .bg(cx.theme().accordion) .overflow_hidden() .when(self.bordered, |this| { this.border_1() .rounded(cx.theme().radius) .border_color(cx.theme().border) }) .text_size(text_size) .child( h_flex() .id(self.index) .justify_between() .gap_3() .map(|this| match self.size { Size::XSmall => this.py_0().px_1p5(), Size::Small => this.py_0p5().px_2(), Size::Large => this.py_1p5().px_4(), _ => this.py_1().px_3(), }) .when(self.open, |this| { this.when(self.bordered, |this| { this.text_color(cx.theme().foreground) .border_b_1() .border_color(cx.theme().border) }) }) .when(!self.bordered, |this| { this.border_b_1().border_color(cx.theme().border) }) .child( h_flex() .items_center() .map(|this| match self.size { Size::XSmall => this.gap_1(), Size::Small => this.gap_1(), _ => this.gap_2(), }) .when_some(self.icon, |this, icon| { this.child( icon.with_size(self.size) .text_color(cx.theme().muted_foreground), ) }) .child(self.title), ) .when(!self.disabled, |this| { this.hover(|this| this.bg(cx.theme().accordion_hover)) .child( Icon::new(if self.open { IconName::ChevronUp } else { IconName::ChevronDown }) .xsmall() .text_color(cx.theme().muted_foreground), ) .when_some(self.on_toggle_click, |this, on_toggle_click| { this.on_click({ move |_, window, cx| { on_toggle_click(&!self.open, window, cx); } }) }) }), ) .when(self.open, |this| { this.child( div() .map(|this| match self.size { Size::XSmall => this.p_1p5(), Size::Small => this.p_2(), Size::Large => this.p_4(), _ => this.p_3(), }) .children(self.children), ) }), ) } } ================================================ FILE: crates/ui/src/actions.rs ================================================ use gpui::{actions, Action}; use serde::Deserialize; #[derive(Clone, Action, PartialEq, Eq, Deserialize)] #[action(namespace = ui, no_json)] pub struct Confirm { /// Is confirm with secondary. pub secondary: bool, } actions!(ui, [Cancel, SelectUp, SelectDown, SelectLeft, SelectRight, SelectFirst, SelectLast, SelectPrevColumn, SelectNextColumn, SelectPageUp, SelectPageDown]); ================================================ FILE: crates/ui/src/alert.rs ================================================ use std::rc::Rc; use gpui::{ App, ClickEvent, ElementId, Empty, Hsla, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _, px, rems, transparent_white, }; use crate::{ ActiveTheme as _, Colorize, Icon, IconName, Sizable, Size, StyledExt, h_flex, text::{Text, TextViewStyle}, }; /// The variant of the [`Alert`]. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum AlertVariant { #[default] Default, Info, Success, Warning, Error, } impl AlertVariant { fn fg(&self, cx: &App) -> Hsla { match self { Self::Default => cx.theme().foreground, Self::Info => cx.theme().info, Self::Success => cx.theme().success, Self::Warning => cx.theme().warning, Self::Error => cx.theme().danger, } } fn bg(&self, cx: &App) -> Hsla { match self { Self::Default => cx.theme().background, Self::Info => cx.theme().info.mix_oklab(transparent_white(), 0.04), Self::Success => cx.theme().success.mix_oklab(transparent_white(), 0.04), Self::Warning => cx.theme().warning.mix_oklab(transparent_white(), 0.04), Self::Error => cx.theme().danger.mix_oklab(transparent_white(), 0.04), } } fn border_color(&self, cx: &App) -> Hsla { match self { Self::Default => cx.theme().border, Self::Info => cx.theme().info.mix_oklab(transparent_white(), 0.3), Self::Success => cx.theme().success.mix_oklab(transparent_white(), 0.3), Self::Warning => cx.theme().warning.mix_oklab(transparent_white(), 0.3), Self::Error => cx.theme().danger.mix_oklab(transparent_white(), 0.3), } } } /// Alert used to display a message to the user. #[derive(IntoElement)] pub struct Alert { id: ElementId, style: StyleRefinement, variant: AlertVariant, icon: Icon, title: Option, message: Text, size: Size, banner: bool, on_close: Option>, visible: bool, } impl Alert { /// Create a new alert with the given message. pub fn new(id: impl Into, message: impl Into) -> Self { Self { id: id.into(), style: StyleRefinement::default(), variant: AlertVariant::default(), icon: Icon::new(IconName::Info), title: None, message: message.into(), size: Size::default(), banner: false, visible: true, on_close: None, } } /// Create a new info [`AlertVariant::Info`] with the given message. pub fn info(id: impl Into, message: impl Into) -> Self { Self::new(id, message) .with_variant(AlertVariant::Info) .icon(IconName::Info) } /// Create a new [`AlertVariant::Success`] alert with the given message. pub fn success(id: impl Into, message: impl Into) -> Self { Self::new(id, message) .with_variant(AlertVariant::Success) .icon(IconName::CircleCheck) } /// Create a new [`AlertVariant::Warning`] alert with the given message. pub fn warning(id: impl Into, message: impl Into) -> Self { Self::new(id, message) .with_variant(AlertVariant::Warning) .icon(IconName::TriangleAlert) } /// Create a new [`AlertVariant::Error`] alert with the given message. pub fn error(id: impl Into, message: impl Into) -> Self { Self::new(id, message) .with_variant(AlertVariant::Error) .icon(IconName::CircleX) } /// Sets the [`AlertVariant`] of the alert. pub fn with_variant(mut self, variant: AlertVariant) -> Self { self.variant = variant; self } /// Set the icon for the alert. pub fn icon(mut self, icon: impl Into) -> Self { self.icon = icon.into(); self } /// Set the title for the alert. pub fn title(mut self, title: impl Into) -> Self { self.title = Some(title.into()); self } /// Set alert as banner style. /// /// The `banner` style will make the alert take the full width of the container and not border and radius. /// This mode will not display `title`. pub fn banner(mut self) -> Self { self.banner = true; self } /// Set the visibility of the alert. pub fn visible(mut self, visible: bool) -> Self { self.visible = visible; self } /// Set alert as closable, true will show Close icon. pub fn on_close( mut self, on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> Self { self.on_close = Some(Rc::new(on_close)); self } } impl Sizable for Alert { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); self } } impl Styled for Alert { fn style(&mut self) -> &mut gpui::StyleRefinement { &mut self.style } } impl RenderOnce for Alert { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { if !self.visible { return Empty.into_any_element(); } let (radius, padding_x, padding_y, gap) = match self.size { Size::XSmall => (cx.theme().radius, px(12.), px(6.), px(6.)), Size::Small => (cx.theme().radius, px(12.), px(8.), px(6.)), Size::Large => (cx.theme().radius_lg, px(20.), px(14.), px(12.)), _ => (cx.theme().radius, px(16.), px(10.), px(12.)), }; let bg = self.variant.bg(cx); let fg = self.variant.fg(cx); let border_color = self.variant.border_color(cx); h_flex() .id(self.id) .w_full() .text_color(fg) .bg(bg) .px(padding_x) .py(padding_y) .gap(gap) .justify_between() .text_sm() .border_1() .border_color(border_color) .when(!self.banner, |this| this.rounded(radius).items_start()) .refine_style(&self.style) .child( div() .flex() .flex_1() .when(self.banner, |this| this.items_center()) .overflow_hidden() .gap(gap) .child( div() .when(!self.banner, |this| this.mt(px(5.))) .child(self.icon), ) .child( div() .flex_1() .overflow_hidden() .gap_3() .when(!self.banner, |this| { this.when_some(self.title, |this, title| { this.child( div().w_full().truncate().font_semibold().child(title), ) }) }) .child( self.message .style(TextViewStyle::default().paragraph_gap(rems(0.2))), ), ), ) .when_some(self.on_close, |this, on_close| { this.child( div() .id("close") .p_0p5() .rounded(cx.theme().radius) .hover(|this| this.bg(bg.opacity(0.8))) .active(|this| this.bg(bg.opacity(0.9))) .on_click(move |ev, window, cx| { on_close(ev, window, cx); }) .child( Icon::new(IconName::Close) .with_size(self.size.max(Size::Medium)) .flex_shrink_0(), ), ) }) .into_any_element() } } ================================================ FILE: crates/ui/src/anchored.rs ================================================ //! This is a fork of gpui's anchored element that adds support for offsetting //! https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/elements/anchored.rs use gpui::{ AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, Half, InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style, Window, point, px, }; use smallvec::SmallVec; use crate::Anchor; /// The state that the anchored element element uses to track its children. pub struct AnchoredState { child_layout_ids: SmallVec<[LayoutId; 4]>, } /// An anchored element that can be used to display UI that /// will avoid overflowing the window bounds. pub(crate) struct Anchored { children: SmallVec<[AnyElement; 2]>, anchor_corner: Anchor, fit_mode: AnchoredFitMode, anchor_position: Option>, position_mode: AnchoredPositionMode, offset: Option>, } /// anchored gives you an element that will avoid overflowing the window bounds. /// Its children should have no margin to avoid measurement issues. pub(crate) fn anchored() -> Anchored { Anchored { children: SmallVec::new(), anchor_corner: Anchor::TopLeft, fit_mode: AnchoredFitMode::SwitchAnchor, anchor_position: None, position_mode: AnchoredPositionMode::Window, offset: None, } } #[allow(dead_code)] impl Anchored { /// Sets which corner of the anchored element should be anchored to the current position. pub fn anchor(mut self, anchor: Anchor) -> Self { self.anchor_corner = anchor; self } /// Sets the position in window coordinates /// (otherwise the location the anchored element is rendered is used) pub fn position(mut self, anchor: Point) -> Self { self.anchor_position = Some(anchor); self } /// Offset the final position by this amount. /// Useful when you want to anchor to an element but offset from it, such as in PopoverMenu. pub fn offset(mut self, offset: Point) -> Self { self.offset = Some(offset); self } /// Sets the position mode for this anchored element. Local will have this /// interpret its [`Anchored::position`] as relative to the parent element. /// While Window will have it interpret the position as relative to the window. pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self { self.position_mode = mode; self } /// Snap to window edge instead of switching anchor corner when an overflow would occur. pub fn snap_to_window(mut self) -> Self { self.fit_mode = AnchoredFitMode::SnapToWindow; self } /// Snap to window edge and leave some margins. pub fn snap_to_window_with_margin(mut self, edges: impl Into>) -> Self { self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into()); self } } impl ParentElement for Anchored { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements) } } impl Element for Anchored { type RequestLayoutState = AnchoredState; type PrepaintState = (); fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (gpui::LayoutId, Self::RequestLayoutState) { let child_layout_ids = self .children .iter_mut() .map(|child| child.request_layout(window, cx)) .collect::>(); let anchored_style = Style { position: Position::Absolute, display: Display::Flex, ..Style::default() }; let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx); (layout_id, AnchoredState { child_layout_ids }) } fn prepaint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) { if request_layout.child_layout_ids.is_empty() { return; } let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); for child_layout_id in &request_layout.child_layout_ids { let child_bounds = window.layout_bounds(*child_layout_id); child_min = child_min.min(&child_bounds.origin); child_max = child_max.max(&child_bounds.bottom_right()); } let size: Size = (child_max - child_min).into(); let (origin, mut desired) = self.position_mode.get_position_and_bounds( self.anchor_position, self.anchor_corner, size, bounds, self.offset, ); let limits = Bounds { origin: Point::default(), size: window.viewport_size(), }; if self.fit_mode == AnchoredFitMode::SwitchAnchor { let mut anchor_corner = self.anchor_corner; if desired.left() < limits.left() || desired.right() > limits.right() { let switched = Bounds::from_corner_and_size( anchor_corner .other_side_corner_along(Axis::Horizontal) .into(), origin, size, ); if !(switched.left() < limits.left() || switched.right() > limits.right()) { anchor_corner = anchor_corner.other_side_corner_along(Axis::Horizontal); desired = switched } } if desired.top() < limits.top() || desired.bottom() > limits.bottom() { let switched = Bounds::from_corner_and_size( anchor_corner.other_side_corner_along(Axis::Vertical).into(), origin, size, ); if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) { desired = switched; } } } let client_inset = window.client_inset().unwrap_or(px(0.)); let edges = match self.fit_mode { AnchoredFitMode::SnapToWindowWithMargin(edges) => edges, _ => Edges::default(), } .map(|edge| *edge + client_inset); // Snap the horizontal edges of the anchored element to the horizontal edges of the window if // its horizontal bounds overflow, aligning to the left if it is wider than the limits. if desired.right() > limits.right() { desired.origin.x -= desired.right() - limits.right() + edges.right; } if desired.left() < limits.left() { desired.origin.x = limits.origin.x + edges.left; } // Snap the vertical edges of the anchored element to the vertical edges of the window if // its vertical bounds overflow, aligning to the top if it is taller than the limits. if desired.bottom() > limits.bottom() { desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom; } if desired.top() < limits.top() { desired.origin.y = limits.origin.y + edges.top; } let offset = desired.origin - bounds.origin; let offset = point(offset.x.round(), offset.y.round()); window.with_element_offset(offset, |window| { for child in &mut self.children { child.prepaint(window, cx); } }) } fn paint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, _bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { for child in &mut self.children { child.paint(window, cx); } } } impl IntoElement for Anchored { type Element = Self; fn into_element(self) -> Self::Element { self } } /// Which algorithm to use when fitting the anchored element to be inside the window. #[allow(dead_code)] #[derive(Copy, Clone, PartialEq)] pub enum AnchoredFitMode { /// Snap the anchored element to the window edge. SnapToWindow, /// Snap to window edge and leave some margins. SnapToWindowWithMargin(Edges), /// Switch which corner anchor this anchored element is attached to. SwitchAnchor, } /// Which algorithm to use when positioning the anchored element. #[allow(dead_code)] #[derive(Copy, Clone, PartialEq)] pub enum AnchoredPositionMode { /// Position the anchored element relative to the window. Window, /// Position the anchored element relative to its parent. Local, } impl AnchoredPositionMode { fn get_position_and_bounds( &self, anchor_position: Option>, anchor_corner: Anchor, size: Size, bounds: Bounds, offset: Option>, ) -> (Point, Bounds) { let offset = offset.unwrap_or_default(); match self { AnchoredPositionMode::Window => { let anchor_position = anchor_position.unwrap_or(bounds.origin); let bounds = Self::from_corner_and_size(anchor_corner, anchor_position + offset, size); (anchor_position, bounds) } AnchoredPositionMode::Local => { let anchor_position = anchor_position.unwrap_or_default(); let bounds = Self::from_corner_and_size( anchor_corner, bounds.origin + anchor_position + offset, size, ); (anchor_position, bounds) } } } // Ref https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/geometry.rs#L863 fn from_corner_and_size( anchor: Anchor, origin: Point, size: Size, ) -> Bounds { let origin = match anchor { Anchor::TopLeft => origin, Anchor::TopCenter => Point { x: origin.x - size.width.half(), y: origin.y, }, Anchor::TopRight => Point { x: origin.x - size.width, y: origin.y, }, Anchor::BottomLeft => Point { x: origin.x, y: origin.y - size.height, }, Anchor::BottomCenter => Point { x: origin.x - size.width.half(), y: origin.y - size.height, }, Anchor::BottomRight => Point { x: origin.x - size.width, y: origin.y - size.height, }, }; Bounds { origin, size } } } ================================================ FILE: crates/ui/src/animation.rs ================================================ /// A cubic bezier function like CSS `cubic-bezier`. /// /// Builder: /// /// https://cubic-bezier.com pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> impl Fn(f32) -> f32 { move |t: f32| { let one_t = 1.0 - t; let one_t2 = one_t * one_t; let t2 = t * t; let t3 = t2 * t; // The Bezier curve function for x and y, where x0 = 0, y0 = 0, x3 = 1, y3 = 1 let _x = 3.0 * x1 * one_t2 * t + 3.0 * x2 * one_t * t2 + t3; let y = 3.0 * y1 * one_t2 * t + 3.0 * y2 * one_t * t2 + t3; y } } ================================================ FILE: crates/ui/src/async_util.rs ================================================ //! Cross-platform async utilities for both native and WASM targets. // For native targets, re-export smol's primitives #[cfg(not(target_family = "wasm"))] pub use smol::channel::{Receiver, Sender, unbounded}; // For WASM targets, use async-channel #[cfg(target_family = "wasm")] pub use async_channel::{Receiver, Sender, unbounded}; ================================================ FILE: crates/ui/src/avatar/avatar.rs ================================================ use gpui::{ App, Div, Hsla, ImageSource, InteractiveElement, Interactivity, IntoElement, ParentElement as _, RenderOnce, SharedString, StyleRefinement, Styled, Window, div, img, prelude::FluentBuilder, }; use crate::{ ActiveTheme, Colorize, Icon, IconName, Sizable, Size, StyledExt, avatar::{AvatarSized as _, avatar_size}, }; /// User avatar element. /// /// We can use [`Sizable`] trait to set the size of the avatar (see also: [`avatar_size`] about the size in pixels). #[derive(IntoElement)] pub struct Avatar { base: Div, style: StyleRefinement, src: Option, name: Option, short_name: SharedString, placeholder: Icon, size: Size, } impl Avatar { pub fn new() -> Self { Self { base: div(), style: StyleRefinement::default(), src: None, name: None, short_name: SharedString::default(), placeholder: Icon::new(IconName::User), size: Size::Medium, } } /// Set to use image source for the avatar. pub fn src(mut self, source: impl Into) -> Self { self.src = Some(source.into()); self } /// Set name of the avatar user, if `src` is none, will use this name as placeholder. pub fn name(mut self, name: impl Into) -> Self { let name: SharedString = name.into(); let short: SharedString = extract_text_initials(&name).into(); self.name = Some(name); self.short_name = short; self } /// Set placeholder icon, default: [`IconName::User`] pub fn placeholder(mut self, icon: impl Into) -> Self { self.placeholder = icon.into(); self } } impl Sizable for Avatar { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); self } } impl Styled for Avatar { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl InteractiveElement for Avatar { fn interactivity(&mut self) -> &mut Interactivity { self.base.interactivity() } } impl RenderOnce for Avatar { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let corner_radii = self.style.corner_radii.clone(); let mut inner_style = StyleRefinement::default(); inner_style.corner_radii = corner_radii; const COLOR_COUNT: u64 = 360 / 15; fn default_color(ix: u64, cx: &mut App) -> Hsla { let h = (ix * 15).clamp(0, 360) as f32; cx.theme().blue.hue(h / 360.0) } const BG_OPACITY: f32 = 0.2; self.base .avatar_size(self.size) .flex() .items_center() .justify_center() .flex_shrink_0() .rounded_full() .overflow_hidden() .bg(cx.theme().secondary) .text_color(cx.theme().background) .border_1() .border_color(cx.theme().border) .when(self.name.is_none() && self.src.is_none(), |this| { this.text_size(avatar_size(self.size) * 0.6) .child(self.placeholder) }) .map(|this| match self.src { None => this.when(self.name.is_some(), |this| { let color_ix = gpui::hash(&self.short_name) % COLOR_COUNT; let color = default_color(color_ix, cx); this.bg(color.opacity(BG_OPACITY)) .text_color(color) .child(div().avatar_text_size(self.size).child(self.short_name)) }), Some(src) => this.child( img(src) .avatar_size(self.size) .rounded_full() .refine_style(&inner_style), ), }) .refine_style(&self.style) } } fn extract_text_initials(text: &str) -> String { let mut result = text .split(" ") .flat_map(|word| word.chars().next().map(|c| c.to_string())) .take(2) .collect::>() .join(""); if result.len() == 1 { result = text.chars().take(2).collect::(); } result.to_uppercase() } #[cfg(test)] mod tests { use super::*; #[test] fn test_avatar_text_initials() { assert_eq!(extract_text_initials(&"Jason Lee"), "JL".to_string()); assert_eq!(extract_text_initials(&"Foo Bar Dar"), "FB".to_string()); assert_eq!(extract_text_initials(&"huacnlee"), "HU".to_string()); } #[gpui::test] fn test_avatar_builder(_cx: &mut gpui::TestAppContext) { let avatar = Avatar::new() .name("Jason Lee") .placeholder(Icon::new(IconName::User)) .large(); assert_eq!(avatar.name, Some(SharedString::from("Jason Lee"))); assert_eq!(avatar.short_name, SharedString::from("JL")); assert_eq!(avatar.size, Size::Large); } } ================================================ FILE: crates/ui/src/avatar/avatar_group.rs ================================================ use gpui::{ div, prelude::FluentBuilder as _, Div, InteractiveElement, Interactivity, IntoElement, ParentElement as _, RenderOnce, StyleRefinement, Styled, }; use crate::{avatar::Avatar, ActiveTheme, Sizable, Size, StyledExt as _}; /// A grouped avatars to display in a compact layout. #[derive(IntoElement)] pub struct AvatarGroup { base: Div, style: StyleRefinement, avatars: Vec, size: Size, limit: usize, ellipsis: bool, } impl AvatarGroup { /// Create a new AvatarGroup. pub fn new() -> Self { Self { base: div(), style: StyleRefinement::default(), avatars: Vec::new(), size: Size::default(), limit: 3, ellipsis: false, } } /// Add a child avatar to the group. pub fn child(mut self, avatar: Avatar) -> Self { self.avatars.push(avatar); self } /// Add multiple child avatars to the group. pub fn children(mut self, avatars: impl IntoIterator) -> Self { self.avatars.extend(avatars); self } /// Set the maximum number of avatars to display before showing a "more" avatar. pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } /// Set whether to show an ellipsis when the limit is reached, default: false pub fn ellipsis(mut self) -> Self { self.ellipsis = true; self } } impl Sizable for AvatarGroup { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); self } } impl Styled for AvatarGroup { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl InteractiveElement for AvatarGroup { fn interactivity(&mut self) -> &mut Interactivity { self.base.interactivity() } } impl RenderOnce for AvatarGroup { fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement { let item_ml = -super::avatar_size(self.size) * 0.3; let avatars_len = self.avatars.len(); self.base .h_flex() .flex_row_reverse() .refine_style(&self.style) .children(if self.ellipsis && avatars_len > self.limit { Some( Avatar::new() .name("⋯") .bg(cx.theme().secondary) .text_color(cx.theme().muted_foreground) .with_size(self.size) .ml_1(), ) } else { None }) .children( self.avatars .into_iter() .take(self.limit) .enumerate() .rev() .map(|(ix, item)| { item.with_size(self.size) .when(ix > 0, |this| this.ml(item_ml)) }), ) } } #[cfg(test)] mod tests { use super::*; #[gpui::test] fn test_avatar_group_builder(_cx: &mut gpui::TestAppContext) { let group = AvatarGroup::new() .child(Avatar::new().name("Alice")) .child(Avatar::new().name("Bob")) .child(Avatar::new().name("Charlie")) .child(Avatar::new().name("David")) .large() .limit(3) .ellipsis(); assert_eq!(group.avatars.len(), 4); assert_eq!(group.size, Size::Large); assert_eq!(group.limit, 3); assert!(group.ellipsis); } } ================================================ FILE: crates/ui/src/avatar/mod.rs ================================================ mod avatar; mod avatar_group; pub use avatar::*; pub use avatar_group::*; use crate::{Icon, Size, StyledExt as _}; use gpui::{Div, Img, IntoElement, Pixels, Styled, px, rems}; /// Returns the size of the avatar based on the given [`Size`]. pub(super) fn avatar_size(size: Size) -> Pixels { match size { Size::Large => px(80.), Size::Medium => px(48.), Size::Small => px(24.), Size::XSmall => px(16.), Size::Size(size) => size, } } /// Extension for add `avatar_size` method to `IntoElement` to apply avatar size to element. pub(super) trait AvatarSized: IntoElement + Styled { fn avatar_size(self, size: Size) -> Self { self.size(avatar_size(size)) } fn avatar_text_size(self, size: Size) -> Self { match size { Size::Large => self.text_3xl().font_semibold(), Size::Medium => self.text_sm(), Size::Small => self.text_xs(), Size::XSmall => self.text_size(rems(0.65)), Size::Size(size) => self.size(size * 0.5), } } } impl AvatarSized for Div {} impl AvatarSized for Icon {} impl AvatarSized for Img {} ================================================ FILE: crates/ui/src/badge.rs ================================================ use gpui::{ div, prelude::FluentBuilder, px, relative, AnyElement, App, Hsla, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, Window, }; use crate::{h_flex, white, ActiveTheme, Icon, Sizable, Size, StyledExt}; #[derive(Default, Clone)] enum BadgeVariant { #[default] Number, Dot, Icon(Box), } #[allow(unused)] impl BadgeVariant { #[inline] fn is_icon(&self) -> bool { matches!(self, BadgeVariant::Icon(_)) } #[inline] fn is_number(&self) -> bool { matches!(self, BadgeVariant::Number) } } /// A badge for displaying a count, dot, or icon on an element. #[derive(IntoElement)] pub struct Badge { style: StyleRefinement, count: usize, max: usize, variant: BadgeVariant, children: Vec, color: Option, size: Size, } impl Badge { /// Create a new badge. pub fn new() -> Self { Self { style: StyleRefinement::default(), count: 0, max: 99, variant: Default::default(), color: None, children: Vec::new(), size: Size::default(), } } /// Set to use [`BadgeVariant::Dot`] to show a dot. pub fn dot(mut self) -> Self { self.variant = BadgeVariant::Dot; self } /// Set to use [`BadgeVariant::Number`] to show a count. /// /// If count is 0, the badge will be hidden. pub fn count(mut self, count: usize) -> Self { self.count = count; self } /// Set to use [`BadgeVariant::Icon`] to show an icon. pub fn icon(mut self, icon: impl Into) -> Self { self.variant = BadgeVariant::Icon(Box::new(icon.into())); self } /// Set the maximum count to show (Only if [`BadgeVariant::Number`] is used). pub fn max(mut self, max: usize) -> Self { self.max = max; self } /// Set the color (background) of the badge. pub fn color(mut self, color: impl Into) -> Self { self.color = Some(color.into()); self } } impl ParentElement for Badge { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } impl Sizable for Badge { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); self } } impl RenderOnce for Badge { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let visible = match self.variant { BadgeVariant::Number => self.count > 0, BadgeVariant::Dot | BadgeVariant::Icon(_) => true, }; let (size, text_size) = match self.size { Size::Large => (px(24.), px(14.)), Size::Medium | Size::Size(_) => (px(16.), px(10.)), Size::Small | Size::XSmall => (px(10.), px(8.)), }; div() .relative() .refine_style(&self.style) .children(self.children) .when(visible, |this| { this.child( h_flex() .absolute() .justify_center() .items_center() .rounded_full() .bg(self.color.unwrap_or(cx.theme().red)) .text_color(white()) .text_size(text_size) .map(|this| match self.variant { BadgeVariant::Dot => this.top_0().right_0().size(px(6.)), BadgeVariant::Number => { let count = if self.count > self.max { format!("{}+", self.max) } else { self.count.to_string() }; let (top, left) = match self.size { Size::Large => (px(2.), -px(count.len() as f32)), Size::Medium | Size::Size(_) => { (-px(3.), -px(3.) * count.len()) } Size::Small | Size::XSmall => (-px(4.), -px(4.) * count.len()), }; this.top(top) .right(left) .py_0p5() .px_0p5() .min_w_3p5() .text_size(px(10.)) .line_height(relative(1.)) .child(count) } BadgeVariant::Icon(icon) => this .right_0() .bottom_0() .size(size) .border_1() .border_color(cx.theme().background) .child(*icon), }), ) }) } } ================================================ FILE: crates/ui/src/breadcrumb.rs ================================================ use std::rc::Rc; use gpui::{ div, prelude::FluentBuilder as _, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window, }; use crate::{h_flex, ActiveTheme, Icon, IconName, StyledExt}; /// A breadcrumb navigation element. #[derive(IntoElement)] pub struct Breadcrumb { style: StyleRefinement, items: Vec, } /// Item for the [`Breadcrumb`]. #[derive(IntoElement)] pub struct BreadcrumbItem { id: ElementId, style: StyleRefinement, label: SharedString, on_click: Option>, disabled: bool, is_last: bool, } impl BreadcrumbItem { /// Create a new BreadcrumbItem with the given id and label. pub fn new(label: impl Into) -> Self { Self { id: ElementId::Integer(0), style: StyleRefinement::default(), label: label.into(), on_click: None, disabled: false, is_last: false, } } pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } pub fn on_click( mut self, on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> Self { self.on_click = Some(Rc::new(on_click)); self } fn id(mut self, id: impl Into) -> Self { self.id = id.into(); self } /// For internal use only. fn is_last(mut self, is_last: bool) -> Self { self.is_last = is_last; self } } impl Styled for BreadcrumbItem { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl From<&'static str> for BreadcrumbItem { fn from(value: &'static str) -> Self { Self::new(value) } } impl From for BreadcrumbItem { fn from(value: String) -> Self { Self::new(value) } } impl From for BreadcrumbItem { fn from(value: SharedString) -> Self { Self::new(value) } } impl RenderOnce for BreadcrumbItem { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { div() .id(self.id) .child(self.label) .text_color(cx.theme().muted_foreground) .when(self.is_last, |this| this.text_color(cx.theme().foreground)) .when(self.disabled, |this| { this.text_color(cx.theme().muted_foreground) }) .refine_style(&self.style) .when(!self.disabled, |this| { this.when_some(self.on_click, |this, on_click| { this.cursor_pointer().on_click(move |event, window, cx| { on_click(event, window, cx); }) }) }) } } impl Breadcrumb { /// Create a new breadcrumb. pub fn new() -> Self { Self { items: Vec::new(), style: StyleRefinement::default(), } } /// Add an [`BreadcrumbItem`] to the breadcrumb. pub fn child(mut self, item: impl Into) -> Self { self.items.push(item.into()); self } /// Add multiple [`BreadcrumbItem`] items to the breadcrumb. pub fn children(mut self, items: impl IntoIterator>) -> Self { self.items.extend(items.into_iter().map(Into::into)); self } } #[derive(IntoElement)] struct BreadcrumbSeparator; impl RenderOnce for BreadcrumbSeparator { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { Icon::new(IconName::ChevronRight) .text_color(cx.theme().muted_foreground) .size_3p5() .into_any_element() } } impl Styled for Breadcrumb { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl RenderOnce for Breadcrumb { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let items_count = self.items.len(); let mut children = vec![]; for (ix, item) in self.items.into_iter().enumerate() { let is_last = ix == items_count - 1; let item = item.id(ix); children.push(item.is_last(is_last).into_any_element()); if !is_last { children.push(BreadcrumbSeparator.into_any_element()); } } h_flex() .gap_1p5() .text_sm() .text_color(cx.theme().muted_foreground) .refine_style(&self.style) .children(children) } } ================================================ FILE: crates/ui/src/button/button.rs ================================================ use std::rc::Rc; use crate::{ ActiveTheme, Colorize as _, Disableable, FocusableExt as _, Icon, IconName, Selectable, Sizable, Size, StyleSized, StyledExt, button::ButtonIcon, h_flex, tooltip::Tooltip, }; use gpui::{ Action, AnyElement, App, ClickEvent, Corners, Div, Edges, ElementId, Hsla, InteractiveElement, Interactivity, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _, px, relative, transparent_white, }; #[derive(Default, Clone, Copy)] pub enum ButtonRounded { None, Small, #[default] Medium, Large, Size(Pixels), } impl From for ButtonRounded { fn from(px: Pixels) -> Self { ButtonRounded::Size(px) } } #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct ButtonCustomVariant { color: Hsla, foreground: Hsla, shadow: bool, hover: Hsla, active: Hsla, } pub trait ButtonVariants: Sized { fn with_variant(self, variant: ButtonVariant) -> Self; /// With the primary style for the Button. fn primary(self) -> Self { self.with_variant(ButtonVariant::Primary) } /// With the secondary style for the Button. fn secondary(self) -> Self { self.with_variant(ButtonVariant::Secondary) } /// With the danger style for the Button. fn danger(self) -> Self { self.with_variant(ButtonVariant::Danger) } /// With the warning style for the Button. fn warning(self) -> Self { self.with_variant(ButtonVariant::Warning) } /// With the success style for the Button. fn success(self) -> Self { self.with_variant(ButtonVariant::Success) } /// With the info style for the Button. fn info(self) -> Self { self.with_variant(ButtonVariant::Info) } /// With the ghost style for the Button. fn ghost(self) -> Self { self.with_variant(ButtonVariant::Ghost) } /// With the link style for the Button. fn link(self) -> Self { self.with_variant(ButtonVariant::Link) } /// With the text style for the Button, it will no padding look like a normal text. fn text(self) -> Self { self.with_variant(ButtonVariant::Text) } /// With the custom style for the Button. fn custom(self, style: ButtonCustomVariant) -> Self { self.with_variant(ButtonVariant::Custom(style)) } } impl ButtonCustomVariant { pub fn new(cx: &App) -> Self { Self { color: cx.theme().transparent, foreground: cx.theme().foreground, hover: cx.theme().transparent, active: cx.theme().transparent, shadow: false, } } /// Set background color, default is transparent. pub fn color(mut self, color: Hsla) -> Self { self.color = color; self } /// Set foreground color, default is theme foreground. pub fn foreground(mut self, color: Hsla) -> Self { self.foreground = color; self } /// Set hover background color, default is transparent. pub fn hover(mut self, color: Hsla) -> Self { self.hover = color; self } /// Set active background color, default is transparent. pub fn active(mut self, color: Hsla) -> Self { self.active = color; self } /// Set shadow, default is false. pub fn shadow(mut self, shadow: bool) -> Self { self.shadow = shadow; self } } /// The variant of the Button. #[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] pub enum ButtonVariant { #[default] Default, Primary, Secondary, Danger, Info, Success, Warning, Ghost, Link, Text, Custom(ButtonCustomVariant), } impl ButtonVariant { #[inline] pub fn is_link(&self) -> bool { matches!(self, Self::Link) } #[inline] pub fn is_text(&self) -> bool { matches!(self, Self::Text) } #[inline] pub fn is_ghost(&self) -> bool { matches!(self, Self::Ghost) } #[inline] fn no_padding(&self) -> bool { self.is_link() || self.is_text() } #[inline] fn is_default(&self) -> bool { matches!(self, Self::Default) } } /// A Button element. #[derive(IntoElement)] pub struct Button { id: ElementId, base: Stateful
, style: StyleRefinement, icon: Option, label: Option, children: Vec, disabled: bool, pub(crate) selected: bool, variant: ButtonVariant, rounded: ButtonRounded, outline: bool, border_corners: Corners, border_edges: Edges, dropdown_caret: bool, size: Size, compact: bool, tooltip: Option<( SharedString, Option<(Rc>, Option)>, )>, on_click: Option>, on_hover: Option>, loading: bool, loading_icon: Option, tab_index: isize, tab_stop: bool, } impl From", "

Some text

", false, false, ), ] { let mut w = vec![]; let mut minifier = Minifier::new(&mut w); minifier .omit_doctype(true) .collapse_whitespace(collapse_whitespace) .preserve_comments(preserve_comments); minifier.minify(&mut input.as_bytes()).unwrap(); let s = str::from_utf8(&w).unwrap(); assert_eq!(expected, s); } } } ================================================ FILE: crates/ui/src/text/format/markdown.rs ================================================ use gpui::SharedString; use markdown::{ ParseOptions, mdast::{self, Node}, }; use crate::{ highlighter::HighlightTheme, text::{ document::ParsedDocument, node::{ self, BlockNode, CodeBlock, ImageNode, InlineNode, LinkMark, NodeContext, Paragraph, Span, Table, TableRow, TextMark, }, }, }; /// Parse Markdown into a tree of nodes. /// /// TODO: Remove `highlight_theme` option, this should in render stage. pub(crate) fn parse( source: &str, cx: &mut NodeContext, highlight_theme: &HighlightTheme, ) -> Result { markdown::to_mdast(&source, &ParseOptions::gfm()) .map(|n| ast_to_document(source, n, cx, highlight_theme)) .map_err(|e| e.to_string().into()) } fn parse_table_row(table: &mut Table, node: &mdast::TableRow, cx: &mut NodeContext) { let mut row = TableRow::default(); node.children.iter().for_each(|c| { match c { Node::TableCell(cell) => { parse_table_cell(&mut row, cell, cx); } _ => {} }; }); table.children.push(row); } fn parse_table_cell(row: &mut node::TableRow, node: &mdast::TableCell, cx: &mut NodeContext) { let mut paragraph = Paragraph::default(); node.children.iter().for_each(|c| { parse_paragraph(&mut paragraph, c, cx); }); let table_cell = node::TableCell { children: paragraph, ..Default::default() }; row.children.push(table_cell); } fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node, cx: &mut NodeContext) -> String { let span = node.position().map(|pos| Span { start: cx.offset + pos.start.offset, end: cx.offset + pos.end.offset, }); if let Some(span) = span { paragraph.set_span(span); } let mut text = String::new(); match node { Node::Paragraph(val) => { val.children.iter().for_each(|c| { text.push_str(&parse_paragraph(paragraph, c, cx)); }); } Node::Text(val) => { text = val.value.clone(); paragraph.push_str(&val.value) } Node::Emphasis(val) => { let mut child_paragraph = Paragraph::default(); for child in val.children.iter() { text.push_str(&parse_paragraph(&mut child_paragraph, &child, cx)); } paragraph.push( InlineNode::new(&text).marks(vec![(0..text.len(), TextMark::default().italic())]), ); } Node::Strong(val) => { let mut child_paragraph = Paragraph::default(); for child in val.children.iter() { text.push_str(&parse_paragraph(&mut child_paragraph, &child, cx)); } paragraph.push( InlineNode::new(&text).marks(vec![(0..text.len(), TextMark::default().bold())]), ); } Node::Delete(val) => { let mut child_paragraph = Paragraph::default(); for child in val.children.iter() { text.push_str(&parse_paragraph(&mut child_paragraph, &child, cx)); } paragraph.push( InlineNode::new(&text) .marks(vec![(0..text.len(), TextMark::default().strikethrough())]), ); } Node::InlineCode(val) => { text = val.value.clone(); paragraph.push( InlineNode::new(&text).marks(vec![(0..text.len(), TextMark::default().code())]), ); } Node::Link(val) => { let link_mark = Some(LinkMark { url: val.url.clone().into(), title: val.title.clone().map(|s| s.into()), ..Default::default() }); let mut child_paragraph = Paragraph::default(); for child in val.children.iter() { text.push_str(&parse_paragraph(&mut child_paragraph, &child, cx)); } // FIXME: GPUI InteractiveText does not support inline images yet. // So here we push images to the paragraph directly. for child in child_paragraph.children.iter_mut() { if let Some(image) = child.image.as_mut() { image.link = link_mark.clone(); } child.marks.push(( 0..child.text.len(), TextMark { link: link_mark.clone(), ..Default::default() }, )); } paragraph.merge(child_paragraph); } Node::Image(raw) => { paragraph.push_image(ImageNode { url: raw.url.clone().into(), title: raw.title.clone().map(|t| t.into()), alt: Some(raw.alt.clone().into()), ..Default::default() }); } Node::InlineMath(raw) => { text = raw.value.clone(); paragraph.push( InlineNode::new(&text).marks(vec![(0..text.len(), TextMark::default().code())]), ); } Node::MdxTextExpression(raw) => { text = raw.value.clone(); paragraph .push(InlineNode::new(&text).marks(vec![(0..text.len(), TextMark::default())])); } Node::Html(val) => match super::html::parse(&val.value, cx) { Ok(el) => { if el .blocks .first() .map(|node| node.is_break()) .unwrap_or(false) { text = "\n".to_owned(); paragraph.push(InlineNode::new(&text)); } else { if cfg!(debug_assertions) { tracing::warn!("unsupported inline html tag: {:#?}", el); } } } Err(err) => { if cfg!(debug_assertions) { tracing::warn!("failed parsing html: {:#?}", err); } text.push_str(&val.value); } }, Node::FootnoteReference(foot) => { let prefix = format!("[{}]", foot.identifier); paragraph.push(InlineNode::new(&prefix).marks(vec![( 0..prefix.len(), TextMark { italic: true, ..Default::default() }, )])); } Node::LinkReference(link) => { let mut child_paragraph = Paragraph::default(); let mut child_text = String::new(); for child in link.children.iter() { child_text.push_str(&parse_paragraph(&mut child_paragraph, child, cx)); } let link_mark = LinkMark { url: "".into(), title: link.label.clone().map(Into::into), identifier: Some(link.identifier.clone().into()), }; paragraph.push(InlineNode::new(&child_text).marks(vec![( 0..child_text.len(), TextMark { link: Some(link_mark), ..Default::default() }, )])); } _ => { if cfg!(debug_assertions) { tracing::warn!("unsupported inline node: {:#?}", node); } } } text } fn ast_to_document( source: &str, root: mdast::Node, cx: &mut NodeContext, highlight_theme: &HighlightTheme, ) -> ParsedDocument { let root = match root { Node::Root(r) => r, _ => panic!("expected root node"), }; let blocks = root .children .into_iter() .map(|c| ast_to_node(c, cx, highlight_theme)) .collect(); ParsedDocument { source: source.to_string().into(), blocks, } } fn new_span(pos: Option, cx: &NodeContext) -> Option { let pos = pos?; Some(Span { start: cx.offset + pos.start.offset, end: cx.offset + pos.end.offset, }) } fn ast_to_node( value: mdast::Node, cx: &mut NodeContext, highlight_theme: &HighlightTheme, ) -> BlockNode { match value { Node::Root(_) => unreachable!("node::Root should be handled separately"), Node::Paragraph(val) => { let mut paragraph = Paragraph::default(); val.children.iter().for_each(|c| { parse_paragraph(&mut paragraph, c, cx); }); paragraph.span = new_span(val.position, cx); BlockNode::Paragraph(paragraph) } Node::Blockquote(val) => { let children = val .children .into_iter() .map(|c| ast_to_node(c, cx, highlight_theme)) .collect(); BlockNode::Blockquote { children, span: new_span(val.position, cx), } } Node::List(list) => { let children = list .children .into_iter() .map(|c| ast_to_node(c, cx, highlight_theme)) .collect(); BlockNode::List { ordered: list.ordered, children, span: new_span(list.position, cx), } } Node::ListItem(val) => { let children = val .children .into_iter() .map(|c| ast_to_node(c, cx, highlight_theme)) .collect(); BlockNode::ListItem { children, spread: val.spread, checked: val.checked, span: new_span(val.position, cx), } } Node::Break(val) => BlockNode::Break { html: false, span: new_span(val.position, cx), }, Node::Code(raw) => BlockNode::CodeBlock(CodeBlock::new( raw.value.into(), raw.lang.map(|s| s.into()), highlight_theme, new_span(raw.position, cx), )), Node::Heading(val) => { let mut paragraph = Paragraph::default(); val.children.iter().for_each(|c| { parse_paragraph(&mut paragraph, c, cx); }); BlockNode::Heading { level: val.depth, children: paragraph, span: new_span(val.position, cx), } } Node::Math(val) => BlockNode::CodeBlock(CodeBlock::new( val.value.into(), None, highlight_theme, new_span(val.position, cx), )), Node::Html(val) => match super::html::parse(&val.value, cx) { Ok(el) => BlockNode::Root { children: el.blocks, span: new_span(val.position, cx), }, Err(err) => { if cfg!(debug_assertions) { tracing::warn!("error parsing html: {:#?}", err); } BlockNode::Paragraph(Paragraph::new(val.value)) } }, Node::MdxFlowExpression(val) => BlockNode::CodeBlock(CodeBlock::new( val.value.into(), Some("mdx".into()), highlight_theme, new_span(val.position, cx), )), Node::Yaml(val) => BlockNode::CodeBlock(CodeBlock::new( val.value.into(), Some("yml".into()), highlight_theme, new_span(val.position, cx), )), Node::Toml(val) => BlockNode::CodeBlock(CodeBlock::new( val.value.into(), Some("toml".into()), highlight_theme, new_span(val.position, cx), )), Node::MdxJsxTextElement(val) => { let mut paragraph = Paragraph::default(); val.children.iter().for_each(|c| { parse_paragraph(&mut paragraph, c, cx); }); paragraph.span = new_span(val.position, cx); BlockNode::Paragraph(paragraph) } Node::MdxJsxFlowElement(val) => { let mut paragraph = Paragraph::default(); val.children.iter().for_each(|c| { parse_paragraph(&mut paragraph, c, cx); }); paragraph.span = new_span(val.position, cx); BlockNode::Paragraph(paragraph) } Node::ThematicBreak(val) => BlockNode::Divider { span: new_span(val.position, cx), }, Node::Table(val) => { let mut table = Table::default(); table.column_aligns = val .align .clone() .into_iter() .map(|align| align.into()) .collect(); val.children.iter().for_each(|c| { if let Node::TableRow(row) = c { parse_table_row(&mut table, row, cx); } }); table.span = new_span(val.position, cx); BlockNode::Table(table) } Node::FootnoteDefinition(def) => { let mut paragraph = Paragraph::default(); let prefix = format!("[{}]: ", def.identifier); paragraph.push(InlineNode::new(&prefix).marks(vec![( 0..prefix.len(), TextMark { italic: true, ..Default::default() }, )])); def.children.iter().for_each(|c| { parse_paragraph(&mut paragraph, c, cx); }); paragraph.span = new_span(def.position, cx); BlockNode::Paragraph(paragraph) } Node::Definition(def) => { cx.add_ref( def.identifier.clone().into(), LinkMark { url: def.url.clone().into(), identifier: Some(def.identifier.clone().into()), title: def.title.clone().map(Into::into), }, ); BlockNode::Definition { identifier: def.identifier.clone().into(), url: def.url.clone().into(), title: def.title.clone().map(|s| s.into()), span: new_span(def.position, cx), } } _ => { if cfg!(debug_assertions) { tracing::warn!("unsupported node: {:#?}", value); } BlockNode::Unknown } } } ================================================ FILE: crates/ui/src/text/format/mod.rs ================================================ pub(super) mod html; mod html5minify; pub(super) mod markdown; ================================================ FILE: crates/ui/src/text/inline.rs ================================================ use std::{ ops::Range, rc::Rc, sync::{Arc, Mutex}, }; use gpui::{ App, BorderStyle, Bounds, CursorStyle, Edges, Element, ElementId, GlobalElementId, Half, HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, StyledText, TextLayout, Window, point, px, quad, }; use crate::{ActiveTheme, global_state::GlobalState, input::Selection, text::node::LinkMark}; /// A inline element used to render a inline text and support selectable. /// /// All text in TextView (including the CodeBlock) used this for text rendering. pub(super) struct Inline { id: ElementId, text: SharedString, links: Rc, LinkMark)>>, highlights: Vec<(Range, HighlightStyle)>, styled_text: StyledText, state: Arc>, } /// The inline text state, used RefCell to keep the selection state. #[derive(Debug, Default, PartialEq)] pub(crate) struct InlineState { hovered_index: Option, /// The text that actually rendering, matched with selection. pub(super) text: SharedString, pub(super) selection: Option, } impl InlineState { /// Save actually rendered text for selected text to use. pub(crate) fn set_text(&mut self, text: SharedString) { self.text = text; } } impl Inline { pub(super) fn new( id: impl Into, state: Arc>, links: Vec<(Range, LinkMark)>, highlights: Vec<(Range, HighlightStyle)>, ) -> Self { let text = state.lock().unwrap().text.clone(); Self { id: id.into(), links: Rc::new(links), highlights, text: text.clone(), styled_text: StyledText::new(text), state, } } /// Get link at given mouse position. fn link_for_position( layout: &TextLayout, links: &Vec<(Range, LinkMark)>, position: Point, ) -> Option { let offset = layout.index_for_position(position).ok()?; for (range, link) in links.iter() { if range.contains(&offset) { return Some(link.clone()); } } None } /// Paint selected bounds for debug. #[allow(unused)] fn paint_selected_bounds(&self, bounds: Bounds, window: &mut Window, cx: &mut App) { window.paint_quad(gpui::PaintQuad { bounds, background: cx.theme().blue.alpha(0.01).into(), corner_radii: gpui::Corners::default(), border_color: gpui::transparent_black(), border_style: BorderStyle::default(), border_widths: gpui::Edges::all(px(0.)), }); } fn layout_selections( &self, text_layout: &TextLayout, window: &mut Window, cx: &mut App, ) -> (bool, bool, Option) { let Some(text_view_state) = GlobalState::global(cx).text_view_state() else { return (false, false, None); }; let text_view_state = text_view_state.read(cx); let is_selectable = text_view_state.is_selectable(); if !text_view_state.has_selection() { return (is_selectable, false, None); } let Some((selection_start, selection_end)) = text_view_state.selection_points() else { return (is_selectable, false, None); }; let line_height = window.line_height(); // Use for debug selection bounds // self.paint_selected_bounds(Bounds::from_corners(selection_start, selection_end), window, cx); let mut selection: Option = None; let mut offset = 0; let mut chars = self.text.chars().peekable(); while let Some(c) = chars.next() { let Some(pos) = text_layout.position_for_index(offset) else { offset += c.len_utf8(); continue; }; let mut char_width = line_height.half(); if let Some(next_pos) = text_layout.position_for_index(offset + 1) { if next_pos.y == pos.y { char_width = next_pos.x - pos.x; } } if point_in_text_selection(pos, char_width, selection_start, selection_end, line_height) { if selection.is_none() { selection = Some((offset..offset).into()); } let next_offset = offset + c.len_utf8(); selection.as_mut().unwrap().end = next_offset; } offset += c.len_utf8(); } (true, true, selection) } /// Paint the selection background. fn paint_selection( selection: &Selection, text_layout: &TextLayout, bounds: &Bounds, window: &mut Window, cx: &mut App, ) { let mut start = selection.start; let mut end = selection.end; if end < start { std::mem::swap(&mut start, &mut end); } let Some(start_position) = text_layout.position_for_index(start) else { return; }; let Some(end_position) = text_layout.position_for_index(end) else { return; }; let line_height = text_layout.line_height(); if start_position.y == end_position.y { window.paint_quad(quad( Bounds::from_corners( start_position, point(end_position.x, end_position.y + line_height), ), px(0.), cx.theme().selection, Edges::default(), gpui::transparent_black(), BorderStyle::default(), )); } else { window.paint_quad(quad( Bounds::from_corners( start_position, point(bounds.right(), start_position.y + line_height), ), px(0.), cx.theme().selection, Edges::default(), gpui::transparent_black(), BorderStyle::default(), )); if end_position.y > start_position.y + line_height { window.paint_quad(quad( Bounds::from_corners( point(bounds.left(), start_position.y + line_height), point(bounds.right(), end_position.y), ), px(0.), cx.theme().selection, Edges::default(), gpui::transparent_black(), BorderStyle::default(), )); } window.paint_quad(quad( Bounds::from_corners( point(bounds.left(), end_position.y), point(end_position.x, end_position.y + line_height), ), px(0.), cx.theme().selection, Edges::default(), gpui::transparent_black(), BorderStyle::default(), )); } } } impl IntoElement for Inline { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for Inline { type RequestLayoutState = (); type PrepaintState = Hitbox; fn id(&self) -> Option { Some(self.id.clone()) } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, global_element_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let text_style = window.text_style(); let mut runs = Vec::new(); let mut ix = 0; for (range, highlight) in self.highlights.iter() { if ix < range.start { runs.push(text_style.clone().to_run(range.start - ix)); } runs.push(text_style.clone().highlight(*highlight).to_run(range.len())); ix = range.end; } if ix < self.text.len() { runs.push(text_style.to_run(self.text.len() - ix)); } self.styled_text = StyledText::new(self.text.clone()).with_runs(runs); let (layout_id, _) = self.styled_text .request_layout(global_element_id, inspector_id, window, cx); (layout_id, ()) } fn prepaint( &mut self, id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { self.styled_text .prepaint(id, inspector_id, bounds, &mut (), window, cx); let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); hitbox } fn paint( &mut self, global_id: Option<&GlobalElementId>, _: Option<&InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { let current_view = window.current_view(); let hitbox = prepaint; let mut state = self.state.lock().unwrap(); let text_layout = self.styled_text.layout().clone(); self.styled_text .paint(global_id, None, bounds, &mut (), &mut (), window, cx); // layout selections let (is_selectable, is_selection, selection) = self.layout_selections(&text_layout, window, cx); state.selection = selection; if is_selection || is_selectable { window.set_cursor_style(CursorStyle::IBeam, &hitbox); } // link cursor pointer let mouse_position = window.mouse_position(); if let Some(_) = Self::link_for_position(&text_layout, &self.links, mouse_position) { window.set_cursor_style(CursorStyle::PointingHand, &hitbox); } if let Some(selection) = &state.selection { Self::paint_selection(selection, &text_layout, &bounds, window, cx); } // mouse move, update hovered link window.on_mouse_event({ let hitbox = hitbox.clone(); let text_layout = text_layout.clone(); let mut hovered_index = state.hovered_index; move |event: &MouseMoveEvent, phase, window, cx| { if !phase.bubble() || !hitbox.is_hovered(window) { return; } let current = hovered_index; let updated = text_layout.index_for_position(event.position).ok(); // notify update when hovering over different links if current != updated { hovered_index = updated; cx.notify(current_view); } } }); if !is_selection { // click to open link window.on_mouse_event({ let links = self.links.clone(); let text_layout = text_layout.clone(); let hitbox = hitbox.clone(); move |event: &MouseUpEvent, phase, window, cx| { if !phase.bubble() || !hitbox.is_hovered(window) { return; } if let Some(link) = Self::link_for_position(&text_layout, &links, event.position) { cx.stop_propagation(); cx.open_url(&link.url); } } }); } } } /// Check if a `pos` is within a `bounds`, considering multi-line selections. fn point_in_text_selection( pos: Point, char_width: Pixels, selection_start: Point, selection_end: Point, line_height: Pixels, ) -> bool { let point_in_line = |point: Point| point.y >= pos.y && point.y < pos.y + line_height; let top = selection_start.y.min(selection_end.y); let bottom = selection_start.y.max(selection_end.y); let x = pos.x + char_width.half(); // Out of the vertical bounds if pos.y + line_height <= top || pos.y > bottom { return false; } // Treat the selection as single-line when both drag points fall within the // same rendered line, even if their y coordinates differ inside that line. if point_in_line(selection_start) && point_in_line(selection_end) { let left = selection_start.x.min(selection_end.x); let right = selection_start.x.max(selection_end.x); return x >= left && x <= right; } let (top_point, bottom_point) = if selection_start.y < selection_end.y { (selection_start, selection_end) } else { (selection_end, selection_start) }; let is_top_line = point_in_line(top_point); let is_bottom_line = point_in_line(bottom_point); if is_top_line { return x >= top_point.x; } else if is_bottom_line { return x <= bottom_point.x; } else { return true; } } #[cfg(test)] mod tests { use super::point_in_text_selection; use gpui::{point, px}; #[test] fn test_point_in_text_selection() { let line_height = px(20.); let char_width = px(10.); let start = point(px(50.), px(50.)); let end = point(px(150.), px(150.)); // First line but haft line height, true // | p --------| // | selection | // |-----------| assert!(point_in_text_selection( point(px(50.), px(40.)), char_width, start, end, line_height )); // First line in selection, true // | p --------| // | selection | // |-----------| assert!(point_in_text_selection( point(px(50.), px(50.)), char_width, start, end, line_height )); // First line, but left out of selection, false // p |-----------| // | selection | // |-----------| assert!(!point_in_text_selection( point(px(40.), px(50.)), char_width, start, end, line_height )); // First line but right out of selection, true // |-----------| p // | selection | // |-----------| assert!(point_in_text_selection( point(px(160.), px(50.)), char_width, start, end, line_height )); // Middle line in selection, true // |-----------| // | p | // |-----------| assert!(point_in_text_selection( point(px(100.), px(70.)), char_width, start, end, line_height )); // Middle line, but left out of selection, true // |-----------| // p | selection | // |-----------| assert!(point_in_text_selection( point(px(40.), px(70.)), char_width, start, end, line_height )); // Middle line, but right out of selection, true // |-----------| // | selection | p // |-----------| assert!(point_in_text_selection( point(px(160.), px(70.)), char_width, start, end, line_height )); // Last line in selection, true // |-----------| // | selection | // |------- p -| assert!(point_in_text_selection( point(px(100.), px(140.)), char_width, start, end, line_height )); // Last line, but left out of selection, true // // |-----------| // | selection | // p |-----------| assert!(point_in_text_selection( point(px(40.), px(140.)), char_width, start, end, line_height )); // Last line, but right out of selection, false // |-----------| // | selection | // |-----------| p assert!(!point_in_text_selection( point(px(160.), px(140.)), char_width, start, end, line_height )); // Out of vertical bounds (top), false // p // |-----------| // | selection | // |-----------| assert!(!point_in_text_selection( point(px(100.), px(20.)), char_width, start, end, line_height )); // Out of vertical bounds (bottom), false // |-----------| // | selection | // |-----------| // p assert!(!point_in_text_selection( point(px(100.), px(160.)), char_width, start, end, line_height )); } #[test] fn test_point_in_text_selection_reversed_drag_direction() { let line_height = px(20.); let char_width = px(10.); // Mouse down on lower line then drag upward to x=150. // Top line should follow current mouse x, bottom line should keep anchor x. let start = point(px(80.), px(150.)); let end = point(px(150.), px(50.)); // On top line, selection starts from top cursor x (150), so x=140 should be excluded. assert!(!point_in_text_selection( point(px(140.), px(50.)), char_width, start, end, line_height )); assert!(point_in_text_selection( point(px(150.), px(50.)), char_width, start, end, line_height )); // On bottom line, selection ends at anchor x (80), so x=90 should be excluded. assert!(point_in_text_selection( point(px(75.), px(140.)), char_width, start, end, line_height )); assert!(!point_in_text_selection( point(px(80.), px(140.)), char_width, start, end, line_height )); } #[test] fn test_point_in_text_selection_same_visual_line_with_different_y() { let line_height = px(20.); let char_width = px(10.); let start = point(px(100.), px(55.)); let end = point(px(60.), px(58.)); assert!(!point_in_text_selection( point(px(40.), px(50.)), char_width, start, end, line_height )); assert!(point_in_text_selection( point(px(70.), px(50.)), char_width, start, end, line_height )); assert!(!point_in_text_selection( point(px(110.), px(50.)), char_width, start, end, line_height )); } #[test] fn test_point_in_text_selection_same_visual_line_with_reversed_y() { let line_height = px(20.); let char_width = px(10.); let start = point(px(60.), px(58.)); let end = point(px(100.), px(55.)); assert!(!point_in_text_selection( point(px(40.), px(50.)), char_width, start, end, line_height )); assert!(point_in_text_selection( point(px(70.), px(50.)), char_width, start, end, line_height )); assert!(!point_in_text_selection( point(px(110.), px(50.)), char_width, start, end, line_height )); } } ================================================ FILE: crates/ui/src/text/mod.rs ================================================ mod document; mod format; mod inline; mod node; mod state; mod style; mod text_view; mod utils; use gpui::{App, ElementId, IntoElement, RenderOnce, SharedString, Window}; pub use state::*; pub use style::*; pub use text_view::*; pub(crate) fn init(cx: &mut App) { state::init(cx); } /// Create a new markdown text view with code location as id. #[track_caller] pub fn markdown(source: impl Into) -> TextView { let id: ElementId = ElementId::CodeLocation(*std::panic::Location::caller()); TextView::markdown(id, source) } /// Create a new html text view with code location as id. #[track_caller] pub fn html(source: impl Into) -> TextView { let id: ElementId = ElementId::CodeLocation(*std::panic::Location::caller()); TextView::html(id, source) } #[derive(IntoElement, Clone)] pub enum Text { String(SharedString), TextView(Box), } impl From for Text { fn from(s: SharedString) -> Self { Self::String(s) } } impl From<&str> for Text { fn from(s: &str) -> Self { Self::String(SharedString::from(s.to_string())) } } impl From for Text { fn from(s: String) -> Self { Self::String(s.into()) } } impl From for Text { fn from(e: TextView) -> Self { Self::TextView(Box::new(e)) } } impl Text { /// Set the style for [`TextView`]. /// /// Do nothing if this is `String`. pub fn style(self, style: TextViewStyle) -> Self { match self { Self::String(s) => Self::String(s), Self::TextView(e) => Self::TextView(Box::new(e.style(style))), } } /// Get the text content. pub(crate) fn get_text(&self, cx: &App) -> SharedString { match self { Self::String(s) => s.clone(), Self::TextView(view) => { if let Some(state) = &view.state { state.read(cx).source() } else { SharedString::default() } } } } } impl RenderOnce for Text { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { match self { Self::String(s) => s.into_any_element(), Self::TextView(e) => e.into_any_element(), } } } ================================================ FILE: crates/ui/src/text/node.rs ================================================ use std::{ collections::HashMap, ops::Range, sync::{Arc, Mutex}, }; use gpui::{ AnyElement, App, DefiniteLength, Div, ElementId, FontStyle, FontWeight, Half, HighlightStyle, InteractiveElement as _, IntoElement, Length, ObjectFit, ParentElement, SharedString, SharedUri, StatefulInteractiveElement, Styled, StyledImage as _, Window, div, img, prelude::FluentBuilder as _, px, relative, rems, }; use markdown::mdast; use ropey::Rope; use crate::{ ActiveTheme as _, Icon, IconName, StyledExt, h_flex, highlighter::{HighlightTheme, SyntaxHighlighter}, text::{ CodeBlockActionsFn, document::NodeRenderOptions, inline::{Inline, InlineState}, }, tooltip::Tooltip, v_flex, }; use super::{TextViewStyle, utils::list_item_prefix}; /// The block-level nodes. #[derive(Debug, Clone, PartialEq)] pub(crate) enum BlockNode { /// Something like a Div container in HTML. Root { children: Vec, span: Option, }, Paragraph(Paragraph), Heading { level: u8, children: Paragraph, span: Option, }, Blockquote { children: Vec, span: Option, }, List { /// Only contains ListItem, others will be ignored children: Vec, ordered: bool, span: Option, }, ListItem { children: Vec, spread: bool, /// Whether the list item is checked, if None, it's not a checkbox checked: Option, span: Option, }, CodeBlock(CodeBlock), Table(Table), Break { html: bool, span: Option, }, Divider { span: Option, }, /// Use for to_markdown get raw definition Definition { identifier: SharedString, url: SharedString, title: Option, span: Option, }, Unknown, } impl BlockNode { pub(super) fn is_list_item(&self) -> bool { matches!(self, Self::ListItem { .. }) } pub(super) fn is_break(&self) -> bool { matches!(self, Self::Break { .. }) } /// Combine all children, omitting the empt parent nodes. pub(super) fn compact(self) -> BlockNode { match self { Self::Root { mut children, .. } if children.len() == 1 => children.remove(0).compact(), _ => self, } } /// Get the span of the node. pub(super) fn span(&self) -> Option { match self { BlockNode::Root { span, .. } => *span, BlockNode::Paragraph(paragraph) => paragraph.span, BlockNode::Heading { span, .. } => *span, BlockNode::Blockquote { span, .. } => *span, BlockNode::List { span, .. } => *span, BlockNode::ListItem { span, .. } => *span, BlockNode::CodeBlock(code_block) => code_block.span, BlockNode::Table(table) => table.span, BlockNode::Break { span, .. } => *span, BlockNode::Divider { span, .. } => *span, BlockNode::Definition { span, .. } => *span, BlockNode::Unknown { .. } => None, } } pub(super) fn selected_text(&self) -> String { let mut text = String::new(); match self { BlockNode::Root { children, .. } => { let mut block_text = String::new(); for c in children.iter() { block_text.push_str(&c.selected_text()); } if !block_text.is_empty() { text.push_str(&block_text); text.push('\n'); } } BlockNode::Paragraph(paragraph) => { let mut block_text = String::new(); block_text.push_str(¶graph.selected_text()); if !block_text.is_empty() { text.push_str(&block_text); text.push('\n'); } } BlockNode::Heading { children, .. } => { let mut block_text = String::new(); block_text.push_str(&children.selected_text()); if !block_text.is_empty() { text.push_str(&block_text); text.push('\n'); } } BlockNode::List { children, .. } => { for c in children.iter() { text.push_str(&c.selected_text()); } } BlockNode::ListItem { children, .. } => { for c in children.iter() { text.push_str(&c.selected_text()); } } BlockNode::Blockquote { children, .. } => { let mut block_text = String::new(); for c in children.iter() { block_text.push_str(&c.selected_text()); } if !block_text.is_empty() { text.push_str(&block_text); text.push('\n'); } } BlockNode::Table(table) => { let mut block_text = String::new(); for row in table.children.iter() { let mut row_texts = vec![]; for cell in row.children.iter() { row_texts.push(cell.children.selected_text()); } if !row_texts.is_empty() { block_text.push_str(&row_texts.join(" ")); block_text.push('\n'); } } if !block_text.is_empty() { text.push_str(&block_text); text.push('\n'); } } BlockNode::CodeBlock(code_block) => { let block_text = code_block.selected_text(); if !block_text.is_empty() { text.push_str(&block_text); text.push('\n'); } } BlockNode::Definition { .. } | BlockNode::Break { .. } | BlockNode::Divider { .. } | BlockNode::Unknown { .. } => {} } text } } #[allow(unused)] #[derive(Debug, Default, Clone, PartialEq)] pub struct LinkMark { pub url: SharedString, /// Optional identifier for footnotes. pub identifier: Option, pub title: Option, } #[derive(Debug, Default, Clone, PartialEq)] pub struct TextMark { pub bold: bool, pub italic: bool, pub strikethrough: bool, pub code: bool, pub link: Option, } impl TextMark { pub fn bold(mut self) -> Self { self.bold = true; self } pub fn italic(mut self) -> Self { self.italic = true; self } pub fn strikethrough(mut self) -> Self { self.strikethrough = true; self } pub fn code(mut self) -> Self { self.code = true; self } pub fn link(mut self, link: impl Into) -> Self { self.link = Some(link.into()); self } pub fn merge(&mut self, other: TextMark) { self.bold |= other.bold; self.italic |= other.italic; self.strikethrough |= other.strikethrough; self.code |= other.code; if let Some(link) = other.link { self.link = Some(link); } } } /// The bytes #[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct Span { pub start: usize, pub end: usize, } impl From for ElementId { fn from(value: Span) -> Self { ElementId::Name(format!("md-{}:{}", value.start, value.end).into()) } } #[allow(unused)] #[derive(Debug, Default, Clone)] pub struct ImageNode { pub url: SharedUri, pub link: Option, pub title: Option, pub alt: Option, pub width: Option, pub height: Option, } impl ImageNode { pub fn title(&self) -> String { self.title .clone() .unwrap_or_else(|| self.alt.clone().unwrap_or_default()) .to_string() } } impl PartialEq for ImageNode { fn eq(&self, other: &Self) -> bool { self.url == other.url && self.link == other.link && self.title == other.title && self.alt == other.alt && self.width == other.width && self.height == other.height } } #[derive(Default, Clone, Debug)] pub(crate) struct InlineNode { /// The text content. pub(crate) text: SharedString, pub(crate) image: Option, /// The text styles, each tuple contains the range of the text and the style. pub(crate) marks: Vec<(Range, TextMark)>, state: Arc>, } impl PartialEq for InlineNode { fn eq(&self, other: &Self) -> bool { self.text == other.text && self.image == other.image && self.marks == other.marks } } impl InlineNode { pub(crate) fn new(text: impl Into) -> Self { Self { text: text.into(), image: None, marks: vec![], state: Arc::new(Mutex::new(InlineState::default())), } } pub(crate) fn image(image: ImageNode) -> Self { let mut this = Self::new(""); this.image = Some(image); this } pub(crate) fn marks(mut self, marks: Vec<(Range, TextMark)>) -> Self { self.marks = marks; self } } /// The paragraph element, contains multiple text nodes. /// /// Unlike other Element, this is cloneable, because it is used in the Node AST. /// We are keep the selection state inside this AST Nodes. #[derive(Debug, Clone, Default)] pub(crate) struct Paragraph { pub(super) span: Option, pub(super) children: Vec, /// The link references in this paragraph, used for reference links. /// /// The key is the identifier, the value is the url. pub(super) link_refs: HashMap, pub(crate) state: Arc>, } impl PartialEq for Paragraph { fn eq(&self, other: &Self) -> bool { self.span == other.span && self.children == other.children && self.link_refs == other.link_refs } } impl Paragraph { pub(crate) fn new(text: String) -> Self { Self { span: None, children: vec![InlineNode::new(&text)], link_refs: HashMap::new(), state: Arc::new(Mutex::new(InlineState::default())), } } pub(super) fn selected_text(&self) -> String { let mut text = String::new(); for c in self.children.iter() { let state = c.state.lock().unwrap(); if let Some(selection) = &state.selection { let part_text = state.text.clone(); text.push_str(&part_text[selection.start..selection.end]); } } let state = self.state.lock().unwrap(); if let Some(selection) = &state.selection { let all_text = state.text.clone(); text.push_str(&all_text[selection.start..selection.end]); } text } } #[derive(Debug, Clone, Default, PartialEq)] pub(crate) struct Table { pub(crate) children: Vec, pub(crate) column_aligns: Vec, pub(crate) span: Option, } impl Table { pub(crate) fn column_align(&self, index: usize) -> ColumnumnAlign { self.column_aligns.get(index).copied().unwrap_or_default() } } #[derive(Debug, Default, Copy, Clone, PartialEq)] pub(crate) enum ColumnumnAlign { #[default] Left, Center, Right, } impl From for ColumnumnAlign { fn from(value: mdast::AlignKind) -> Self { match value { mdast::AlignKind::None => ColumnumnAlign::Left, mdast::AlignKind::Left => ColumnumnAlign::Left, mdast::AlignKind::Center => ColumnumnAlign::Center, mdast::AlignKind::Right => ColumnumnAlign::Right, } } } #[derive(Debug, Clone, Default, PartialEq)] pub(crate) struct TableRow { pub children: Vec, } #[derive(Debug, Clone, Default, PartialEq)] pub(crate) struct TableCell { pub children: Paragraph, pub width: Option, } impl Paragraph { pub(crate) fn take(&mut self) -> Paragraph { std::mem::replace( self, Paragraph { span: None, children: vec![], link_refs: Default::default(), state: Arc::new(Mutex::new(InlineState::default())), }, ) } pub(crate) fn is_image(&self) -> bool { false } pub(crate) fn set_span(&mut self, span: Span) { self.span = Some(span); } pub(crate) fn push_str(&mut self, text: &str) { self.children.push( InlineNode::new(text.to_string()).marks(vec![(0..text.len(), TextMark::default())]), ); } pub(crate) fn push(&mut self, text: InlineNode) { self.children.push(text); } pub(crate) fn push_image(&mut self, image: ImageNode) { self.children.push(InlineNode::image(image)); } pub(crate) fn is_empty(&self) -> bool { self.children.is_empty() || self .children .iter() .all(|node| node.text.is_empty() && node.image.is_none()) } /// Return length of children text. pub(crate) fn text_len(&self) -> usize { self.children .iter() .map(|node| node.text.len()) .sum::() } pub(crate) fn merge(&mut self, other: Self) { self.children.extend(other.children); } } #[derive(Debug, Clone)] pub struct CodeBlock { lang: Option, styles: Vec<(Range, HighlightStyle)>, state: Arc>, pub span: Option, } impl PartialEq for CodeBlock { fn eq(&self, other: &Self) -> bool { self.lang == other.lang && self.styles == other.styles } } impl CodeBlock { /// Get the language of the code block. pub fn lang(&self) -> Option { self.lang.clone() } /// Get the code content of the code block. pub fn code(&self) -> SharedString { self.state.lock().unwrap().text.clone() } pub(crate) fn new( code: SharedString, lang: Option, highlight_theme: &HighlightTheme, span: Option>, ) -> Self { let mut styles = vec![]; if let Some(lang) = &lang { let mut highlighter = SyntaxHighlighter::new(&lang); highlighter.update(None, &Rope::from_str(code.as_str()), None); styles = highlighter.styles(&(0..code.len()), highlight_theme); }; let state = Arc::new(Mutex::new(InlineState::default())); state.lock().unwrap().set_text(code); Self { lang, styles, state, span: span.map(|s| s.into()), } } pub(super) fn selected_text(&self) -> String { let mut text = String::new(); let state = self.state.lock().unwrap(); if let Some(selection) = &state.selection { let part_text = state.text.clone(); text.push_str(&part_text[selection.start..selection.end]); } text } fn render( &self, options: &NodeRenderOptions, node_cx: &NodeContext, window: &mut Window, cx: &mut App, ) -> AnyElement { let style = &node_cx.style; div() .when(!options.is_last, |this| this.pb(style.paragraph_gap)) .child( div() .id(("codeblock", options.ix)) .p_3() .rounded(cx.theme().radius) .bg(cx.theme().muted) .font_family(cx.theme().mono_font_family.clone()) .text_size(cx.theme().mono_font_size) .relative() .refine_style(&style.code_block) .child(Inline::new( "code", self.state.clone(), vec![], self.styles.clone(), )) .when_some(node_cx.code_block_actions.clone(), |this, actions| { this.child( div() .id("actions") .absolute() .top_2() .right_2() .bg(cx.theme().muted) .rounded(cx.theme().radius) .child(actions(&self, window, cx)), ) }), ) .into_any_element() } } /// A context for rendering nodes, contains link references. #[derive(Default, Clone)] pub(crate) struct NodeContext { /// The byte offset of the node in the original markdown text. /// Used for incremental updates. pub(crate) offset: usize, pub(crate) link_refs: HashMap, pub(crate) style: TextViewStyle, pub(crate) code_block_actions: Option>, } impl NodeContext { pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) { self.link_refs.insert(identifier, link); } } impl PartialEq for NodeContext { fn eq(&self, other: &Self) -> bool { self.link_refs == other.link_refs && self.style == other.style // Note: code_block_buttons is intentionally not compared (closures can't be compared) } } impl Paragraph { fn render( &self, node_cx: &NodeContext, _window: &mut Window, cx: &mut App, ) -> impl IntoElement { let span = self.span; let children = &self.children; let mut child_nodes: Vec = vec![]; let mut text = String::new(); let mut highlights: Vec<(Range, HighlightStyle)> = vec![]; let mut links: Vec<(Range, LinkMark)> = vec![]; let mut offset = 0; let mut ix = 0; for inline_node in children { let text_len = inline_node.text.len(); text.push_str(&inline_node.text); if let Some(image) = &inline_node.image { if text.len() > 0 { inline_node .state .lock() .unwrap() .set_text(text.clone().into()); child_nodes.push( Inline::new( ix, inline_node.state.clone(), links.clone(), highlights.clone(), ) .into_any_element(), ); } child_nodes.push( img(image.url.clone()) .id(ix) .object_fit(ObjectFit::Contain) .max_w(relative(1.)) .when_some(image.width, |this, width| this.w(width)) .when_some(image.link.clone(), |this, link| { let title = image.title(); this.cursor_pointer() .tooltip(move |window, cx| { Tooltip::new(title.clone()).build(window, cx) }) .on_click(move |_, _, cx| { cx.stop_propagation(); cx.open_url(&link.url); }) }) .into_any_element(), ); text.clear(); links.clear(); highlights.clear(); offset = 0; } else { let mut node_highlights = vec![]; for (range, style) in &inline_node.marks { let inner_range = (offset + range.start)..(offset + range.end); let mut highlight = HighlightStyle::default(); if style.bold { highlight.font_weight = Some(FontWeight::BOLD); } if style.italic { highlight.font_style = Some(FontStyle::Italic); } if style.strikethrough { highlight.strikethrough = Some(gpui::StrikethroughStyle { thickness: gpui::px(1.), ..Default::default() }); } if style.code { highlight.background_color = Some(cx.theme().accent); } if let Some(mut link_mark) = style.link.clone() { highlight.color = Some(cx.theme().link); highlight.underline = Some(gpui::UnderlineStyle { thickness: gpui::px(1.), ..Default::default() }); // convert link references, replace link if let Some(identifier) = link_mark.identifier.as_ref() { if let Some(mark) = node_cx.link_refs.get(identifier) { link_mark = mark.clone(); } } links.push((inner_range.clone(), link_mark)); } node_highlights.push((inner_range, highlight)); } highlights = gpui::combine_highlights(highlights, node_highlights).collect(); offset += text_len; } ix += 1; } // Add the last text node if text.len() > 0 { self.state.lock().unwrap().set_text(text.into()); child_nodes .push(Inline::new(ix, self.state.clone(), links, highlights).into_any_element()); } div().id(span.unwrap_or_default()).children(child_nodes) } } impl Paragraph { fn to_markdown(&self) -> String { let mut text = self .children .iter() .map(|text_node| { let mut text = text_node.text.to_string(); for (range, style) in &text_node.marks { if style.bold { text = format!("**{}**", &text_node.text[range.clone()]); } if style.italic { text = format!("*{}*", &text_node.text[range.clone()]); } if style.strikethrough { text = format!("~~{}~~", &text_node.text[range.clone()]); } if style.code { text = format!("`{}`", &text_node.text[range.clone()]); } if let Some(link) = &style.link { text = format!("[{}]({})", &text_node.text[range.clone()], link.url); } } if let Some(image) = &text_node.image { let alt = image.alt.clone().unwrap_or_default(); let title = image .title .clone() .map_or(String::new(), |t| format!(" \"{}\"", t)); text.push_str(&format!("![{}]({}{})", alt, image.url, title)) } text }) .collect::>() .join(""); text.push_str("\n\n"); text } } impl BlockNode { /// Converts the node to markdown format. /// /// This is used to generate markdown for test. #[allow(dead_code)] pub(crate) fn to_markdown(&self) -> String { match self { BlockNode::Root { children, .. } => children .iter() .map(|child| child.to_markdown()) .collect::>() .join("\n\n"), BlockNode::Paragraph(paragraph) => paragraph.to_markdown(), BlockNode::Heading { level, children, .. } => { let hashes = "#".repeat(*level as usize); format!("{} {}", hashes, children.to_markdown()) } BlockNode::Blockquote { children, .. } => { let content = children .iter() .map(|child| child.to_markdown()) .collect::>() .join("\n\n"); content .lines() .map(|line| format!("> {}", line)) .collect::>() .join("\n") } BlockNode::List { children, ordered, .. } => children .iter() .enumerate() .map(|(i, child)| { let prefix = if *ordered { format!("{}. ", i + 1) } else { "- ".to_string() }; format!("{}{}", prefix, child.to_markdown()) }) .collect::>() .join("\n"), BlockNode::ListItem { children, checked, .. } => { let checkbox = if let Some(checked) = checked { if *checked { "[x] " } else { "[ ] " } } else { "" }; format!( "{}{}", checkbox, children .iter() .map(|child| child.to_markdown()) .collect::>() .join("\n") ) } BlockNode::CodeBlock(code_block) => { format!( "```{}\n{}\n```", code_block.lang.clone().unwrap_or_default(), code_block.code() ) } BlockNode::Table(table) => { let header = table .children .first() .map(|row| { row.children .iter() .map(|cell| cell.children.to_markdown()) .collect::>() .join(" | ") }) .unwrap_or_default(); let alignments = table .column_aligns .iter() .map(|align| { match align { ColumnumnAlign::Left => ":--", ColumnumnAlign::Center => ":-:", ColumnumnAlign::Right => "--:", } .to_string() }) .collect::>() .join(" | "); let rows = table .children .iter() .skip(1) .map(|row| { row.children .iter() .map(|cell| cell.children.to_markdown()) .collect::>() .join(" | ") }) .collect::>() .join("\n"); format!("{}\n{}\n{}", header, alignments, rows) } BlockNode::Break { html, .. } => { if *html { "
".to_string() } else { "\n".to_string() } } BlockNode::Divider { .. } => "---".to_string(), BlockNode::Definition { identifier, url, title, .. } => { if let Some(title) = title { format!("[{}]: {} \"{}\"", identifier, url, title) } else { format!("[{}]: {}", identifier, url) } } BlockNode::Unknown { .. } => "".to_string(), } .trim() .to_string() } } impl BlockNode { fn render_list_item( item: &BlockNode, ix: usize, options: NodeRenderOptions, node_cx: &NodeContext, window: &mut Window, cx: &mut App, ) -> AnyElement { match item { BlockNode::ListItem { children, spread, checked, .. } => v_flex() .id(("li", options.ix)) .w_full() .min_w_0() .when(*spread, |this| this.child(div())) .children({ let mut items: Vec
= Vec::with_capacity(children.len()); for (child_ix, child) in children.iter().enumerate() { match child { BlockNode::Paragraph { .. } => { let last_not_list = child_ix > 0 && !matches!(children[child_ix - 1], BlockNode::List { .. }); let text = child.render_block( NodeRenderOptions { depth: options.depth + 1, todo: checked.is_some(), is_last: true, ..options }, node_cx, window, cx, ); // merge content into last item. if last_not_list { if let Some(item_item) = items.last_mut() { item_item.extend(vec![ div() .flex_1() .min_w_0() .overflow_hidden() .child(text) .into_any_element(), ]); continue; } } items.push( h_flex() .w_full() .flex_1() .min_w_0() .relative() .items_start() .content_start() .when(!options.todo && checked.is_none(), |this| { this.child(list_item_prefix( ix, options.ordered, options.depth, )) }) .when_some(*checked, |this, checked| { // Todo list checkbox this.child( div() .flex() .mt(rems(0.4)) .mr_1p5() .size(rems(0.875)) .items_center() .justify_center() .rounded(cx.theme().radius.half()) .border_1() .border_color(cx.theme().primary) .text_color(cx.theme().primary_foreground) .when(checked, |this| { this.bg(cx.theme().primary).child( Icon::new(IconName::Check) .size_2() .text_xs(), ) }), ) }) .child( div() .flex_1() .min_w_0() .overflow_hidden() .child(text), ), ); } BlockNode::List { .. } => { items.push(div().ml(rems(1.)).child(child.render_block( NodeRenderOptions { depth: options.depth + 1, todo: checked.is_some(), is_last: true, ..options }, node_cx, window, cx, ))); } _ => {} } } items }) .into_any_element(), _ => div().into_any_element(), } } fn render_table( item: &BlockNode, options: &NodeRenderOptions, node_cx: &NodeContext, window: &mut Window, cx: &mut App, ) -> impl IntoElement { const DEFAULT_LENGTH: usize = 5; const MAX_LENGTH: usize = 150; let col_lens = match item { BlockNode::Table(table) => { let mut col_lens = vec![]; for row in table.children.iter() { for (ix, cell) in row.children.iter().enumerate() { if col_lens.len() <= ix { col_lens.push(DEFAULT_LENGTH); } let len = cell.children.text_len(); if len > col_lens[ix] { col_lens[ix] = len; } } } col_lens } _ => vec![], }; match item { BlockNode::Table(table) => div() .pb(rems(1.)) .w_full() .child( div() .id(("table", options.ix)) .w_full() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .children({ let mut rows = Vec::with_capacity(table.children.len()); for (row_ix, row) in table.children.iter().enumerate() { rows.push( div() .id("row") .w_full() .when(row_ix < table.children.len() - 1, |this| { this.border_b_1() }) .border_color(cx.theme().border) .flex() .flex_row() .children({ let mut cells = Vec::with_capacity(row.children.len()); for (ix, cell) in row.children.iter().enumerate() { let align = table.column_align(ix); let is_last_col = ix == row.children.len() - 1; let len = col_lens .get(ix) .copied() .unwrap_or(MAX_LENGTH) .min(MAX_LENGTH); cells.push( div() .id("cell") .flex() .when( align == ColumnumnAlign::Center, |this| this.justify_center(), ) .when( align == ColumnumnAlign::Right, |this| this.justify_end(), ) .w(Length::Definite(relative(len as f32))) .px_2() .py_1() .when(!is_last_col, |this| { this.border_r_1() .border_color(cx.theme().border) }) .truncate() .child( cell.children .render(node_cx, window, cx), ), ) } cells }), ) } rows }), ) .into_any_element(), _ => div().into_any_element(), } } pub(crate) fn render_block( &self, options: NodeRenderOptions, node_cx: &NodeContext, window: &mut Window, cx: &mut App, ) -> AnyElement { let ix = options.ix; let mb = if options.in_list || options.is_last { rems(0.) } else { node_cx.style.paragraph_gap }; match self { BlockNode::Root { children, .. } => div() .id(("div", ix)) .children(children.into_iter().enumerate().map(move |(ix, node)| { node.render_block(NodeRenderOptions { ix, ..options }, node_cx, window, cx) })) .into_any_element(), BlockNode::Paragraph(paragraph) => div() .id(("p", ix)) .pb(mb) .child(paragraph.render(node_cx, window, cx)) .into_any_element(), BlockNode::Heading { level, children, .. } => { let (text_size, font_weight) = match level { 1 => (rems(2.), FontWeight::BOLD), 2 => (rems(1.5), FontWeight::SEMIBOLD), 3 => (rems(1.25), FontWeight::SEMIBOLD), 4 => (rems(1.125), FontWeight::SEMIBOLD), 5 => (rems(1.), FontWeight::SEMIBOLD), 6 => (rems(1.), FontWeight::MEDIUM), _ => (rems(1.), FontWeight::NORMAL), }; let mut text_size = text_size.to_pixels(node_cx.style.heading_base_font_size); if let Some(f) = node_cx.style.heading_font_size.as_ref() { text_size = (f)(*level, node_cx.style.heading_base_font_size); } h_flex() .id(SharedString::from(format!("h{}-{}", level, ix))) .pb(rems(0.3)) .whitespace_normal() .text_size(text_size) .font_weight(font_weight) .child(children.render(node_cx, window, cx)) .into_any_element() } BlockNode::Blockquote { children, .. } => div() .w_full() .pb(mb) .child( div() .id(("blockquote", ix)) .w_full() .text_color(cx.theme().muted_foreground) .border_l_3() .border_color(cx.theme().secondary_active) .px_4() .children({ let children_len = children.len(); children.into_iter().enumerate().map(move |(index, c)| { let is_last = index == children_len - 1; c.render_block(options.is_last(is_last), node_cx, window, cx) }) }), ) .into_any_element(), BlockNode::List { children, ordered, .. } => v_flex() .id((if *ordered { "ol" } else { "ul" }, ix)) .pb(mb) .children({ let mut items = Vec::with_capacity(children.len()); let mut item_index = 0; for (ix, item) in children.into_iter().enumerate() { let is_item = item.is_list_item(); items.push(Self::render_list_item( item, item_index, NodeRenderOptions { ix, ordered: *ordered, ..options }, node_cx, window, cx, )); if is_item { item_index += 1; } } items }) .into_any_element(), BlockNode::CodeBlock(code_block) => code_block.render(&options, node_cx, window, cx), BlockNode::Table { .. } => { Self::render_table(self, &options, node_cx, window, cx).into_any_element() } BlockNode::Divider { .. } => div() .pb(mb) .child(div().id("divider").bg(cx.theme().border).h(px(2.))) .into_any_element(), BlockNode::Break { .. } => div().id("break").into_any_element(), BlockNode::Unknown { .. } | BlockNode::Definition { .. } => div().into_any_element(), _ => { if cfg!(debug_assertions) { tracing::warn!("unknown implementation: {:?}", self); } div().into_any_element() } } } } ================================================ FILE: crates/ui/src/text/state.rs ================================================ use std::{ pin::Pin, task::Poll, }; use futures::Stream as _; use gpui::{ App, AppContext as _, Bounds, ClipboardItem, Context, FocusHandle, IntoElement, KeyBinding, ListState, ParentElement as _, Pixels, Point, Render, SharedString, Styled as _, Task, Window, prelude::FluentBuilder as _, px, }; use crate::{ ActiveTheme, ElementExt, async_util::{Sender, Receiver, unbounded}, highlighter::HighlightTheme, input::{self, Copy}, text::{ CodeBlockActionsFn, TextViewStyle, document::ParsedDocument, format, node::{self, NodeContext}, }, v_flex, }; const CONTEXT: &'static str = "TextView"; pub(crate) fn init(cx: &mut App) { cx.bind_keys(vec![ #[cfg(target_os = "macos")] KeyBinding::new("cmd-c", input::Copy, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-c", input::Copy, Some(CONTEXT)), ]); } /// The content format of the text view. #[derive(Clone, Copy, PartialEq, Eq)] pub(super) enum TextViewFormat { /// Markdown view Markdown, /// HTML view Html, } /// The state of a TextView. pub struct TextViewState { pub(super) focus_handle: FocusHandle, pub(super) list_state: ListState, /// The bounds of the text view bounds: Bounds, pub(super) selectable: bool, pub(super) scrollable: bool, pub(super) text_view_style: TextViewStyle, pub(super) code_block_actions: Option>, pub(super) is_selecting: bool, /// The local (in TextView) position of the selection. selection_positions: (Option>, Option>), pub(super) parsed_content: ParsedContent, text: SharedString, parsed_error: Option, tx: Sender, _parse_task: Task<()>, _receive_task: Task<()>, } impl TextViewState { /// Create a Markdown TextViewState. pub fn markdown(text: &str, cx: &mut Context) -> Self { Self::new(TextViewFormat::Markdown, text, cx) } /// Create a HTML TextViewState. pub fn html(text: &str, cx: &mut Context) -> Self { Self::new(TextViewFormat::Html, text, cx) } /// Create a new TextViewState. fn new(format: TextViewFormat, text: &str, cx: &mut Context) -> Self { let focus_handle = cx.focus_handle(); let (tx, rx) = unbounded::(); let (tx_result, rx_result) = unbounded::>(); let _receive_task = cx.spawn({ async move |weak_self, cx| { while let Ok(parsed_result) = rx_result.recv().await { _ = weak_self.update(cx, |state, cx| { match parsed_result { Ok(content) => { state.parsed_content = content; state.parsed_error = None; } Err(err) => { state.parsed_error = Some(err); } } state.clear_selection(); cx.notify(); }); } } }); let _parse_task = cx.background_spawn(UpdateFuture::new(format, rx, tx_result, cx)); let mut this = Self { focus_handle, bounds: Bounds::default(), selection_positions: (None, None), selectable: false, scrollable: false, list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)), text_view_style: TextViewStyle::default(), code_block_actions: None, is_selecting: false, parsed_content: Default::default(), parsed_error: None, text: text.to_string().into(), tx, _parse_task, _receive_task, }; this.increment_update(&text, false, cx); this } /// Get the text content. pub(crate) fn source(&self) -> SharedString { self.parsed_content.document.source.clone() } /// Set whether the text is selectable, default false. pub fn selectable(mut self, selectable: bool) -> Self { self.selectable = selectable; self } /// Set whether the text is selectable, default false. pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context) { self.selectable = selectable; cx.notify(); } /// Set whether the text is selectable, default false. pub fn scrollable(mut self, scrollable: bool) -> Self { self.scrollable = scrollable; self } /// Set whether the text is selectable, default false. pub fn set_scrollable(&mut self, scrollable: bool, cx: &mut Context) { self.scrollable = scrollable; cx.notify(); } /// Set the text content. pub fn set_text(&mut self, text: &str, cx: &mut Context) { if self.text.as_str() == text { return; } self.text = text.to_string().into(); self.parsed_error = None; self.increment_update(text, false, cx); } /// Append partial text content to the existing text. pub fn push_str(&mut self, new_text: &str, cx: &mut Context) { if new_text.is_empty() { return; } self.increment_update(new_text, true, cx); } /// Return the selected text. pub fn selected_text(&self) -> String { self.parsed_content.document.selected_text() } fn increment_update(&mut self, text: &str, append: bool, cx: &mut Context) { let update_options = UpdateOptions { append, content: self.parsed_content.clone(), pending_text: text.to_string(), highlight_theme: cx.theme().highlight_theme.clone(), }; _ = self.tx.try_send(update_options); } /// Save bounds and unselect if bounds changed. pub(super) fn update_bounds(&mut self, bounds: Bounds) { if self.bounds.size != bounds.size { self.clear_selection(); } self.bounds = bounds; } pub(super) fn clear_selection(&mut self) { self.selection_positions = (None, None); self.is_selecting = false; } pub(super) fn start_selection(&mut self, pos: Point) { // Store content coordinates (not affected by scrolling) let scroll_offset = if self.scrollable { self.list_state.scroll_px_offset_for_scrollbar() } else { Point::default() }; let pos = pos - self.bounds.origin - scroll_offset; self.selection_positions = (Some(pos), Some(pos)); self.is_selecting = true; } pub(super) fn update_selection(&mut self, pos: Point) { let scroll_offset = if self.scrollable { self.list_state.scroll_px_offset_for_scrollbar() } else { Point::default() }; let pos = pos - self.bounds.origin - scroll_offset; if let (Some(start), Some(_)) = self.selection_positions { self.selection_positions = (Some(start), Some(pos)) } } pub(super) fn end_selection(&mut self) { self.is_selecting = false; } pub(crate) fn has_selection(&self) -> bool { if let (Some(start), Some(end)) = self.selection_positions { start != end } else { false } } /// Return the selection start/end in window coordinates. pub(crate) fn selection_points(&self) -> Option<(Point, Point)> { let scroll_offset = if self.scrollable { self.list_state.scroll_px_offset_for_scrollbar() } else { Point::default() }; selection_points( self.selection_positions.0, self.selection_positions.1, self.bounds, scroll_offset, ) } pub(super) fn on_action_copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { let selected_text = self.selected_text().trim().to_string(); if selected_text.is_empty() { return; } cx.write_to_clipboard(ClipboardItem::new_string(selected_text)); } pub(crate) fn is_selectable(&self) -> bool { self.selectable } } impl Render for TextViewState { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let state = cx.entity(); let document = self.parsed_content.document.clone(); let mut node_cx = self.parsed_content.node_cx.clone(); node_cx.code_block_actions = self.code_block_actions.clone(); node_cx.style = self.text_view_style.clone(); v_flex() .size_full() .map(|this| match &mut self.parsed_error { None => this.child(document.render_root( if self.scrollable { Some(self.list_state.clone()) } else { None }, &node_cx, window, cx, )), Some(err) => this.child( v_flex() .gap_1() .child("Failed to parse content") .child(err.to_string()), ), }) .on_prepaint(move |bounds, _, cx| { state.update(cx, |state, _| { state.update_bounds(bounds); }) }) } } #[derive(Clone, PartialEq, Default)] pub(crate) struct ParsedContent { pub(crate) document: ParsedDocument, pub(crate) node_cx: node::NodeContext, } struct UpdateFuture { format: TextViewFormat, options: UpdateOptions, pending_text: String, rx: Pin>>, tx_result: Sender>, } impl UpdateFuture { fn new( format: TextViewFormat, rx: Receiver, tx_result: Sender>, cx: &App, ) -> Self { Self { format, pending_text: String::new(), options: UpdateOptions { append: false, pending_text: String::new(), content: Default::default(), highlight_theme: cx.theme().highlight_theme.clone(), }, rx: Box::pin(rx), tx_result, } } } impl Future for UpdateFuture { type Output = (); fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { loop { match self.rx.as_mut().poll_next(cx) { Poll::Ready(Some(options)) => { if options.append { self.pending_text.push_str(options.pending_text.as_str()); } else { self.pending_text = options.pending_text.clone(); } self.options = options; // Process immediately without debounce let pending_text = std::mem::take(&mut self.pending_text); let res = parse_content( self.format, &UpdateOptions { pending_text, ..self.options.clone() }, ); _ = self.tx_result.try_send(res); continue; } Poll::Ready(None) => return Poll::Ready(()), Poll::Pending => return Poll::Pending, } } } } #[derive(Clone)] struct UpdateOptions { content: ParsedContent, pending_text: String, append: bool, highlight_theme: std::sync::Arc, } fn parse_content(format: TextViewFormat, options: &UpdateOptions) -> Result { let mut node_cx = NodeContext { ..NodeContext::default() }; let mut content = options.content.clone(); let mut source = String::new(); if options.append && let Some(last_block) = content.document.blocks.pop() && let Some(span) = last_block.span() { node_cx.offset = span.start; let last_source = &content.document.source[span.start..]; source.push_str(last_source); source.push_str(&options.pending_text); } else { source = options.pending_text.to_string(); } let new_document = match format { TextViewFormat::Markdown => { format::markdown::parse(&source, &mut node_cx, &options.highlight_theme) } TextViewFormat::Html => format::html::parse(&source, &mut node_cx), }?; if options.append { content.document.source = format!("{}{}", content.document.source, options.pending_text).into(); content.document.blocks.extend(new_document.blocks); } else { content.document = new_document; } Ok(content) } fn selection_points( start: Option>, end: Option>, bounds: Bounds, scroll_offset: Point, ) -> Option<(Point, Point)> { if let (Some(start), Some(end)) = (start, end) { // Convert content coordinates to window coordinates let start = start + scroll_offset + bounds.origin; let end = end + scroll_offset + bounds.origin; return Some((start, end)); } None } #[cfg(test)] mod tests { use super::*; use gpui::point; #[test] fn test_text_view_state_selection_points() { assert_eq!( selection_points(None, None, Default::default(), Point::default()), None ); assert_eq!( selection_points( None, Some(point(px(10.), px(20.))), Default::default(), Point::default() ), None ); assert_eq!( selection_points( Some(point(px(10.), px(20.))), None, Default::default(), Point::default() ), None ); // 10,10 start // |------| // | | // |------| // 50,50 assert_eq!( selection_points( Some(point(px(10.), px(10.))), Some(point(px(50.), px(50.))), Default::default(), Point::default() ), Some((point(px(10.), px(10.)), point(px(50.), px(50.)))) ); // 10,10 // |------| // | | // |------| // 50,50 start assert_eq!( selection_points( Some(point(px(50.), px(50.))), Some(point(px(10.), px(10.))), Default::default(), Point::default() ), Some((point(px(50.), px(50.)), point(px(10.), px(10.)))) ); // 50,10 start // |------| // | | // |------| // 10,50 assert_eq!( selection_points( Some(point(px(50.), px(10.))), Some(point(px(10.), px(50.))), Default::default(), Point::default() ), Some((point(px(50.), px(10.)), point(px(10.), px(50.)))) ); // 50,10 // |------| // | | // |------| // 10,50 start assert_eq!( selection_points( Some(point(px(10.), px(50.))), Some(point(px(50.), px(10.))), Default::default(), Point::default() ), Some((point(px(10.), px(50.)), point(px(50.), px(10.)))) ); } } ================================================ FILE: crates/ui/src/text/style.rs ================================================ use std::sync::Arc; use gpui::{Pixels, Rems, StyleRefinement, px, rems}; use crate::highlighter::HighlightTheme; /// TextViewStyle used to customize the style for [`TextView`]. #[derive(Clone)] pub struct TextViewStyle { /// Gap of each paragraphs, default is 1 rem. pub paragraph_gap: Rems, /// Base font size for headings, default is 14px. pub heading_base_font_size: Pixels, /// Function to calculate heading font size based on heading level (1-6). /// /// The first parameter is the heading level (1-6), the second parameter is the base font size. /// The second parameter is the base font size. pub heading_font_size: Option Pixels + Send + Sync + 'static>>, /// Highlight theme for code blocks. Default: [`HighlightTheme::default_light()`] pub highlight_theme: Arc, /// The style refinement for code blocks. pub code_block: StyleRefinement, pub is_dark: bool, } impl PartialEq for TextViewStyle { fn eq(&self, other: &Self) -> bool { self.paragraph_gap == other.paragraph_gap && self.heading_base_font_size == other.heading_base_font_size && self.highlight_theme == other.highlight_theme } } impl Default for TextViewStyle { fn default() -> Self { Self { paragraph_gap: rems(1.), heading_base_font_size: px(14.), heading_font_size: None, highlight_theme: HighlightTheme::default_light().clone(), code_block: StyleRefinement::default(), is_dark: false, } } } impl TextViewStyle { /// Set paragraph gap, default is 1 rem. pub fn paragraph_gap(mut self, gap: Rems) -> Self { self.paragraph_gap = gap; self } pub fn heading_font_size(mut self, f: F) -> Self where F: Fn(u8, Pixels) -> Pixels + Send + Sync + 'static, { self.heading_font_size = Some(Arc::new(f)); self } /// Set style for code blocks. pub fn code_block(mut self, style: StyleRefinement) -> Self { self.code_block = style; self } } ================================================ FILE: crates/ui/src/text/text_view.rs ================================================ use std::sync::Arc; use gpui::prelude::FluentBuilder as _; use gpui::{ AnyElement, App, Bounds, Element, ElementId, Entity, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, InteractiveElement, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, SharedString, StyleRefinement, Styled, Window, div, }; use crate::StyledExt; use crate::scroll::ScrollableElement; use crate::text::TextViewFormat; use crate::text::node::CodeBlock; use crate::text::state::TextViewState; use crate::{global_state::GlobalState, text::TextViewStyle}; /// Type for code block actions generator function. pub(crate) type CodeBlockActionsFn = dyn Fn(&CodeBlock, &mut Window, &mut App) -> AnyElement + Send + Sync; /// A text view that can render Markdown or HTML. /// /// ## Goals /// /// - Provide a rich text rendering component for such as Markdown or HTML, /// used to display rich text in GPUI application (e.g., Help messages, Release notes) /// - Support Markdown GFM and HTML (Simple HTML like Safari Reader Mode) for showing most common used markups. /// - Support Heading, Paragraph, Bold, Italic, StrikeThrough, Code, Link, Image, Blockquote, List, Table, HorizontalRule, CodeBlock ... /// /// ## Not Goals /// /// - Customization of the complex style (some simple styles will be supported) /// - As a Markdown editor or viewer (If you want to like this, you must fork your version). /// - As a HTML viewer, we not support CSS, we only support basic HTML tags for used to as a content reader. /// /// See also [`MarkdownElement`], [`HtmlElement`] #[derive(Clone)] pub struct TextView { id: ElementId, format: Option, text: Option, pub(crate) state: Option>, text_view_style: TextViewStyle, style: StyleRefinement, selectable: bool, scrollable: bool, code_block_actions: Option>, } impl Styled for TextView { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl TextView { /// Create new TextView with managed state. pub fn new(state: &Entity) -> Self { Self { id: ElementId::Name(state.entity_id().to_string().into()), state: Some(state.clone()), format: None, text: None, text_view_style: TextViewStyle::default(), style: StyleRefinement::default(), selectable: false, scrollable: false, code_block_actions: None, } } /// Create a new markdown text view. pub fn markdown(id: impl Into, markdown: impl Into) -> Self { Self { id: id.into(), format: Some(TextViewFormat::Markdown), text: Some(markdown.into()), text_view_style: TextViewStyle::default(), style: StyleRefinement::default(), state: None, selectable: false, scrollable: false, code_block_actions: None, } } /// Create a new html text view. pub fn html(id: impl Into, html: impl Into) -> Self { Self { id: id.into(), format: Some(TextViewFormat::Html), text: Some(html.into()), text_view_style: TextViewStyle::default(), style: StyleRefinement::default(), state: None, selectable: false, scrollable: false, code_block_actions: None, } } /// Set [`TextViewStyle`]. pub fn style(mut self, style: TextViewStyle) -> Self { self.text_view_style = style; self } /// Set the text view to be selectable, default is false. pub fn selectable(mut self, selectable: bool) -> Self { self.selectable = selectable; self } /// Set the text view to be scrollable, default is false. /// /// ## If true for `scrollable` /// /// The `scrollable` mode used for large content, /// will show scrollbar, but requires the parent to have a fixed height, /// and use [`gpui::list`] to render the content in a virtualized way. /// /// ## If false to fit content /// /// The TextView will expand to fit all content, no scrollbar. /// This mode is suitable for small content, such as a few lines of text, a label, etc. pub fn scrollable(mut self, scrollable: bool) -> Self { self.scrollable = scrollable; self } /// Set custom block actions for code blocks. /// /// The closure receives the [`CodeBlock`], /// and returns an element to display. pub fn code_block_actions(mut self, f: F) -> Self where F: Fn(&CodeBlock, &mut Window, &mut App) -> E + Send + Sync + 'static, E: IntoElement, { self.code_block_actions = Some(Arc::new(move |code_block, window, cx| { f(&code_block, window, cx).into_any_element() })); self } } impl IntoElement for TextView { type Element = Self; fn into_element(self) -> Self::Element { self } } pub struct TextViewLayoutState { state: Entity, element: AnyElement, } impl Element for TextView { type RequestLayoutState = TextViewLayoutState; type PrepaintState = Hitbox; fn id(&self) -> Option { Some(self.id.clone()) } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let state = if let Some(state) = self.state.clone() { state } else { let default_format = self.format.unwrap_or(TextViewFormat::Markdown); let default_text = self.text.clone().unwrap_or_default(); let state = window.use_keyed_state( SharedString::from(format!("{}/state", self.id)), cx, move |_, cx| { if default_format == TextViewFormat::Markdown { TextViewState::markdown(default_text.as_str(), cx) } else { TextViewState::html(default_text.as_str(), cx) } }, ); self.state = Some(state.clone()); state }; state.update(cx, |state, cx| { state.code_block_actions = self.code_block_actions.clone(); state.selectable = self.selectable; state.scrollable = self.scrollable; state.text_view_style = self.text_view_style.clone(); if let Some(text) = self.text.clone() { state.set_text(text.as_str(), cx); } }); let focus_handle = state.read(cx).focus_handle.clone(); let list_state = state.read(cx).list_state.clone(); let mut el = div() .key_context("TextView") .track_focus(&focus_handle) .when(self.scrollable, |this| { this.size_full().vertical_scrollbar(&list_state) }) .relative() .on_action(window.listener_for(&state, TextViewState::on_action_copy)) .child(state.clone()) .refine_style(&self.style) .into_any_element(); let layout_id = el.request_layout(window, cx); (layout_id, TextViewLayoutState { state, element: el }) } fn prepaint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { request_layout.element.prepaint(window, cx); window.insert_hitbox(bounds, HitboxBehavior::Normal) } fn paint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, _bounds: Bounds, request_layout: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { let state = &request_layout.state; GlobalState::global_mut(cx) .text_view_state_stack .push(state.clone()); request_layout.element.paint(window, cx); GlobalState::global_mut(cx).text_view_state_stack.pop(); if self.selectable { let is_selecting = state.read(cx).is_selecting; let has_selection = state.read(cx).has_selection(); let parent_view_id = window.current_view(); window.on_mouse_event({ let state = state.clone(); let hitbox = hitbox.clone(); move |event: &MouseDownEvent, phase, window, cx| { if !phase.bubble() || !hitbox.is_hovered(window) { return; } state.update(cx, |state, _| { state.start_selection(event.position); }); cx.notify(parent_view_id); } }); if is_selecting { // move to update end position. window.on_mouse_event({ let state = state.clone(); move |event: &MouseMoveEvent, phase, _, cx| { if !phase.bubble() { return; } state.update(cx, |state, _| { state.update_selection(event.position); }); cx.notify(parent_view_id); } }); // up to end selection window.on_mouse_event({ let state = state.clone(); move |_: &MouseUpEvent, phase, _, cx| { if !phase.bubble() { return; } state.update(cx, |state, _| { state.end_selection(); }); cx.notify(parent_view_id); } }); } if has_selection { // down outside to clear selection window.on_mouse_event({ let state = state.clone(); let hitbox = hitbox.clone(); move |_: &MouseDownEvent, _, window, cx| { if hitbox.is_hovered(window) { return; } state.update(cx, |state, _| { state.clear_selection(); }); cx.notify(parent_view_id); } }); } } } } #[cfg(test)] mod tests { use super::TextView; use crate::text::TextViewState; use gpui::{ AppContext as _, Context, Entity, IntoElement, Modifiers, MouseButton, ParentElement as _, Render, Styled as _, TestAppContext, VisualTestContext, Window, div, point, px, }; struct TextViewTestRoot { text_view: Entity, } impl TextViewTestRoot { fn new(text: &str, cx: &mut Context) -> Self { let text = text.to_string(); let text_view = cx.new(|cx| TextViewState::markdown(&text, cx)); Self { text_view } } } impl Render for TextViewTestRoot { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div() .w(px(160.)) .child( div() .h(px(24.)) .overflow_hidden() .child(TextView::new(&self.text_view).selectable(true)), ) .child(div().h(px(40.)).child("footer")) } } #[gpui::test] fn clipped_markdown_link_does_not_open(cx: &mut TestAppContext) { cx.update(crate::init); let (_, cx) = cx.add_window_view(|_, cx| { TextViewTestRoot::new("visible\n\n[hidden](https://example.com)", cx) }); let cx: &mut VisualTestContext = cx; cx.simulate_click(point(px(10.), px(34.)), Modifiers::default()); assert_eq!(cx.opened_url(), None); } #[gpui::test] fn clipped_markdown_cannot_start_selection(cx: &mut TestAppContext) { cx.update(crate::init); let (view, cx) = cx .add_window_view(|_, cx| TextViewTestRoot::new("visible\n\nhidden selection text", cx)); let cx: &mut VisualTestContext = cx; cx.simulate_mouse_down( point(px(10.), px(34.)), MouseButton::Left, Modifiers::default(), ); cx.simulate_mouse_move( point(px(90.), px(34.)), Some(MouseButton::Left), Modifiers::default(), ); cx.simulate_mouse_up( point(px(90.), px(34.)), MouseButton::Left, Modifiers::default(), ); let selected_text = view.read_with(cx, |root, cx| root.text_view.read(cx).selected_text()); assert!( selected_text.is_empty(), "unexpected selection: {selected_text:?}" ); } } ================================================ FILE: crates/ui/src/text/utils.rs ================================================ const NUMBERED_PREFIXES_1: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const NUMBERED_PREFIXES_2: &str = "abcdefghijklmnopqrstuvwxyz"; const BULLETS: [&str; 5] = ["•", "◦", "▪", "‣", "⁃"]; /// Returns the prefix for a list item. pub(super) fn list_item_prefix(ix: usize, ordered: bool, depth: usize) -> String { if ordered { if depth == 0 { return format!("{}. ", ix + 1); } if depth == 1 { return format!( "{}. ", NUMBERED_PREFIXES_1 .chars() .nth(ix % NUMBERED_PREFIXES_1.len()) .unwrap() ); } else { return format!( "{}. ", NUMBERED_PREFIXES_2 .chars() .nth(ix % NUMBERED_PREFIXES_2.len()) .unwrap() ); } } else { let depth = depth.min(BULLETS.len() - 1); let bullet = BULLETS[depth]; return format!("{} ", bullet); } } #[cfg(test)] mod tests { use crate::text::utils::list_item_prefix; #[test] fn test_list_item_prefix() { assert_eq!(list_item_prefix(0, true, 0), "1. "); assert_eq!(list_item_prefix(1, true, 0), "2. "); assert_eq!(list_item_prefix(2, true, 0), "3. "); assert_eq!(list_item_prefix(10, true, 0), "11. "); assert_eq!(list_item_prefix(0, true, 1), "A. "); assert_eq!(list_item_prefix(1, true, 1), "B. "); assert_eq!(list_item_prefix(2, true, 1), "C. "); assert_eq!(list_item_prefix(0, true, 2), "a. "); assert_eq!(list_item_prefix(1, true, 2), "b. "); assert_eq!(list_item_prefix(6, true, 2), "g. "); assert_eq!(list_item_prefix(0, true, 1), "A. "); assert_eq!(list_item_prefix(0, true, 2), "a. "); assert_eq!(list_item_prefix(0, false, 0), "• "); assert_eq!(list_item_prefix(0, false, 1), "◦ "); assert_eq!(list_item_prefix(0, false, 2), "▪ "); assert_eq!(list_item_prefix(0, false, 3), "‣ "); assert_eq!(list_item_prefix(0, false, 4), "⁃ "); } } ================================================ FILE: crates/ui/src/theme/color.rs ================================================ use std::{collections::HashMap, fmt::Display}; use gpui::{Hsla, SharedString, hsla}; use serde::{Deserialize, Deserializer, de::Error as _}; use anyhow::{Error, Result, anyhow}; /// Create a [`gpui::Hsla`] color. /// /// - h: 0..360.0 /// - s: 0.0..100.0 /// - l: 0.0..100.0 #[inline] pub fn hsl(h: f32, s: f32, l: f32) -> Hsla { hsla(h / 360., s / 100.0, l / 100.0, 1.0) } pub trait Colorize: Sized { /// Returns a new color with the given opacity. /// /// The opacity is a value between 0.0 and 1.0, where 0.0 is fully transparent and 1.0 is fully opaque. fn opacity(&self, opacity: f32) -> Self; /// Returns a new color with each channel divided by the given divisor. /// /// The divisor in range of 0.0 .. 1.0 fn divide(&self, divisor: f32) -> Self; /// Return inverted color fn invert(&self) -> Self; /// Return inverted lightness fn invert_l(&self) -> Self; /// Return a new color with the lightness increased by the given factor. /// /// factor range: 0.0 .. 1.0 fn lighten(&self, amount: f32) -> Self; /// Return a new color with the darkness increased by the given factor. /// /// factor range: 0.0 .. 1.0 fn darken(&self, amount: f32) -> Self; /// Return a new color with the same lightness and alpha but different hue and saturation. fn apply(&self, base_color: Self) -> Self; /// Mix two colors together, the `factor` is a value between 0.0 and 1.0 for first color. fn mix(&self, other: Self, factor: f32) -> Self; /// Mix two colors together in Oklab color space, the `factor` is a value between 0.0 and 1.0 for first color. /// /// This is similar to CSS `color-mix(in oklab, color1 factor%, color2)`. fn mix_oklab(&self, other: Self, factor: f32) -> Self; /// Change the `Hue` of the color by the given in range: 0.0 .. 1.0 fn hue(&self, hue: f32) -> Self; /// Change the `Saturation` of the color by the given value in range: 0.0 .. 1.0 fn saturation(&self, saturation: f32) -> Self; /// Change the `Lightness` of the color by the given value in range: 0.0 .. 1.0 fn lightness(&self, lightness: f32) -> Self; /// Convert the color to a hex string. For example, "#F8FAFC". fn to_hex(&self) -> String; /// Parse a hex string to a color. fn parse_hex(hex: &str) -> Result; } /// Helper functions for Oklab color space conversions mod oklab { use gpui::Rgba; /// Convert sRGB component to linear RGB #[inline] fn to_linear(c: f32) -> f32 { if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) } } /// Convert linear RGB component to sRGB #[inline] fn from_linear(c: f32) -> f32 { if c <= 0.0031308 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 } } /// Convert RGB to Oklab color space #[allow(non_snake_case)] pub fn rgb_to_oklab(rgb: Rgba) -> (f32, f32, f32) { // sRGB to linear RGB let lr = to_linear(rgb.r); let lg = to_linear(rgb.g); let lb = to_linear(rgb.b); // Linear RGB to LMS let l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb; let m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb; let s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb; // LMS to Oklab (using cube root) let l_ = l.cbrt(); let m_ = m.cbrt(); let s_ = s.cbrt(); let L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_; let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_; let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_; (L, a, b) } /// Convert Oklab to RGB color space #[allow(non_snake_case)] pub fn oklab_to_rgb(L: f32, a: f32, b: f32) -> Rgba { // Oklab to LMS let l_ = L + 0.3963377774 * a + 0.2158037573 * b; let m_ = L - 0.1055613458 * a - 0.0638541728 * b; let s_ = L - 0.0894841775 * a - 1.2914855480 * b; let l = l_ * l_ * l_; let m = m_ * m_ * m_; let s = s_ * s_ * s_; // LMS to Linear RGB let lr = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; let lg = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; let lb = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s; // Linear RGB to sRGB Rgba { r: from_linear(lr).clamp(0.0, 1.0), g: from_linear(lg).clamp(0.0, 1.0), b: from_linear(lb).clamp(0.0, 1.0), a: 1.0, } } } impl Colorize for Hsla { fn opacity(&self, factor: f32) -> Self { Self { a: self.a * factor.clamp(0.0, 1.0), ..*self } } fn divide(&self, divisor: f32) -> Self { Self { a: divisor, ..*self } } fn invert(&self) -> Self { Self { h: 1.0 - self.h, s: 1.0 - self.s, l: 1.0 - self.l, a: self.a, } } fn invert_l(&self) -> Self { Self { l: 1.0 - self.l, ..*self } } fn lighten(&self, factor: f32) -> Self { let l = self.l * (1.0 + factor.clamp(0.0, 1.0)); Hsla { l, ..*self } } fn darken(&self, factor: f32) -> Self { let l = self.l * (1.0 - factor.clamp(0.0, 1.0)); Self { l, ..*self } } fn apply(&self, new_color: Self) -> Self { Hsla { h: new_color.h, s: new_color.s, l: self.l, a: self.a, } } /// Reference: /// https://github.com/bevyengine/bevy/blob/85eceb022da0326b47ac2b0d9202c9c9f01835bb/crates/bevy_color/src/hsla.rs#L112 fn mix(&self, other: Self, factor: f32) -> Self { let factor = factor.clamp(0.0, 1.0); let inv = 1.0 - factor; #[inline] fn lerp_hue(a: f32, b: f32, t: f32) -> f32 { let diff = (b - a + 180.0).rem_euclid(360.) - 180.; (a + diff * t).rem_euclid(360.0) } Hsla { h: lerp_hue(self.h * 360., other.h * 360., factor) / 360., s: self.s * factor + other.s * inv, l: self.l * factor + other.l * inv, a: self.a * factor + other.a * inv, } } #[allow(non_snake_case)] fn mix_oklab(&self, other: Self, factor: f32) -> Self { let factor = factor.clamp(0.0, 1.0); let inv = 1.0 - factor; // Interpolate alpha first let result_alpha = self.a * factor + other.a * inv; // Handle the case where result alpha is zero if result_alpha == 0.0 { return Self { h: 0.0, s: 0.0, l: 0.0, a: 0.0, }; } // Convert both colors to RGB let rgb1 = self.to_rgb(); let rgb2 = other.to_rgb(); // Convert to Oklab color space let (l1, a1, b1) = oklab::rgb_to_oklab(rgb1); let (l2, a2, b2) = oklab::rgb_to_oklab(rgb2); // Premultiply alpha in Oklab space (using alpha-premultiplied interpolation) // This matches CSS color-mix behavior let alpha1 = self.a; let alpha2 = other.a; // Premultiply let l1_pm = l1 * alpha1; let a1_pm = a1 * alpha1; let b1_pm = b1 * alpha1; let l2_pm = l2 * alpha2; let a2_pm = a2 * alpha2; let b2_pm = b2 * alpha2; // Interpolate premultiplied values let L_pm = l1_pm * factor + l2_pm * inv; let a_pm = a1_pm * factor + a2_pm * inv; let b_pm = b1_pm * factor + b2_pm * inv; // Unpremultiply let L = L_pm / result_alpha; let a = a_pm / result_alpha; let b = b_pm / result_alpha; // Convert back to RGB let mut rgb = oklab::oklab_to_rgb(L, a, b); rgb.a = result_alpha; // Convert RGB to HSLA rgb.into() } fn to_hex(&self) -> String { let rgb = self.to_rgb(); if rgb.a < 1. { return format!( "#{:02X}{:02X}{:02X}{:02X}", ((rgb.r * 255.) as u32), ((rgb.g * 255.) as u32), ((rgb.b * 255.) as u32), ((self.a * 255.) as u32) ); } format!( "#{:02X}{:02X}{:02X}", ((rgb.r * 255.) as u32), ((rgb.g * 255.) as u32), ((rgb.b * 255.) as u32) ) } fn parse_hex(hex: &str) -> Result { let hex = hex.trim_start_matches('#'); let len = hex.len(); if len != 6 && len != 8 { return Err(anyhow::anyhow!("invalid hex color")); } let r = u8::from_str_radix(&hex[0..2], 16)? as f32 / 255.; let g = u8::from_str_radix(&hex[2..4], 16)? as f32 / 255.; let b = u8::from_str_radix(&hex[4..6], 16)? as f32 / 255.; let a = if len == 8 { u8::from_str_radix(&hex[6..8], 16)? as f32 / 255. } else { 1. }; let v = gpui::Rgba { r, g, b, a }; let color: Hsla = v.into(); Ok(color) } fn hue(&self, hue: f32) -> Self { let mut color = *self; color.h = hue.clamp(0., 1.); color } fn saturation(&self, saturation: f32) -> Self { let mut color = *self; color.s = saturation.clamp(0., 1.); color } fn lightness(&self, lightness: f32) -> Self { let mut color = *self; color.l = lightness.clamp(0., 1.); color } } pub(crate) static DEFAULT_COLORS: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { serde_json::from_str(include_str!("./default-colors.json")) .expect("failed to parse default-colors.json") }); type ColorScales = HashMap; mod color_scales { use std::collections::HashMap; use super::{ColorScales, ShadcnColor}; use serde::de::{Deserialize, Deserializer}; pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let mut map = HashMap::new(); for color in Vec::::deserialize(deserializer)? { map.insert(color.scale, color); } Ok(map) } } /// Enum representing the available color names. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ColorName { White, Black, Neutral, Gray, Red, Orange, Amber, Yellow, Lime, Green, Emerald, Teal, Cyan, Sky, Blue, Indigo, Violet, Purple, Fuchsia, Pink, Rose, } impl Display for ColorName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } // Strict color name parser. impl TryFrom<&str> for ColorName { type Error = anyhow::Error; fn try_from(value: &str) -> std::result::Result { match value.to_lowercase().as_str() { "white" => Ok(ColorName::White), "black" => Ok(ColorName::Black), "neutral" => Ok(ColorName::Neutral), "gray" => Ok(ColorName::Gray), "red" => Ok(ColorName::Red), "orange" => Ok(ColorName::Orange), "amber" => Ok(ColorName::Amber), "yellow" => Ok(ColorName::Yellow), "lime" => Ok(ColorName::Lime), "green" => Ok(ColorName::Green), "emerald" => Ok(ColorName::Emerald), "teal" => Ok(ColorName::Teal), "cyan" => Ok(ColorName::Cyan), "sky" => Ok(ColorName::Sky), "blue" => Ok(ColorName::Blue), "indigo" => Ok(ColorName::Indigo), "violet" => Ok(ColorName::Violet), "purple" => Ok(ColorName::Purple), "fuchsia" => Ok(ColorName::Fuchsia), "pink" => Ok(ColorName::Pink), "rose" => Ok(ColorName::Rose), _ => Err(anyhow::anyhow!("Invalid color name")), } } } impl TryFrom for ColorName { type Error = anyhow::Error; fn try_from(value: SharedString) -> std::result::Result { value.as_ref().try_into() } } impl ColorName { /// Returns all available color names. pub fn all() -> [Self; 19] { [ ColorName::Neutral, ColorName::Gray, ColorName::Red, ColorName::Orange, ColorName::Amber, ColorName::Yellow, ColorName::Lime, ColorName::Green, ColorName::Emerald, ColorName::Teal, ColorName::Cyan, ColorName::Sky, ColorName::Blue, ColorName::Indigo, ColorName::Violet, ColorName::Purple, ColorName::Fuchsia, ColorName::Pink, ColorName::Rose, ] } /// Returns the color for the given scale. /// /// The `scale` is any of `[50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]` /// falls back to 500 if out of range. pub fn scale(&self, scale: usize) -> Hsla { if self == &ColorName::White { return DEFAULT_COLORS.white.hsla; } if self == &ColorName::Black { return DEFAULT_COLORS.black.hsla; } let colors = match self { ColorName::Neutral => &DEFAULT_COLORS.neutral, ColorName::Gray => &DEFAULT_COLORS.gray, ColorName::Red => &DEFAULT_COLORS.red, ColorName::Orange => &DEFAULT_COLORS.orange, ColorName::Amber => &DEFAULT_COLORS.amber, ColorName::Yellow => &DEFAULT_COLORS.yellow, ColorName::Lime => &DEFAULT_COLORS.lime, ColorName::Green => &DEFAULT_COLORS.green, ColorName::Emerald => &DEFAULT_COLORS.emerald, ColorName::Teal => &DEFAULT_COLORS.teal, ColorName::Cyan => &DEFAULT_COLORS.cyan, ColorName::Sky => &DEFAULT_COLORS.sky, ColorName::Blue => &DEFAULT_COLORS.blue, ColorName::Indigo => &DEFAULT_COLORS.indigo, ColorName::Violet => &DEFAULT_COLORS.violet, ColorName::Purple => &DEFAULT_COLORS.purple, ColorName::Fuchsia => &DEFAULT_COLORS.fuchsia, ColorName::Pink => &DEFAULT_COLORS.pink, ColorName::Rose => &DEFAULT_COLORS.rose, _ => unreachable!(), }; if let Some(color) = colors.get(&scale) { color.hsla } else { colors.get(&500).unwrap().hsla } } } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] pub(crate) struct ShadcnColors { pub(crate) black: ShadcnColor, pub(crate) white: ShadcnColor, #[serde(with = "color_scales")] pub(crate) slate: ColorScales, #[serde(with = "color_scales")] pub(crate) gray: ColorScales, #[serde(with = "color_scales")] pub(crate) zinc: ColorScales, #[serde(with = "color_scales")] pub(crate) neutral: ColorScales, #[serde(with = "color_scales")] pub(crate) stone: ColorScales, #[serde(with = "color_scales")] pub(crate) red: ColorScales, #[serde(with = "color_scales")] pub(crate) orange: ColorScales, #[serde(with = "color_scales")] pub(crate) amber: ColorScales, #[serde(with = "color_scales")] pub(crate) yellow: ColorScales, #[serde(with = "color_scales")] pub(crate) lime: ColorScales, #[serde(with = "color_scales")] pub(crate) green: ColorScales, #[serde(with = "color_scales")] pub(crate) emerald: ColorScales, #[serde(with = "color_scales")] pub(crate) teal: ColorScales, #[serde(with = "color_scales")] pub(crate) cyan: ColorScales, #[serde(with = "color_scales")] pub(crate) sky: ColorScales, #[serde(with = "color_scales")] pub(crate) blue: ColorScales, #[serde(with = "color_scales")] pub(crate) indigo: ColorScales, #[serde(with = "color_scales")] pub(crate) violet: ColorScales, #[serde(with = "color_scales")] pub(crate) purple: ColorScales, #[serde(with = "color_scales")] pub(crate) fuchsia: ColorScales, #[serde(with = "color_scales")] pub(crate) pink: ColorScales, #[serde(with = "color_scales")] pub(crate) rose: ColorScales, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize)] pub(crate) struct ShadcnColor { #[serde(default)] pub(crate) scale: usize, #[serde(deserialize_with = "from_hsl_channel", alias = "hslChannel")] pub(crate) hsla: Hsla, } /// Deserialize Hsla from a string in the format "210 40% 98%" fn from_hsl_channel<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let s: String = Deserialize::deserialize(deserializer).unwrap(); let mut parts = s.split_whitespace(); if parts.clone().count() != 3 { return Err(D::Error::custom( "expected hslChannel has 3 parts, e.g: '210 40% 98%'", )); } fn parse_number(s: &str) -> f32 { s.trim_end_matches('%') .parse() .expect("failed to parse number") } let (h, s, l) = ( parse_number(parts.next().unwrap()), parse_number(parts.next().unwrap()), parse_number(parts.next().unwrap()), ); Ok(hsl(h, s, l)) } macro_rules! color_method { ($color:tt, $scale:tt) => { paste::paste! { #[inline] #[allow(unused)] pub fn [<$color _ $scale>]() -> Hsla { if let Some(color) = DEFAULT_COLORS.$color.get(&($scale as usize)) { return color.hsla; } black() } } }; } macro_rules! color_methods { ($color:tt) => { paste::paste! { /// Get color by scale number. /// /// The possible scale numbers are: /// 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 /// /// If the scale number is not found, it will return black color. #[inline] pub fn [<$color>](scale: usize) -> Hsla { if let Some(color) = DEFAULT_COLORS.$color.get(&scale) { return color.hsla; } black() } } color_method!($color, 50); color_method!($color, 100); color_method!($color, 200); color_method!($color, 300); color_method!($color, 400); color_method!($color, 500); color_method!($color, 600); color_method!($color, 700); color_method!($color, 800); color_method!($color, 900); color_method!($color, 950); }; } pub fn black() -> Hsla { DEFAULT_COLORS.black.hsla } pub fn white() -> Hsla { DEFAULT_COLORS.white.hsla } color_methods!(slate); color_methods!(gray); color_methods!(zinc); color_methods!(neutral); color_methods!(stone); color_methods!(red); color_methods!(orange); color_methods!(amber); color_methods!(yellow); color_methods!(lime); color_methods!(green); color_methods!(emerald); color_methods!(teal); color_methods!(cyan); color_methods!(sky); color_methods!(blue); color_methods!(indigo); color_methods!(violet); color_methods!(purple); color_methods!(fuchsia); color_methods!(pink); color_methods!(rose); /// Try to parse the color, HEX or [Tailwind Color](https://tailwindcss.com/docs/colors) expression. /// /// # Parameter `color` should be one string value listed below: /// /// - `#RRGGBB` - The HEX color string. /// - `#RRGGBBAA` - The HEX color string with alpha. /// /// Or the Tailwind Color format: /// /// - `name` - The color name `black`, `white`, or any other defined in `crate::color`. /// - `name-scale` - The color name with scale. /// - `name/opacity` - The color name with opacity, `opacity` should be an integer between 0 and 100. /// - `name-scale/opacity` - The color name with scale and opacity. /// pub fn try_parse_color(color: &str) -> Result { if color.starts_with("#") { let rgba = gpui::Rgba::try_from(color)?; return Ok(rgba.into()); } let mut name = String::new(); let mut scale = None; let mut opacity = None; // 0: name, 1: scale, 2: opacity let mut state = 0; let mut part = String::new(); for c in color.chars() { match c { '-' if state == 0 => { name = std::mem::take(&mut part); state = 1; } '/' if state <= 1 => { if state == 0 { name = std::mem::take(&mut part); } else if state == 1 { scale = part.parse::().ok(); part.clear(); } state = 2; } _ => part.push(c), } } match state { 0 => name = part, 1 => scale = part.parse::().ok(), 2 => opacity = part.parse::().ok(), _ => {} } if name.is_empty() { return Err(anyhow!("Empty color name")); } let mut hsla = match name.as_str() { "black" => Ok::(crate::black()), "white" => Ok(crate::white()), _ => { let color_name = ColorName::try_from(name.as_str())?; if let Some(scale) = scale { Ok(color_name.scale(scale)) } else { Ok(color_name.scale(500)) } } }?; if let Some(opacity) = opacity { if opacity > 100. { return Err(anyhow!("Invalid color opacity")); } hsla = hsla.opacity(opacity / 100.); } Ok(hsla) } #[cfg(test)] mod tests { use gpui::{rgb, rgba}; use super::*; #[test] fn test_default_colors() { assert_eq!(white(), hsl(0.0, 0.0, 100.0)); assert_eq!(black(), hsl(0.0, 0.0, 0.0)); assert_eq!(slate_50(), hsl(210.0, 40.0, 98.0)); assert_eq!(slate_100(), hsl(210.0, 40.0, 96.1)); assert_eq!(slate_900(), hsl(222.2, 47.4, 11.2)); assert_eq!(red_50(), hsl(0.0, 85.7, 97.3)); assert_eq!(yellow_100(), hsl(54.9, 96.7, 88.0)); assert_eq!(green_200(), hsl(141.0, 78.9, 85.1)); assert_eq!(cyan_300(), hsl(187.0, 92.4, 69.0)); assert_eq!(blue_400(), hsl(213.1, 93.9, 67.8)); assert_eq!(indigo_500(), hsl(238.7, 83.5, 66.7)); } #[test] fn test_to_hex_string() { let color: Hsla = rgb(0xf8fafc).into(); assert_eq!(color.to_hex(), "#F8FAFC"); let color: Hsla = rgb(0xfef2f2).into(); assert_eq!(color.to_hex(), "#FEF2F2"); let color: Hsla = rgba(0x0413fcaa).into(); assert_eq!(color.to_hex(), "#0413FCAA"); } #[test] fn test_from_hex_string() { let color: Hsla = Hsla::parse_hex("#F8FAFC").unwrap(); assert_eq!(color, rgb(0xf8fafc).into()); let color: Hsla = Hsla::parse_hex("#FEF2F2").unwrap(); assert_eq!(color, rgb(0xfef2f2).into()); let color: Hsla = Hsla::parse_hex("#0413FCAA").unwrap(); assert_eq!(color, rgba(0x0413fcaa).into()); } #[test] fn test_lighten() { let color = super::hsl(240.0, 5.0, 30.0); let color = color.lighten(0.5); assert_eq!(color.l, 0.45000002); let color = color.lighten(0.5); assert_eq!(color.l, 0.675); let color = color.lighten(0.1); assert_eq!(color.l, 0.7425); } #[test] fn test_darken() { let color = super::hsl(240.0, 5.0, 96.0); let color = color.darken(0.5); assert_eq!(color.l, 0.48); let color = color.darken(0.5); assert_eq!(color.l, 0.24); } #[test] fn test_mix() { let red = Hsla::parse_hex("#FF0000").unwrap(); let blue = Hsla::parse_hex("#0000FF").unwrap(); let green = Hsla::parse_hex("#00FF00").unwrap(); let yellow = Hsla::parse_hex("#FFFF00").unwrap(); assert_eq!(red.mix(blue, 0.5).to_hex(), "#FF00FF"); assert_eq!(green.mix(red, 0.5).to_hex(), "#FFFF00"); assert_eq!(blue.mix(yellow, 0.2).to_hex(), "#0098FF"); } #[test] fn test_mix_oklab() { let red = Hsla::parse_hex("#FF0000").unwrap(); let blue = Hsla::parse_hex("#0000FF").unwrap(); let transparent = gpui::Hsla { h: 0.0, s: 0.0, l: 0.0, a: 0.0, }; // Test mixing red with transparent (similar to CSS color-mix example) // color-mix(in oklab, red 20%, transparent) should give red with 20% opacity let result = red.mix_oklab(transparent, 0.2); assert!((result.a - 0.2).abs() < 0.01); // Alpha should be 20% // The color should remain red (hue should be preserved) let rgb_result = result.to_rgb(); let rgb_red = red.to_rgb(); // Allow some tolerance due to color space conversions assert!( (rgb_result.r - rgb_red.r).abs() < 0.05, "Red channel should be preserved" ); assert!(rgb_result.g < 0.05, "Green channel should be near 0"); assert!(rgb_result.b < 0.05, "Blue channel should be near 0"); // Test basic color mixing in Oklab space let purple = red.mix_oklab(blue, 0.5); // Oklab mixing should produce different results than HSL mixing let purple_hsl = red.mix(blue, 0.5); assert_ne!(purple.to_hex(), purple_hsl.to_hex()); // Test factor boundaries (allowing small floating point errors) let result_0 = red.mix_oklab(blue, 0.0); let result_1 = red.mix_oklab(blue, 1.0); // Check that result is close to expected (within 1 color unit per channel) let rgb_0 = result_0.to_rgb(); let rgb_blue = blue.to_rgb(); assert!((rgb_0.r - rgb_blue.r).abs() < 0.01); assert!((rgb_0.g - rgb_blue.g).abs() < 0.01); assert!((rgb_0.b - rgb_blue.b).abs() < 0.01); let rgb_1 = result_1.to_rgb(); let rgb_red = red.to_rgb(); assert!((rgb_1.r - rgb_red.r).abs() < 0.01); assert!((rgb_1.g - rgb_red.g).abs() < 0.01); assert!((rgb_1.b - rgb_red.b).abs() < 0.01); } #[test] fn test_color_name() { assert_eq!(ColorName::Purple.to_string(), "Purple"); assert_eq!(format!("{}", ColorName::Green), "Green"); assert_eq!(format!("{:?}", ColorName::Yellow), "Yellow"); let color = ColorName::Green; assert_eq!(color.scale(500).to_hex(), "#21C55E"); assert_eq!(color.scale(1500).to_hex(), "#21C55E"); for name in ColorName::all().iter() { let name1: ColorName = name.to_string().as_str().try_into().unwrap(); assert_eq!(name1, *name); } } #[test] fn test_h_s_l() { let color = hsl(260., 94., 80.); assert_eq!(color.hue(200. / 360.), hsl(200., 94., 80.)); assert_eq!(color.saturation(74. / 100.), hsl(260., 74., 80.)); assert_eq!(color.lightness(74. / 100.), hsl(260., 94., 74.)); } #[test] fn test_try_parse_color() { assert_eq!( try_parse_color("#F2F200").ok(), Some(hsla(0.16666667, 1., 0.4745098, 1.0)) ); assert_eq!( try_parse_color("#00f21888").ok(), Some(hsla(0.34986225, 1.0, 0.4745098, 0.53333336)) ); assert_eq!(try_parse_color("black").ok(), Some(crate::black())); assert_eq!(try_parse_color("white-800").ok(), Some(crate::white())); assert_eq!(try_parse_color("red").ok(), Some(crate::red_500())); assert_eq!(try_parse_color("blue-600").ok(), Some(crate::blue_600())); assert_eq!( try_parse_color("pink/33").ok(), Some(crate::pink_500().opacity(0.33)) ); assert_eq!( try_parse_color("orange-300/66").ok(), Some(crate::orange_300().opacity(0.66)) ); } } ================================================ FILE: crates/ui/src/theme/default-colors.json ================================================ { "inherit": "inherit", "_doc": "https://github.com/shadcn-ui/ui/blob/a46eea77a6680fc40cee9fa1f209e77931068f35/apps/v4/public/r/colors/index.json", "current": "currentColor", "transparent": "transparent", "black": { "hex": "#000000", "rgb": "rgb(0,0,0)", "hsl": "hsl(0,0%,0%)", "oklch": "oklch(0.00,0.00,0)", "rgbChannel": "0 0 0", "hslChannel": "0 0% 0%" }, "white": { "hex": "#ffffff", "rgb": "rgb(255,255,255)", "hsl": "hsl(0,0%,100%)", "oklch": "oklch(1.00,0.00,0)", "rgbChannel": "255 255 255", "hslChannel": "0 0% 100%" }, "slate": [ { "scale": 50, "hex": "#f8fafc", "rgb": "rgb(248,250,252)", "hsl": "hsl(210,40%,98%)", "oklch": "oklch(0.98,0.00,248)", "rgbChannel": "248 250 252", "hslChannel": "210 40% 98%" }, { "scale": 100, "hex": "#f1f5f9", "rgb": "rgb(241,245,249)", "hsl": "hsl(210,40%,96.1%)", "oklch": "oklch(0.97,0.01,248)", "rgbChannel": "241 245 249", "hslChannel": "210 40% 96.1%" }, { "scale": 200, "hex": "#e2e8f0", "rgb": "rgb(226,232,240)", "hsl": "hsl(214.3,31.8%,91.4%)", "oklch": "oklch(0.93,0.01,256)", "rgbChannel": "226 232 240", "hslChannel": "214.3 31.8% 91.4%" }, { "scale": 300, "hex": "#cbd5e1", "rgb": "rgb(203,213,225)", "hsl": "hsl(212.7,26.8%,83.9%)", "oklch": "oklch(0.87,0.02,253)", "rgbChannel": "203 213 225", "hslChannel": "212.7 26.8% 83.9%" }, { "scale": 400, "hex": "#94a3b8", "rgb": "rgb(148,163,184)", "hsl": "hsl(215,20.2%,65.1%)", "oklch": "oklch(0.71,0.04,257)", "rgbChannel": "148 163 184", "hslChannel": "215 20.2% 65.1%" }, { "scale": 500, "hex": "#64748b", "rgb": "rgb(100,116,139)", "hsl": "hsl(215.4,16.3%,46.9%)", "oklch": "oklch(0.55,0.04,257)", "rgbChannel": "100 116 139", "hslChannel": "215.4 16.3% 46.9%" }, { "scale": 600, "hex": "#475569", "rgb": "rgb(71,85,105)", "hsl": "hsl(215.3,19.3%,34.5%)", "oklch": "oklch(0.45,0.04,257)", "rgbChannel": "71 85 105", "hslChannel": "215.3 19.3% 34.5%" }, { "scale": 700, "hex": "#334155", "rgb": "rgb(51,65,85)", "hsl": "hsl(215.3,25%,26.7%)", "oklch": "oklch(0.37,0.04,257)", "rgbChannel": "51 65 85", "hslChannel": "215.3 25% 26.7%" }, { "scale": 800, "hex": "#1e293b", "rgb": "rgb(30,41,59)", "hsl": "hsl(217.2,32.6%,17.5%)", "oklch": "oklch(0.28,0.04,260)", "rgbChannel": "30 41 59", "hslChannel": "217.2 32.6% 17.5%" }, { "scale": 900, "hex": "#0f172a", "rgb": "rgb(15,23,42)", "hsl": "hsl(222.2,47.4%,11.2%)", "oklch": "oklch(0.21,0.04,266)", "rgbChannel": "15 23 42", "hslChannel": "222.2 47.4% 11.2%" }, { "scale": 950, "hex": "#020617", "rgb": "rgb(2,6,23)", "hsl": "hsl(222.2,84%,4.9%)", "oklch": "oklch(0.13,0.04,265)", "rgbChannel": "2 6 23", "hslChannel": "222.2 84% 4.9%" } ], "gray": [ { "scale": 50, "hex": "#f9fafb", "rgb": "rgb(249,250,251)", "hsl": "hsl(210,20%,98%)", "oklch": "oklch(0.98,0.00,248)", "rgbChannel": "249 250 251", "hslChannel": "210 20% 98%" }, { "scale": 100, "hex": "#f3f4f6", "rgb": "rgb(243,244,246)", "hsl": "hsl(220,14.3%,95.9%)", "oklch": "oklch(0.97,0.00,265)", "rgbChannel": "243 244 246", "hslChannel": "220 14.3% 95.9%" }, { "scale": 200, "hex": "#e5e7eb", "rgb": "rgb(229,231,235)", "hsl": "hsl(220,13%,91%)", "oklch": "oklch(0.93,0.01,265)", "rgbChannel": "229 231 235", "hslChannel": "220 13% 91%" }, { "scale": 300, "hex": "#d1d5db", "rgb": "rgb(209,213,219)", "hsl": "hsl(216,12.2%,83.9%)", "oklch": "oklch(0.87,0.01,258)", "rgbChannel": "209 213 219", "hslChannel": "216 12.2% 83.9%" }, { "scale": 400, "hex": "#9ca3af", "rgb": "rgb(156,163,175)", "hsl": "hsl(217.9,10.6%,64.9%)", "oklch": "oklch(0.71,0.02,261)", "rgbChannel": "156 163 175", "hslChannel": "217.9 10.6% 64.9%" }, { "scale": 500, "hex": "#6b7280", "rgb": "rgb(107,114,128)", "hsl": "hsl(220,8.9%,46.1%)", "oklch": "oklch(0.55,0.02,264)", "rgbChannel": "107 114 128", "hslChannel": "220 8.9% 46.1%" }, { "scale": 600, "hex": "#4b5563", "rgb": "rgb(75,85,99)", "hsl": "hsl(215,13.8%,34.1%)", "oklch": "oklch(0.45,0.03,257)", "rgbChannel": "75 85 99", "hslChannel": "215 13.8% 34.1%" }, { "scale": 700, "hex": "#374151", "rgb": "rgb(55,65,81)", "hsl": "hsl(216.9,19.1%,26.7%)", "oklch": "oklch(0.37,0.03,260)", "rgbChannel": "55 65 81", "hslChannel": "216.9 19.1% 26.7%" }, { "scale": 800, "hex": "#1f2937", "rgb": "rgb(31,41,55)", "hsl": "hsl(215,27.9%,16.9%)", "oklch": "oklch(0.28,0.03,257)", "rgbChannel": "31 41 55", "hslChannel": "215 27.9% 16.9%" }, { "scale": 900, "hex": "#111827", "rgb": "rgb(17,24,39)", "hsl": "hsl(220.9,39.3%,11%)", "oklch": "oklch(0.21,0.03,265)", "rgbChannel": "17 24 39", "hslChannel": "220.9 39.3% 11%" }, { "scale": 950, "hex": "#030712", "rgb": "rgb(3,7,18)", "hsl": "hsl(224,71.4%,4.1%)", "oklch": "oklch(0.13,0.03,262)", "rgbChannel": "3 7 18", "hslChannel": "224 71.4% 4.1%" } ], "zinc": [ { "scale": 50, "hex": "#fafafa", "rgb": "rgb(250,250,250)", "hsl": "hsl(0,0%,98%)", "oklch": "oklch(0.99,0.00,0)", "rgbChannel": "250 250 250", "hslChannel": "0 0% 98%" }, { "scale": 100, "hex": "#f4f4f5", "rgb": "rgb(244,244,245)", "hsl": "hsl(240,4.8%,95.9%)", "oklch": "oklch(0.97,0.00,286)", "rgbChannel": "244 244 245", "hslChannel": "240 4.8% 95.9%" }, { "scale": 200, "hex": "#e4e4e7", "rgb": "rgb(228,228,231)", "hsl": "hsl(240,5.9%,90%)", "oklch": "oklch(0.92,0.00,286)", "rgbChannel": "228 228 231", "hslChannel": "240 5.9% 90%" }, { "scale": 300, "hex": "#d4d4d8", "rgb": "rgb(212,212,216)", "hsl": "hsl(240,4.9%,83.9%)", "oklch": "oklch(0.87,0.01,286)", "rgbChannel": "212 212 216", "hslChannel": "240 4.9% 83.9%" }, { "scale": 400, "hex": "#a1a1aa", "rgb": "rgb(161,161,170)", "hsl": "hsl(240,5%,64.9%)", "oklch": "oklch(0.71,0.01,286)", "rgbChannel": "161 161 170", "hslChannel": "240 5% 64.9%" }, { "scale": 500, "hex": "#71717a", "rgb": "rgb(113,113,122)", "hsl": "hsl(240,3.8%,46.1%)", "oklch": "oklch(0.55,0.01,286)", "rgbChannel": "113 113 122", "hslChannel": "240 3.8% 46.1%" }, { "scale": 600, "hex": "#52525b", "rgb": "rgb(82,82,91)", "hsl": "hsl(240,5.2%,33.9%)", "oklch": "oklch(0.44,0.01,286)", "rgbChannel": "82 82 91", "hslChannel": "240 5.2% 33.9%" }, { "scale": 700, "hex": "#3f3f46", "rgb": "rgb(63,63,70)", "hsl": "hsl(240,5.3%,26.1%)", "oklch": "oklch(0.37,0.01,286)", "rgbChannel": "63 63 70", "hslChannel": "240 5.3% 26.1%" }, { "scale": 800, "hex": "#27272a", "rgb": "rgb(39,39,42)", "hsl": "hsl(240,3.7%,15.9%)", "oklch": "oklch(0.27,0.01,286)", "rgbChannel": "39 39 42", "hslChannel": "240 3.7% 15.9%" }, { "scale": 900, "hex": "#18181b", "rgb": "rgb(24,24,27)", "hsl": "hsl(240,5.9%,10%)", "oklch": "oklch(0.21,0.01,286)", "rgbChannel": "24 24 27", "hslChannel": "240 5.9% 10%" }, { "scale": 950, "hex": "#09090b", "rgb": "rgb(9,9,11)", "hsl": "hsl(240,10%,3.9%)", "oklch": "oklch(0.14,0.00,286)", "rgbChannel": "9 9 11", "hslChannel": "240 10% 3.9%" } ], "neutral": [ { "scale": 50, "hex": "#fafafa", "rgb": "rgb(250,250,250)", "hsl": "hsl(0,0%,98%)", "oklch": "oklch(0.99,0.00,0)", "rgbChannel": "250 250 250", "hslChannel": "0 0% 98%" }, { "scale": 100, "hex": "#f5f5f5", "rgb": "rgb(245,245,245)", "hsl": "hsl(0,0%,96.1%)", "oklch": "oklch(0.97,0.00,0)", "rgbChannel": "245 245 245", "hslChannel": "0 0% 96.1%" }, { "scale": 200, "hex": "#e5e5e5", "rgb": "rgb(229,229,229)", "hsl": "hsl(0,0%,89.8%)", "oklch": "oklch(0.92,0.00,0)", "rgbChannel": "229 229 229", "hslChannel": "0 0% 89.8%" }, { "scale": 300, "hex": "#d4d4d4", "rgb": "rgb(212,212,212)", "hsl": "hsl(0,0%,83.1%)", "oklch": "oklch(0.87,0.00,0)", "rgbChannel": "212 212 212", "hslChannel": "0 0% 83.1%" }, { "scale": 400, "hex": "#a3a3a3", "rgb": "rgb(163,163,163)", "hsl": "hsl(0,0%,63.9%)", "oklch": "oklch(0.72,0.00,0)", "rgbChannel": "163 163 163", "hslChannel": "0 0% 63.9%" }, { "scale": 500, "hex": "#737373", "rgb": "rgb(115,115,115)", "hsl": "hsl(0,0%,45.1%)", "oklch": "oklch(0.56,0.00,0)", "rgbChannel": "115 115 115", "hslChannel": "0 0% 45.1%" }, { "scale": 600, "hex": "#525252", "rgb": "rgb(82,82,82)", "hsl": "hsl(0,0%,32.2%)", "oklch": "oklch(0.44,0.00,0)", "rgbChannel": "82 82 82", "hslChannel": "0 0% 32.2%" }, { "scale": 700, "hex": "#404040", "rgb": "rgb(64,64,64)", "hsl": "hsl(0,0%,25.1%)", "oklch": "oklch(0.37,0.00,0)", "rgbChannel": "64 64 64", "hslChannel": "0 0% 25.1%" }, { "scale": 800, "hex": "#262626", "rgb": "rgb(38,38,38)", "hsl": "hsl(0,0%,14.9%)", "oklch": "oklch(0.27,0.00,0)", "rgbChannel": "38 38 38", "hslChannel": "0 0% 14.9%" }, { "scale": 900, "hex": "#171717", "rgb": "rgb(23,23,23)", "hsl": "hsl(0,0%,9%)", "oklch": "oklch(0.20,0.00,0)", "rgbChannel": "23 23 23", "hslChannel": "0 0% 9%" }, { "scale": 950, "hex": "#0a0a0a", "rgb": "rgb(10,10,10)", "hsl": "hsl(0,0%,3.9%)", "oklch": "oklch(0.14,0.00,0)", "rgbChannel": "10 10 10", "hslChannel": "0 0% 3.9%" } ], "stone": [ { "scale": 50, "hex": "#fafaf9", "rgb": "rgb(250,250,249)", "hsl": "hsl(60,9.1%,97.8%)", "oklch": "oklch(0.98,0.00,106)", "rgbChannel": "250 250 249", "hslChannel": "60 9.1% 97.8%" }, { "scale": 100, "hex": "#f5f5f4", "rgb": "rgb(245,245,244)", "hsl": "hsl(60,4.8%,95.9%)", "oklch": "oklch(0.97,0.00,106)", "rgbChannel": "245 245 244", "hslChannel": "60 4.8% 95.9%" }, { "scale": 200, "hex": "#e7e5e4", "rgb": "rgb(231,229,228)", "hsl": "hsl(20,5.9%,90%)", "oklch": "oklch(0.92,0.00,49)", "rgbChannel": "231 229 228", "hslChannel": "20 5.9% 90%" }, { "scale": 300, "hex": "#d6d3d1", "rgb": "rgb(214,211,209)", "hsl": "hsl(24,5.7%,82.9%)", "oklch": "oklch(0.87,0.00,56)", "rgbChannel": "214 211 209", "hslChannel": "24 5.7% 82.9%" }, { "scale": 400, "hex": "#a8a29e", "rgb": "rgb(168,162,158)", "hsl": "hsl(24,5.4%,63.9%)", "oklch": "oklch(0.72,0.01,56)", "rgbChannel": "168 162 158", "hslChannel": "24 5.4% 63.9%" }, { "scale": 500, "hex": "#78716c", "rgb": "rgb(120,113,108)", "hsl": "hsl(25,5.3%,44.7%)", "oklch": "oklch(0.55,0.01,58)", "rgbChannel": "120 113 108", "hslChannel": "25 5.3% 44.7%" }, { "scale": 600, "hex": "#57534e", "rgb": "rgb(87,83,78)", "hsl": "hsl(33.3,5.5%,32.4%)", "oklch": "oklch(0.44,0.01,74)", "rgbChannel": "87 83 78", "hslChannel": "33.3 5.5% 32.4%" }, { "scale": 700, "hex": "#44403c", "rgb": "rgb(68,64,60)", "hsl": "hsl(30,6.3%,25.1%)", "oklch": "oklch(0.37,0.01,68)", "rgbChannel": "68 64 60", "hslChannel": "30 6.3% 25.1%" }, { "scale": 800, "hex": "#292524", "rgb": "rgb(41,37,36)", "hsl": "hsl(12,6.5%,15.1%)", "oklch": "oklch(0.27,0.01,34)", "rgbChannel": "41 37 36", "hslChannel": "12 6.5% 15.1%" }, { "scale": 900, "hex": "#1c1917", "rgb": "rgb(28,25,23)", "hsl": "hsl(24,9.8%,10%)", "oklch": "oklch(0.22,0.01,56)", "rgbChannel": "28 25 23", "hslChannel": "24 9.8% 10%" }, { "scale": 950, "hex": "#0c0a09", "rgb": "rgb(12,10,9)", "hsl": "hsl(20,14.3%,4.1%)", "oklch": "oklch(0.15,0.00,49)", "rgbChannel": "12 10 9", "hslChannel": "20 14.3% 4.1%" } ], "red": [ { "scale": 50, "hex": "#fef2f2", "rgb": "rgb(254,242,242)", "hsl": "hsl(0,85.7%,97.3%)", "oklch": "oklch(0.97,0.01,17)", "rgbChannel": "254 242 242", "hslChannel": "0 85.7% 97.3%" }, { "scale": 100, "hex": "#fee2e2", "rgb": "rgb(254,226,226)", "hsl": "hsl(0,93.3%,94.1%)", "oklch": "oklch(0.94,0.03,18)", "rgbChannel": "254 226 226", "hslChannel": "0 93.3% 94.1%" }, { "scale": 200, "hex": "#fecaca", "rgb": "rgb(254,202,202)", "hsl": "hsl(0,96.3%,89.4%)", "oklch": "oklch(0.88,0.06,18)", "rgbChannel": "254 202 202", "hslChannel": "0 96.3% 89.4%" }, { "scale": 300, "hex": "#fca5a5", "rgb": "rgb(252,165,165)", "hsl": "hsl(0,93.5%,81.8%)", "oklch": "oklch(0.81,0.10,20)", "rgbChannel": "252 165 165", "hslChannel": "0 93.5% 81.8%" }, { "scale": 400, "hex": "#f87171", "rgb": "rgb(248,113,113)", "hsl": "hsl(0,90.6%,70.8%)", "oklch": "oklch(0.71,0.17,22)", "rgbChannel": "248 113 113", "hslChannel": "0 90.6% 70.8%" }, { "scale": 500, "hex": "#ef4444", "rgb": "rgb(239,68,68)", "hsl": "hsl(0,84.2%,60.2%)", "oklch": "oklch(0.64,0.21,25)", "rgbChannel": "239 68 68", "hslChannel": "0 84.2% 60.2%" }, { "scale": 600, "hex": "#dc2626", "rgb": "rgb(220,38,38)", "hsl": "hsl(0,72.2%,50.6%)", "oklch": "oklch(0.58,0.22,27)", "rgbChannel": "220 38 38", "hslChannel": "0 72.2% 50.6%" }, { "scale": 700, "hex": "#b91c1c", "rgb": "rgb(185,28,28)", "hsl": "hsl(0,73.7%,41.8%)", "oklch": "oklch(0.51,0.19,28)", "rgbChannel": "185 28 28", "hslChannel": "0 73.7% 41.8%" }, { "scale": 800, "hex": "#991b1b", "rgb": "rgb(153,27,27)", "hsl": "hsl(0,70%,35.3%)", "oklch": "oklch(0.44,0.16,27)", "rgbChannel": "153 27 27", "hslChannel": "0 70% 35.3%" }, { "scale": 900, "hex": "#7f1d1d", "rgb": "rgb(127,29,29)", "hsl": "hsl(0,62.8%,30.6%)", "oklch": "oklch(0.40,0.13,26)", "rgbChannel": "127 29 29", "hslChannel": "0 62.8% 30.6%" }, { "scale": 950, "hex": "#450a0a", "rgb": "rgb(69,10,10)", "hsl": "hsl(0,74.7%,15.5%)", "oklch": "oklch(0.26,0.09,26)", "rgbChannel": "69 10 10", "hslChannel": "0 74.7% 15.5%" } ], "orange": [ { "scale": 50, "hex": "#fff7ed", "rgb": "rgb(255,247,237)", "hsl": "hsl(33.3,100%,96.5%)", "oklch": "oklch(0.98,0.02,74)", "rgbChannel": "255 247 237", "hslChannel": "33.3 100% 96.5%" }, { "scale": 100, "hex": "#ffedd5", "rgb": "rgb(255,237,213)", "hsl": "hsl(34.3,100%,91.8%)", "oklch": "oklch(0.95,0.04,75)", "rgbChannel": "255 237 213", "hslChannel": "34.3 100% 91.8%" }, { "scale": 200, "hex": "#fed7aa", "rgb": "rgb(254,215,170)", "hsl": "hsl(32.1,97.7%,83.1%)", "oklch": "oklch(0.90,0.07,71)", "rgbChannel": "254 215 170", "hslChannel": "32.1 97.7% 83.1%" }, { "scale": 300, "hex": "#fdba74", "rgb": "rgb(253,186,116)", "hsl": "hsl(30.7,97.2%,72.4%)", "oklch": "oklch(0.84,0.12,66)", "rgbChannel": "253 186 116", "hslChannel": "30.7 97.2% 72.4%" }, { "scale": 400, "hex": "#fb923c", "rgb": "rgb(251,146,60)", "hsl": "hsl(27,96%,61%)", "oklch": "oklch(0.76,0.16,56)", "rgbChannel": "251 146 60", "hslChannel": "27 96% 61%" }, { "scale": 500, "hex": "#f97316", "rgb": "rgb(249,115,22)", "hsl": "hsl(24.6,95%,53.1%)", "oklch": "oklch(0.70,0.19,48)", "rgbChannel": "249 115 22", "hslChannel": "24.6 95% 53.1%" }, { "scale": 600, "hex": "#ea580c", "rgb": "rgb(234,88,12)", "hsl": "hsl(20.5,90.2%,48.2%)", "oklch": "oklch(0.65,0.19,41)", "rgbChannel": "234 88 12", "hslChannel": "20.5 90.2% 48.2%" }, { "scale": 700, "hex": "#c2410c", "rgb": "rgb(194,65,12)", "hsl": "hsl(17.5,88.3%,40.4%)", "oklch": "oklch(0.55,0.17,38)", "rgbChannel": "194 65 12", "hslChannel": "17.5 88.3% 40.4%" }, { "scale": 800, "hex": "#9a3412", "rgb": "rgb(154,52,18)", "hsl": "hsl(15,79.1%,33.7%)", "oklch": "oklch(0.47,0.14,37)", "rgbChannel": "154 52 18", "hslChannel": "15 79.1% 33.7%" }, { "scale": 900, "hex": "#7c2d12", "rgb": "rgb(124,45,18)", "hsl": "hsl(15.3,74.6%,27.8%)", "oklch": "oklch(0.41,0.12,38)", "rgbChannel": "124 45 18", "hslChannel": "15.3 74.6% 27.8%" }, { "scale": 950, "hex": "#431407", "rgb": "rgb(67,20,7)", "hsl": "hsl(13,81.1%,14.5%)", "oklch": "oklch(0.27,0.08,36)", "rgbChannel": "67 20 7", "hslChannel": "13 81.1% 14.5%" } ], "amber": [ { "scale": 50, "hex": "#fffbeb", "rgb": "rgb(255,251,235)", "hsl": "hsl(48,100%,96.1%)", "oklch": "oklch(0.99,0.02,95)", "rgbChannel": "255 251 235", "hslChannel": "48 100% 96.1%" }, { "scale": 100, "hex": "#fef3c7", "rgb": "rgb(254,243,199)", "hsl": "hsl(48,96.5%,88.8%)", "oklch": "oklch(0.96,0.06,96)", "rgbChannel": "254 243 199", "hslChannel": "48 96.5% 88.8%" }, { "scale": 200, "hex": "#fde68a", "rgb": "rgb(253,230,138)", "hsl": "hsl(48,96.6%,76.7%)", "oklch": "oklch(0.92,0.12,96)", "rgbChannel": "253 230 138", "hslChannel": "48 96.6% 76.7%" }, { "scale": 300, "hex": "#fcd34d", "rgb": "rgb(252,211,77)", "hsl": "hsl(45.9,96.7%,64.5%)", "oklch": "oklch(0.88,0.15,92)", "rgbChannel": "252 211 77", "hslChannel": "45.9 96.7% 64.5%" }, { "scale": 400, "hex": "#fbbf24", "rgb": "rgb(251,191,36)", "hsl": "hsl(43.3,96.4%,56.3%)", "oklch": "oklch(0.84,0.16,84)", "rgbChannel": "251 191 36", "hslChannel": "43.3 96.4% 56.3%" }, { "scale": 500, "hex": "#f59e0b", "rgb": "rgb(245,158,11)", "hsl": "hsl(37.7,92.1%,50.2%)", "oklch": "oklch(0.77,0.16,70)", "rgbChannel": "245 158 11", "hslChannel": "37.7 92.1% 50.2%" }, { "scale": 600, "hex": "#d97706", "rgb": "rgb(217,119,6)", "hsl": "hsl(32.1,94.6%,43.7%)", "oklch": "oklch(0.67,0.16,58)", "rgbChannel": "217 119 6", "hslChannel": "32.1 94.6% 43.7%" }, { "scale": 700, "hex": "#b45309", "rgb": "rgb(180,83,9)", "hsl": "hsl(26,90.5%,37.1%)", "oklch": "oklch(0.56,0.15,49)", "rgbChannel": "180 83 9", "hslChannel": "26 90.5% 37.1%" }, { "scale": 800, "hex": "#92400e", "rgb": "rgb(146,64,14)", "hsl": "hsl(22.7,82.5%,31.4%)", "oklch": "oklch(0.47,0.12,46)", "rgbChannel": "146 64 14", "hslChannel": "22.7 82.5% 31.4%" }, { "scale": 900, "hex": "#78350f", "rgb": "rgb(120,53,15)", "hsl": "hsl(21.7,77.8%,26.5%)", "oklch": "oklch(0.41,0.11,46)", "rgbChannel": "120 53 15", "hslChannel": "21.7 77.8% 26.5%" }, { "scale": 950, "hex": "#451a03", "rgb": "rgb(69,26,3)", "hsl": "hsl(20.9,91.7%,14.1%)", "oklch": "oklch(0.28,0.07,46)", "rgbChannel": "69 26 3", "hslChannel": "20.9 91.7% 14.1%" } ], "yellow": [ { "scale": 50, "hex": "#fefce8", "rgb": "rgb(254,252,232)", "hsl": "hsl(54.5,91.7%,95.3%)", "oklch": "oklch(0.99,0.03,102)", "rgbChannel": "254 252 232", "hslChannel": "54.5 91.7% 95.3%" }, { "scale": 100, "hex": "#fef9c3", "rgb": "rgb(254,249,195)", "hsl": "hsl(54.9,96.7%,88%)", "oklch": "oklch(0.97,0.07,103)", "rgbChannel": "254 249 195", "hslChannel": "54.9 96.7% 88%" }, { "scale": 200, "hex": "#fef08a", "rgb": "rgb(254,240,138)", "hsl": "hsl(52.8,98.3%,76.9%)", "oklch": "oklch(0.95,0.12,102)", "rgbChannel": "254 240 138", "hslChannel": "52.8 98.3% 76.9%" }, { "scale": 300, "hex": "#fde047", "rgb": "rgb(253,224,71)", "hsl": "hsl(50.4,97.8%,63.5%)", "oklch": "oklch(0.91,0.17,98)", "rgbChannel": "253 224 71", "hslChannel": "50.4 97.8% 63.5%" }, { "scale": 400, "hex": "#facc15", "rgb": "rgb(250,204,21)", "hsl": "hsl(47.9,95.8%,53.1%)", "oklch": "oklch(0.86,0.17,92)", "rgbChannel": "250 204 21", "hslChannel": "47.9 95.8% 53.1%" }, { "scale": 500, "hex": "#eab308", "rgb": "rgb(234,179,8)", "hsl": "hsl(45.4,93.4%,47.5%)", "oklch": "oklch(0.80,0.16,86)", "rgbChannel": "234 179 8", "hslChannel": "45.4 93.4% 47.5%" }, { "scale": 600, "hex": "#ca8a04", "rgb": "rgb(202,138,4)", "hsl": "hsl(40.6,96.1%,40.4%)", "oklch": "oklch(0.68,0.14,76)", "rgbChannel": "202 138 4", "hslChannel": "40.6 96.1% 40.4%" }, { "scale": 700, "hex": "#a16207", "rgb": "rgb(161,98,7)", "hsl": "hsl(35.5,91.7%,32.9%)", "oklch": "oklch(0.55,0.12,66)", "rgbChannel": "161 98 7", "hslChannel": "35.5 91.7% 32.9%" }, { "scale": 800, "hex": "#854d0e", "rgb": "rgb(133,77,14)", "hsl": "hsl(31.8,81%,28.8%)", "oklch": "oklch(0.48,0.10,62)", "rgbChannel": "133 77 14", "hslChannel": "31.8 81% 28.8%" }, { "scale": 900, "hex": "#713f12", "rgb": "rgb(113,63,18)", "hsl": "hsl(28.4,72.5%,25.7%)", "oklch": "oklch(0.42,0.09,58)", "rgbChannel": "113 63 18", "hslChannel": "28.4 72.5% 25.7%" }, { "scale": 950, "hex": "#422006", "rgb": "rgb(66,32,6)", "hsl": "hsl(26,83.3%,14.1%)", "oklch": "oklch(0.29,0.06,54)", "rgbChannel": "66 32 6", "hslChannel": "26 83.3% 14.1%" } ], "lime": [ { "scale": 50, "hex": "#f7fee7", "rgb": "rgb(247,254,231)", "hsl": "hsl(78.3,92%,95.1%)", "oklch": "oklch(0.99,0.03,121)", "rgbChannel": "247 254 231", "hslChannel": "78.3 92% 95.1%" }, { "scale": 100, "hex": "#ecfccb", "rgb": "rgb(236,252,203)", "hsl": "hsl(79.6,89.1%,89.2%)", "oklch": "oklch(0.97,0.07,122)", "rgbChannel": "236 252 203", "hslChannel": "79.6 89.1% 89.2%" }, { "scale": 200, "hex": "#d9f99d", "rgb": "rgb(217,249,157)", "hsl": "hsl(80.9,88.5%,79.6%)", "oklch": "oklch(0.94,0.12,124)", "rgbChannel": "217 249 157", "hslChannel": "80.9 88.5% 79.6%" }, { "scale": 300, "hex": "#bef264", "rgb": "rgb(190,242,100)", "hsl": "hsl(82,84.5%,67.1%)", "oklch": "oklch(0.90,0.18,127)", "rgbChannel": "190 242 100", "hslChannel": "82 84.5% 67.1%" }, { "scale": 400, "hex": "#a3e635", "rgb": "rgb(163,230,53)", "hsl": "hsl(82.7,78%,55.5%)", "oklch": "oklch(0.85,0.21,129)", "rgbChannel": "163 230 53", "hslChannel": "82.7 78% 55.5%" }, { "scale": 500, "hex": "#84cc16", "rgb": "rgb(132,204,22)", "hsl": "hsl(83.7,80.5%,44.3%)", "oklch": "oklch(0.77,0.20,131)", "rgbChannel": "132 204 22", "hslChannel": "83.7 80.5% 44.3%" }, { "scale": 600, "hex": "#65a30d", "rgb": "rgb(101,163,13)", "hsl": "hsl(84.8,85.2%,34.5%)", "oklch": "oklch(0.65,0.18,132)", "rgbChannel": "101 163 13", "hslChannel": "84.8 85.2% 34.5%" }, { "scale": 700, "hex": "#4d7c0f", "rgb": "rgb(77,124,15)", "hsl": "hsl(85.9,78.4%,27.3%)", "oklch": "oklch(0.53,0.14,132)", "rgbChannel": "77 124 15", "hslChannel": "85.9 78.4% 27.3%" }, { "scale": 800, "hex": "#3f6212", "rgb": "rgb(63,98,18)", "hsl": "hsl(86.3,69%,22.7%)", "oklch": "oklch(0.45,0.11,131)", "rgbChannel": "63 98 18", "hslChannel": "86.3 69% 22.7%" }, { "scale": 900, "hex": "#365314", "rgb": "rgb(54,83,20)", "hsl": "hsl(87.6,61.2%,20.2%)", "oklch": "oklch(0.41,0.10,131)", "rgbChannel": "54 83 20", "hslChannel": "87.6 61.2% 20.2%" }, { "scale": 950, "hex": "#1a2e05", "rgb": "rgb(26,46,5)", "hsl": "hsl(89.3,80.4%,10%)", "oklch": "oklch(0.27,0.07,132)", "rgbChannel": "26 46 5", "hslChannel": "89.3 80.4% 10%" } ], "green": [ { "scale": 50, "hex": "#f0fdf4", "rgb": "rgb(240,253,244)", "hsl": "hsl(138.5,76.5%,96.7%)", "oklch": "oklch(0.98,0.02,156)", "rgbChannel": "240 253 244", "hslChannel": "138.5 76.5% 96.7%" }, { "scale": 100, "hex": "#dcfce7", "rgb": "rgb(220,252,231)", "hsl": "hsl(140.6,84.2%,92.5%)", "oklch": "oklch(0.96,0.04,157)", "rgbChannel": "220 252 231", "hslChannel": "140.6 84.2% 92.5%" }, { "scale": 200, "hex": "#bbf7d0", "rgb": "rgb(187,247,208)", "hsl": "hsl(141,78.9%,85.1%)", "oklch": "oklch(0.93,0.08,156)", "rgbChannel": "187 247 208", "hslChannel": "141 78.9% 85.1%" }, { "scale": 300, "hex": "#86efac", "rgb": "rgb(134,239,172)", "hsl": "hsl(141.7,76.6%,73.1%)", "oklch": "oklch(0.87,0.14,154)", "rgbChannel": "134 239 172", "hslChannel": "141.7 76.6% 73.1%" }, { "scale": 400, "hex": "#4ade80", "rgb": "rgb(74,222,128)", "hsl": "hsl(141.9,69.2%,58%)", "oklch": "oklch(0.80,0.18,152)", "rgbChannel": "74 222 128", "hslChannel": "141.9 69.2% 58%" }, { "scale": 500, "hex": "#22c55e", "rgb": "rgb(34,197,94)", "hsl": "hsl(142.1,70.6%,45.3%)", "oklch": "oklch(0.72,0.19,150)", "rgbChannel": "34 197 94", "hslChannel": "142.1 70.6% 45.3%" }, { "scale": 600, "hex": "#16a34a", "rgb": "rgb(22,163,74)", "hsl": "hsl(142.1,76.2%,36.3%)", "oklch": "oklch(0.63,0.17,149)", "rgbChannel": "22 163 74", "hslChannel": "142.1 76.2% 36.3%" }, { "scale": 700, "hex": "#15803d", "rgb": "rgb(21,128,61)", "hsl": "hsl(142.4,71.8%,29.2%)", "oklch": "oklch(0.53,0.14,150)", "rgbChannel": "21 128 61", "hslChannel": "142.4 71.8% 29.2%" }, { "scale": 800, "hex": "#166534", "rgb": "rgb(22,101,52)", "hsl": "hsl(142.8,64.2%,24.1%)", "oklch": "oklch(0.45,0.11,151)", "rgbChannel": "22 101 52", "hslChannel": "142.8 64.2% 24.1%" }, { "scale": 900, "hex": "#14532d", "rgb": "rgb(20,83,45)", "hsl": "hsl(143.8,61.2%,20.2%)", "oklch": "oklch(0.39,0.09,153)", "rgbChannel": "20 83 45", "hslChannel": "143.8 61.2% 20.2%" }, { "scale": 950, "hex": "#052e16", "rgb": "rgb(5,46,22)", "hsl": "hsl(144.9,80.4%,10%)", "oklch": "oklch(0.27,0.06,153)", "rgbChannel": "5 46 22", "hslChannel": "144.9 80.4% 10%" } ], "emerald": [ { "scale": 50, "hex": "#ecfdf5", "rgb": "rgb(236,253,245)", "hsl": "hsl(151.8,81%,95.9%)", "oklch": "oklch(0.98,0.02,166)", "rgbChannel": "236 253 245", "hslChannel": "151.8 81% 95.9%" }, { "scale": 100, "hex": "#d1fae5", "rgb": "rgb(209,250,229)", "hsl": "hsl(149.3,80.4%,90%)", "oklch": "oklch(0.95,0.05,163)", "rgbChannel": "209 250 229", "hslChannel": "149.3 80.4% 90%" }, { "scale": 200, "hex": "#a7f3d0", "rgb": "rgb(167,243,208)", "hsl": "hsl(152.4,76%,80.4%)", "oklch": "oklch(0.90,0.09,164)", "rgbChannel": "167 243 208", "hslChannel": "152.4 76% 80.4%" }, { "scale": 300, "hex": "#6ee7b7", "rgb": "rgb(110,231,183)", "hsl": "hsl(156.2,71.6%,66.9%)", "oklch": "oklch(0.85,0.13,165)", "rgbChannel": "110 231 183", "hslChannel": "156.2 71.6% 66.9%" }, { "scale": 400, "hex": "#34d399", "rgb": "rgb(52,211,153)", "hsl": "hsl(158.1,64.4%,51.6%)", "oklch": "oklch(0.77,0.15,163)", "rgbChannel": "52 211 153", "hslChannel": "158.1 64.4% 51.6%" }, { "scale": 500, "hex": "#10b981", "rgb": "rgb(16,185,129)", "hsl": "hsl(160.1,84.1%,39.4%)", "oklch": "oklch(0.70,0.15,162)", "rgbChannel": "16 185 129", "hslChannel": "160.1 84.1% 39.4%" }, { "scale": 600, "hex": "#059669", "rgb": "rgb(5,150,105)", "hsl": "hsl(161.4,93.5%,30.4%)", "oklch": "oklch(0.60,0.13,163)", "rgbChannel": "5 150 105", "hslChannel": "161.4 93.5% 30.4%" }, { "scale": 700, "hex": "#047857", "rgb": "rgb(4,120,87)", "hsl": "hsl(162.9,93.5%,24.3%)", "oklch": "oklch(0.51,0.10,166)", "rgbChannel": "4 120 87", "hslChannel": "162.9 93.5% 24.3%" }, { "scale": 800, "hex": "#065f46", "rgb": "rgb(6,95,70)", "hsl": "hsl(163.1,88.1%,19.8%)", "oklch": "oklch(0.43,0.09,167)", "rgbChannel": "6 95 70", "hslChannel": "163.1 88.1% 19.8%" }, { "scale": 900, "hex": "#064e3b", "rgb": "rgb(6,78,59)", "hsl": "hsl(164.2,85.7%,16.5%)", "oklch": "oklch(0.38,0.07,169)", "rgbChannel": "6 78 59", "hslChannel": "164.2 85.7% 16.5%" }, { "scale": 950, "hex": "#022c22", "rgb": "rgb(2,44,34)", "hsl": "hsl(165.7,91.3%,9%)", "oklch": "oklch(0.26,0.05,173)", "rgbChannel": "2 44 34", "hslChannel": "165.7 91.3% 9%" } ], "teal": [ { "scale": 50, "hex": "#f0fdfa", "rgb": "rgb(240,253,250)", "hsl": "hsl(166.2,76.5%,96.7%)", "oklch": "oklch(0.98,0.01,181)", "rgbChannel": "240 253 250", "hslChannel": "166.2 76.5% 96.7%" }, { "scale": 100, "hex": "#ccfbf1", "rgb": "rgb(204,251,241)", "hsl": "hsl(167.2,85.5%,89.2%)", "oklch": "oklch(0.95,0.05,181)", "rgbChannel": "204 251 241", "hslChannel": "167.2 85.5% 89.2%" }, { "scale": 200, "hex": "#99f6e4", "rgb": "rgb(153,246,228)", "hsl": "hsl(168.4,83.8%,78.2%)", "oklch": "oklch(0.91,0.09,180)", "rgbChannel": "153 246 228", "hslChannel": "168.4 83.8% 78.2%" }, { "scale": 300, "hex": "#5eead4", "rgb": "rgb(94,234,212)", "hsl": "hsl(170.6,76.9%,64.3%)", "oklch": "oklch(0.85,0.13,181)", "rgbChannel": "94 234 212", "hslChannel": "170.6 76.9% 64.3%" }, { "scale": 400, "hex": "#2dd4bf", "rgb": "rgb(45,212,191)", "hsl": "hsl(172.5,66%,50.4%)", "oklch": "oklch(0.78,0.13,182)", "rgbChannel": "45 212 191", "hslChannel": "172.5 66% 50.4%" }, { "scale": 500, "hex": "#14b8a6", "rgb": "rgb(20,184,166)", "hsl": "hsl(173.4,80.4%,40%)", "oklch": "oklch(0.70,0.12,183)", "rgbChannel": "20 184 166", "hslChannel": "173.4 80.4% 40%" }, { "scale": 600, "hex": "#0d9488", "rgb": "rgb(13,148,136)", "hsl": "hsl(174.7,83.9%,31.6%)", "oklch": "oklch(0.60,0.10,185)", "rgbChannel": "13 148 136", "hslChannel": "174.7 83.9% 31.6%" }, { "scale": 700, "hex": "#0f766e", "rgb": "rgb(15,118,110)", "hsl": "hsl(175.3,77.4%,26.1%)", "oklch": "oklch(0.51,0.09,186)", "rgbChannel": "15 118 110", "hslChannel": "175.3 77.4% 26.1%" }, { "scale": 800, "hex": "#115e59", "rgb": "rgb(17,94,89)", "hsl": "hsl(176.1,69.4%,21.8%)", "oklch": "oklch(0.44,0.07,188)", "rgbChannel": "17 94 89", "hslChannel": "176.1 69.4% 21.8%" }, { "scale": 900, "hex": "#134e4a", "rgb": "rgb(19,78,74)", "hsl": "hsl(175.9,60.8%,19%)", "oklch": "oklch(0.39,0.06,188)", "rgbChannel": "19 78 74", "hslChannel": "175.9 60.8% 19%" }, { "scale": 950, "hex": "#042f2e", "rgb": "rgb(4,47,46)", "hsl": "hsl(178.6,84.3%,10%)", "oklch": "oklch(0.28,0.04,193)", "rgbChannel": "4 47 46", "hslChannel": "178.6 84.3% 10%" } ], "cyan": [ { "scale": 50, "hex": "#ecfeff", "rgb": "rgb(236,254,255)", "hsl": "hsl(183.2,100%,96.3%)", "oklch": "oklch(0.98,0.02,201)", "rgbChannel": "236 254 255", "hslChannel": "183.2 100% 96.3%" }, { "scale": 100, "hex": "#cffafe", "rgb": "rgb(207,250,254)", "hsl": "hsl(185.1,95.9%,90.4%)", "oklch": "oklch(0.96,0.04,203)", "rgbChannel": "207 250 254", "hslChannel": "185.1 95.9% 90.4%" }, { "scale": 200, "hex": "#a5f3fc", "rgb": "rgb(165,243,252)", "hsl": "hsl(186.2,93.5%,81.8%)", "oklch": "oklch(0.92,0.08,205)", "rgbChannel": "165 243 252", "hslChannel": "186.2 93.5% 81.8%" }, { "scale": 300, "hex": "#67e8f9", "rgb": "rgb(103,232,249)", "hsl": "hsl(187,92.4%,69%)", "oklch": "oklch(0.87,0.12,207)", "rgbChannel": "103 232 249", "hslChannel": "187 92.4% 69%" }, { "scale": 400, "hex": "#22d3ee", "rgb": "rgb(34,211,238)", "hsl": "hsl(187.9,85.7%,53.3%)", "oklch": "oklch(0.80,0.13,212)", "rgbChannel": "34 211 238", "hslChannel": "187.9 85.7% 53.3%" }, { "scale": 500, "hex": "#06b6d4", "rgb": "rgb(6,182,212)", "hsl": "hsl(188.7,94.5%,42.7%)", "oklch": "oklch(0.71,0.13,215)", "rgbChannel": "6 182 212", "hslChannel": "188.7 94.5% 42.7%" }, { "scale": 600, "hex": "#0891b2", "rgb": "rgb(8,145,178)", "hsl": "hsl(191.6,91.4%,36.5%)", "oklch": "oklch(0.61,0.11,222)", "rgbChannel": "8 145 178", "hslChannel": "191.6 91.4% 36.5%" }, { "scale": 700, "hex": "#0e7490", "rgb": "rgb(14,116,144)", "hsl": "hsl(192.9,82.3%,31%)", "oklch": "oklch(0.52,0.09,223)", "rgbChannel": "14 116 144", "hslChannel": "192.9 82.3% 31%" }, { "scale": 800, "hex": "#155e75", "rgb": "rgb(21,94,117)", "hsl": "hsl(194.4,69.6%,27.1%)", "oklch": "oklch(0.45,0.08,224)", "rgbChannel": "21 94 117", "hslChannel": "194.4 69.6% 27.1%" }, { "scale": 900, "hex": "#164e63", "rgb": "rgb(22,78,99)", "hsl": "hsl(196.4,63.6%,23.7%)", "oklch": "oklch(0.40,0.07,227)", "rgbChannel": "22 78 99", "hslChannel": "196.4 63.6% 23.7%" }, { "scale": 950, "hex": "#083344", "rgb": "rgb(8,51,68)", "hsl": "hsl(197,78.9%,14.9%)", "oklch": "oklch(0.30,0.05,230)", "rgbChannel": "8 51 68", "hslChannel": "197 78.9% 14.9%" } ], "sky": [ { "scale": 50, "hex": "#f0f9ff", "rgb": "rgb(240,249,255)", "hsl": "hsl(204,100%,97.1%)", "oklch": "oklch(0.98,0.01,237)", "rgbChannel": "240 249 255", "hslChannel": "204 100% 97.1%" }, { "scale": 100, "hex": "#e0f2fe", "rgb": "rgb(224,242,254)", "hsl": "hsl(204,93.8%,93.7%)", "oklch": "oklch(0.95,0.03,237)", "rgbChannel": "224 242 254", "hslChannel": "204 93.8% 93.7%" }, { "scale": 200, "hex": "#bae6fd", "rgb": "rgb(186,230,253)", "hsl": "hsl(200.6,94.4%,86.1%)", "oklch": "oklch(0.90,0.06,231)", "rgbChannel": "186 230 253", "hslChannel": "200.6 94.4% 86.1%" }, { "scale": 300, "hex": "#7dd3fc", "rgb": "rgb(125,211,252)", "hsl": "hsl(199.4,95.5%,73.9%)", "oklch": "oklch(0.83,0.10,230)", "rgbChannel": "125 211 252", "hslChannel": "199.4 95.5% 73.9%" }, { "scale": 400, "hex": "#38bdf8", "rgb": "rgb(56,189,248)", "hsl": "hsl(198.4,93.2%,59.6%)", "oklch": "oklch(0.75,0.14,233)", "rgbChannel": "56 189 248", "hslChannel": "198.4 93.2% 59.6%" }, { "scale": 500, "hex": "#0ea5e9", "rgb": "rgb(14,165,233)", "hsl": "hsl(198.6,88.7%,48.4%)", "oklch": "oklch(0.68,0.15,237)", "rgbChannel": "14 165 233", "hslChannel": "198.6 88.7% 48.4%" }, { "scale": 600, "hex": "#0284c7", "rgb": "rgb(2,132,199)", "hsl": "hsl(200.4,98%,39.4%)", "oklch": "oklch(0.59,0.14,242)", "rgbChannel": "2 132 199", "hslChannel": "200.4 98% 39.4%" }, { "scale": 700, "hex": "#0369a1", "rgb": "rgb(3,105,161)", "hsl": "hsl(201.3,96.3%,32.2%)", "oklch": "oklch(0.50,0.12,243)", "rgbChannel": "3 105 161", "hslChannel": "201.3 96.3% 32.2%" }, { "scale": 800, "hex": "#075985", "rgb": "rgb(7,89,133)", "hsl": "hsl(201,90%,27.5%)", "oklch": "oklch(0.44,0.10,241)", "rgbChannel": "7 89 133", "hslChannel": "201 90% 27.5%" }, { "scale": 900, "hex": "#0c4a6e", "rgb": "rgb(12,74,110)", "hsl": "hsl(202,80.3%,23.9%)", "oklch": "oklch(0.39,0.08,241)", "rgbChannel": "12 74 110", "hslChannel": "202 80.3% 23.9%" }, { "scale": 950, "hex": "#082f49", "rgb": "rgb(8,47,73)", "hsl": "hsl(204,80.2%,15.9%)", "oklch": "oklch(0.29,0.06,243)", "rgbChannel": "8 47 73", "hslChannel": "204 80.2% 15.9%" } ], "blue": [ { "scale": 50, "hex": "#eff6ff", "rgb": "rgb(239,246,255)", "hsl": "hsl(213.8,100%,96.9%)", "oklch": "oklch(0.97,0.01,255)", "rgbChannel": "239 246 255", "hslChannel": "213.8 100% 96.9%" }, { "scale": 100, "hex": "#dbeafe", "rgb": "rgb(219,234,254)", "hsl": "hsl(214.3,94.6%,92.7%)", "oklch": "oklch(0.93,0.03,256)", "rgbChannel": "219 234 254", "hslChannel": "214.3 94.6% 92.7%" }, { "scale": 200, "hex": "#bfdbfe", "rgb": "rgb(191,219,254)", "hsl": "hsl(213.3,96.9%,87.3%)", "oklch": "oklch(0.88,0.06,254)", "rgbChannel": "191 219 254", "hslChannel": "213.3 96.9% 87.3%" }, { "scale": 300, "hex": "#93c5fd", "rgb": "rgb(147,197,253)", "hsl": "hsl(211.7,96.4%,78.4%)", "oklch": "oklch(0.81,0.10,252)", "rgbChannel": "147 197 253", "hslChannel": "211.7 96.4% 78.4%" }, { "scale": 400, "hex": "#60a5fa", "rgb": "rgb(96,165,250)", "hsl": "hsl(213.1,93.9%,67.8%)", "oklch": "oklch(0.71,0.14,255)", "rgbChannel": "96 165 250", "hslChannel": "213.1 93.9% 67.8%" }, { "scale": 500, "hex": "#3b82f6", "rgb": "rgb(59,130,246)", "hsl": "hsl(217.2,91.2%,59.8%)", "oklch": "oklch(0.62,0.19,260)", "rgbChannel": "59 130 246", "hslChannel": "217.2 91.2% 59.8%" }, { "scale": 600, "hex": "#2563eb", "rgb": "rgb(37,99,235)", "hsl": "hsl(221.2,83.2%,53.3%)", "oklch": "oklch(0.55,0.22,263)", "rgbChannel": "37 99 235", "hslChannel": "221.2 83.2% 53.3%" }, { "scale": 700, "hex": "#1d4ed8", "rgb": "rgb(29,78,216)", "hsl": "hsl(224.3,76.3%,48%)", "oklch": "oklch(0.49,0.22,264)", "rgbChannel": "29 78 216", "hslChannel": "224.3 76.3% 48%" }, { "scale": 800, "hex": "#1e40af", "rgb": "rgb(30,64,175)", "hsl": "hsl(225.9,70.7%,40.2%)", "oklch": "oklch(0.42,0.18,266)", "rgbChannel": "30 64 175", "hslChannel": "225.9 70.7% 40.2%" }, { "scale": 900, "hex": "#1e3a8a", "rgb": "rgb(30,58,138)", "hsl": "hsl(224.4,64.3%,32.9%)", "oklch": "oklch(0.38,0.14,266)", "rgbChannel": "30 58 138", "hslChannel": "224.4 64.3% 32.9%" }, { "scale": 950, "hex": "#172554", "rgb": "rgb(23,37,84)", "hsl": "hsl(226.2,57%,21%)", "oklch": "oklch(0.28,0.09,268)", "rgbChannel": "23 37 84", "hslChannel": "226.2 57% 21%" } ], "indigo": [ { "scale": 50, "hex": "#eef2ff", "rgb": "rgb(238,242,255)", "hsl": "hsl(225.9,100%,96.7%)", "oklch": "oklch(0.96,0.02,272)", "rgbChannel": "238 242 255", "hslChannel": "225.9 100% 96.7%" }, { "scale": 100, "hex": "#e0e7ff", "rgb": "rgb(224,231,255)", "hsl": "hsl(226.5,100%,93.9%)", "oklch": "oklch(0.93,0.03,273)", "rgbChannel": "224 231 255", "hslChannel": "226.5 100% 93.9%" }, { "scale": 200, "hex": "#c7d2fe", "rgb": "rgb(199,210,254)", "hsl": "hsl(228,96.5%,88.8%)", "oklch": "oklch(0.87,0.06,274)", "rgbChannel": "199 210 254", "hslChannel": "228 96.5% 88.8%" }, { "scale": 300, "hex": "#a5b4fc", "rgb": "rgb(165,180,252)", "hsl": "hsl(229.7,93.5%,81.8%)", "oklch": "oklch(0.79,0.10,275)", "rgbChannel": "165 180 252", "hslChannel": "229.7 93.5% 81.8%" }, { "scale": 400, "hex": "#818cf8", "rgb": "rgb(129,140,248)", "hsl": "hsl(234.5,89.5%,73.9%)", "oklch": "oklch(0.68,0.16,277)", "rgbChannel": "129 140 248", "hslChannel": "234.5 89.5% 73.9%" }, { "scale": 500, "hex": "#6366f1", "rgb": "rgb(99,102,241)", "hsl": "hsl(238.7,83.5%,66.7%)", "oklch": "oklch(0.59,0.20,277)", "rgbChannel": "99 102 241", "hslChannel": "238.7 83.5% 66.7%" }, { "scale": 600, "hex": "#4f46e5", "rgb": "rgb(79,70,229)", "hsl": "hsl(243.4,75.4%,58.6%)", "oklch": "oklch(0.51,0.23,277)", "rgbChannel": "79 70 229", "hslChannel": "243.4 75.4% 58.6%" }, { "scale": 700, "hex": "#4338ca", "rgb": "rgb(67,56,202)", "hsl": "hsl(244.5,57.9%,50.6%)", "oklch": "oklch(0.46,0.21,277)", "rgbChannel": "67 56 202", "hslChannel": "244.5 57.9% 50.6%" }, { "scale": 800, "hex": "#3730a3", "rgb": "rgb(55,48,163)", "hsl": "hsl(243.7,54.5%,41.4%)", "oklch": "oklch(0.40,0.18,277)", "rgbChannel": "55 48 163", "hslChannel": "243.7 54.5% 41.4%" }, { "scale": 900, "hex": "#312e81", "rgb": "rgb(49,46,129)", "hsl": "hsl(242.2,47.4%,34.3%)", "oklch": "oklch(0.36,0.14,279)", "rgbChannel": "49 46 129", "hslChannel": "242.2 47.4% 34.3%" }, { "scale": 950, "hex": "#1e1b4b", "rgb": "rgb(30,27,75)", "hsl": "hsl(243.8,47.1%,20%)", "oklch": "oklch(0.26,0.09,281)", "rgbChannel": "30 27 75", "hslChannel": "243.8 47.1% 20%" } ], "violet": [ { "scale": 50, "hex": "#f5f3ff", "rgb": "rgb(245,243,255)", "hsl": "hsl(250,100%,97.6%)", "oklch": "oklch(0.97,0.02,294)", "rgbChannel": "245 243 255", "hslChannel": "250 100% 97.6%" }, { "scale": 100, "hex": "#ede9fe", "rgb": "rgb(237,233,254)", "hsl": "hsl(251.4,91.3%,95.5%)", "oklch": "oklch(0.94,0.03,295)", "rgbChannel": "237 233 254", "hslChannel": "251.4 91.3% 95.5%" }, { "scale": 200, "hex": "#ddd6fe", "rgb": "rgb(221,214,254)", "hsl": "hsl(250.5,95.2%,91.8%)", "oklch": "oklch(0.89,0.05,293)", "rgbChannel": "221 214 254", "hslChannel": "250.5 95.2% 91.8%" }, { "scale": 300, "hex": "#c4b5fd", "rgb": "rgb(196,181,253)", "hsl": "hsl(252.5,94.7%,85.1%)", "oklch": "oklch(0.81,0.10,294)", "rgbChannel": "196 181 253", "hslChannel": "252.5 94.7% 85.1%" }, { "scale": 400, "hex": "#a78bfa", "rgb": "rgb(167,139,250)", "hsl": "hsl(255.1,91.7%,76.3%)", "oklch": "oklch(0.71,0.16,294)", "rgbChannel": "167 139 250", "hslChannel": "255.1 91.7% 76.3%" }, { "scale": 500, "hex": "#8b5cf6", "rgb": "rgb(139,92,246)", "hsl": "hsl(258.3,89.5%,66.3%)", "oklch": "oklch(0.61,0.22,293)", "rgbChannel": "139 92 246", "hslChannel": "258.3 89.5% 66.3%" }, { "scale": 600, "hex": "#7c3aed", "rgb": "rgb(124,58,237)", "hsl": "hsl(262.1,83.3%,57.8%)", "oklch": "oklch(0.54,0.25,293)", "rgbChannel": "124 58 237", "hslChannel": "262.1 83.3% 57.8%" }, { "scale": 700, "hex": "#6d28d9", "rgb": "rgb(109,40,217)", "hsl": "hsl(263.4,70%,50.4%)", "oklch": "oklch(0.49,0.24,293)", "rgbChannel": "109 40 217", "hslChannel": "263.4 70% 50.4%" }, { "scale": 800, "hex": "#5b21b6", "rgb": "rgb(91,33,182)", "hsl": "hsl(263.4,69.3%,42.2%)", "oklch": "oklch(0.43,0.21,293)", "rgbChannel": "91 33 182", "hslChannel": "263.4 69.3% 42.2%" }, { "scale": 900, "hex": "#4c1d95", "rgb": "rgb(76,29,149)", "hsl": "hsl(263.5,67.4%,34.9%)", "oklch": "oklch(0.38,0.18,294)", "rgbChannel": "76 29 149", "hslChannel": "263.5 67.4% 34.9%" }, { "scale": 950, "hex": "#1e1b4b", "rgb": "rgb(46,16,101)", "hsl": "hsl(261.2,72.6%,22.9%)", "oklch": "oklch(0.28,0.14,291)", "rgbChannel": "46 16 101", "hslChannel": "261.2 72.6% 22.9%" } ], "purple": [ { "scale": 50, "hex": "#faf5ff", "rgb": "rgb(250,245,255)", "hsl": "hsl(270,100%,98%)", "oklch": "oklch(0.98,0.01,308)", "rgbChannel": "250 245 255", "hslChannel": "270 100% 98%" }, { "scale": 100, "hex": "#f3e8ff", "rgb": "rgb(243,232,255)", "hsl": "hsl(268.7,100%,95.5%)", "oklch": "oklch(0.95,0.03,307)", "rgbChannel": "243 232 255", "hslChannel": "268.7 100% 95.5%" }, { "scale": 200, "hex": "#e9d5ff", "rgb": "rgb(233,213,255)", "hsl": "hsl(268.6,100%,91.8%)", "oklch": "oklch(0.90,0.06,307)", "rgbChannel": "233 213 255", "hslChannel": "268.6 100% 91.8%" }, { "scale": 300, "hex": "#d8b4fe", "rgb": "rgb(216,180,254)", "hsl": "hsl(269.2,97.4%,85.1%)", "oklch": "oklch(0.83,0.11,306)", "rgbChannel": "216 180 254", "hslChannel": "269.2 97.4% 85.1%" }, { "scale": 400, "hex": "#c084fc", "rgb": "rgb(192,132,252)", "hsl": "hsl(270,95.2%,75.3%)", "oklch": "oklch(0.72,0.18,306)", "rgbChannel": "192 132 252", "hslChannel": "270 95.2% 75.3%" }, { "scale": 500, "hex": "#a855f7", "rgb": "rgb(168,85,247)", "hsl": "hsl(270.7,91%,65.1%)", "oklch": "oklch(0.63,0.23,304)", "rgbChannel": "168 85 247", "hslChannel": "270.7 91% 65.1%" }, { "scale": 600, "hex": "#9333ea", "rgb": "rgb(147,51,234)", "hsl": "hsl(271.5,81.3%,55.9%)", "oklch": "oklch(0.56,0.25,302)", "rgbChannel": "147 51 234", "hslChannel": "271.5 81.3% 55.9%" }, { "scale": 700, "hex": "#7e22ce", "rgb": "rgb(126,34,206)", "hsl": "hsl(272.1,71.7%,47.1%)", "oklch": "oklch(0.50,0.24,302)", "rgbChannel": "126 34 206", "hslChannel": "272.1 71.7% 47.1%" }, { "scale": 800, "hex": "#6b21a8", "rgb": "rgb(107,33,168)", "hsl": "hsl(272.9,67.2%,39.4%)", "oklch": "oklch(0.44,0.20,304)", "rgbChannel": "107 33 168", "hslChannel": "272.9 67.2% 39.4%" }, { "scale": 900, "hex": "#581c87", "rgb": "rgb(88,28,135)", "hsl": "hsl(273.6,65.6%,32%)", "oklch": "oklch(0.38,0.17,305)", "rgbChannel": "88 28 135", "hslChannel": "273.6 65.6% 32%" }, { "scale": 950, "hex": "#3b0764", "rgb": "rgb(59,7,100)", "hsl": "hsl(273.5,86.9%,21%)", "oklch": "oklch(0.29,0.14,303)", "rgbChannel": "59 7 100", "hslChannel": "273.5 86.9% 21%" } ], "fuchsia": [ { "scale": 50, "hex": "#fdf4ff", "rgb": "rgb(253,244,255)", "hsl": "hsl(289.1,100%,97.8%)", "oklch": "oklch(0.98,0.02,320)", "rgbChannel": "253 244 255", "hslChannel": "289.1 100% 97.8%" }, { "scale": 100, "hex": "#fae8ff", "rgb": "rgb(250,232,255)", "hsl": "hsl(287,100%,95.5%)", "oklch": "oklch(0.95,0.04,319)", "rgbChannel": "250 232 255", "hslChannel": "287 100% 95.5%" }, { "scale": 200, "hex": "#f5d0fe", "rgb": "rgb(245,208,254)", "hsl": "hsl(288.3,95.8%,90.6%)", "oklch": "oklch(0.90,0.07,320)", "rgbChannel": "245 208 254", "hslChannel": "288.3 95.8% 90.6%" }, { "scale": 300, "hex": "#f0abfc", "rgb": "rgb(240,171,252)", "hsl": "hsl(291.1,93.1%,82.9%)", "oklch": "oklch(0.83,0.13,321)", "rgbChannel": "240 171 252", "hslChannel": "291.1 93.1% 82.9%" }, { "scale": 400, "hex": "#e879f9", "rgb": "rgb(232,121,249)", "hsl": "hsl(292,91.4%,72.5%)", "oklch": "oklch(0.75,0.21,322)", "rgbChannel": "232 121 249", "hslChannel": "292 91.4% 72.5%" }, { "scale": 500, "hex": "#d946ef", "rgb": "rgb(217,70,239)", "hsl": "hsl(292.2,84.1%,60.6%)", "oklch": "oklch(0.67,0.26,322)", "rgbChannel": "217 70 239", "hslChannel": "292.2 84.1% 60.6%" }, { "scale": 600, "hex": "#c026d3", "rgb": "rgb(192,38,211)", "hsl": "hsl(293.4,69.5%,48.8%)", "oklch": "oklch(0.59,0.26,323)", "rgbChannel": "192 38 211", "hslChannel": "293.4 69.5% 48.8%" }, { "scale": 700, "hex": "#a21caf", "rgb": "rgb(162,28,175)", "hsl": "hsl(294.7,72.4%,39.8%)", "oklch": "oklch(0.52,0.23,324)", "rgbChannel": "162 28 175", "hslChannel": "294.7 72.4% 39.8%" }, { "scale": 800, "hex": "#86198f", "rgb": "rgb(134,25,143)", "hsl": "hsl(295.4,70.2%,32.9%)", "oklch": "oklch(0.45,0.19,325)", "rgbChannel": "134 25 143", "hslChannel": "295.4 70.2% 32.9%" }, { "scale": 900, "hex": "#701a75", "rgb": "rgb(112,26,117)", "hsl": "hsl(296.7,63.6%,28%)", "oklch": "oklch(0.40,0.16,326)", "rgbChannel": "112 26 117", "hslChannel": "296.7 63.6% 28%" }, { "scale": 950, "hex": "#4a044e", "rgb": "rgb(74,4,78)", "hsl": "hsl(296.8,90.2%,16.1%)", "oklch": "oklch(0.29,0.13,326)", "rgbChannel": "74 4 78", "hslChannel": "296.8 90.2% 16.1%" } ], "pink": [ { "scale": 50, "hex": "#fdf2f8", "rgb": "rgb(253,242,248)", "hsl": "hsl(327.3,73.3%,97.1%)", "oklch": "oklch(0.97,0.01,343)", "rgbChannel": "253 242 248", "hslChannel": "327.3 73.3% 97.1%" }, { "scale": 100, "hex": "#fce7f3", "rgb": "rgb(252,231,243)", "hsl": "hsl(325.7,77.8%,94.7%)", "oklch": "oklch(0.95,0.03,342)", "rgbChannel": "252 231 243", "hslChannel": "325.7 77.8% 94.7%" }, { "scale": 200, "hex": "#fbcfe8", "rgb": "rgb(251,207,232)", "hsl": "hsl(325.9,84.6%,89.8%)", "oklch": "oklch(0.90,0.06,343)", "rgbChannel": "251 207 232", "hslChannel": "325.9 84.6% 89.8%" }, { "scale": 300, "hex": "#f9a8d4", "rgb": "rgb(249,168,212)", "hsl": "hsl(327.4,87.1%,81.8%)", "oklch": "oklch(0.82,0.11,346)", "rgbChannel": "249 168 212", "hslChannel": "327.4 87.1% 81.8%" }, { "scale": 400, "hex": "#f472b6", "rgb": "rgb(244,114,182)", "hsl": "hsl(328.6,85.5%,70.2%)", "oklch": "oklch(0.73,0.18,350)", "rgbChannel": "244 114 182", "hslChannel": "328.6 85.5% 70.2%" }, { "scale": 500, "hex": "#ec4899", "rgb": "rgb(236,72,153)", "hsl": "hsl(330.4,81.2%,60.4%)", "oklch": "oklch(0.66,0.21,354)", "rgbChannel": "236 72 153", "hslChannel": "330.4 81.2% 60.4%" }, { "scale": 600, "hex": "#db2777", "rgb": "rgb(219,39,119)", "hsl": "hsl(333.3,71.4%,50.6%)", "oklch": "oklch(0.59,0.22,1)", "rgbChannel": "219 39 119", "hslChannel": "333.3 71.4% 50.6%" }, { "scale": 700, "hex": "#be185d", "rgb": "rgb(190,24,93)", "hsl": "hsl(335.1,77.6%,42%)", "oklch": "oklch(0.52,0.20,4)", "rgbChannel": "190 24 93", "hslChannel": "335.1 77.6% 42%" }, { "scale": 800, "hex": "#9d174d", "rgb": "rgb(157,23,77)", "hsl": "hsl(335.8,74.4%,35.3%)", "oklch": "oklch(0.46,0.17,4)", "rgbChannel": "157 23 77", "hslChannel": "335.8 74.4% 35.3%" }, { "scale": 900, "hex": "#831843", "rgb": "rgb(131,24,67)", "hsl": "hsl(335.9,69%,30.4%)", "oklch": "oklch(0.41,0.14,2)", "rgbChannel": "131 24 67", "hslChannel": "335.9 69% 30.4%" }, { "scale": 950, "hex": "#500724", "rgb": "rgb(80,7,36)", "hsl": "hsl(336.2,83.9%,17.1%)", "oklch": "oklch(0.28,0.10,4)", "rgbChannel": "80 7 36", "hslChannel": "336.2 83.9% 17.1%" } ], "rose": [ { "scale": 50, "hex": "#fff1f2", "rgb": "rgb(255,241,242)", "hsl": "hsl(355.7,100%,97.3%)", "oklch": "oklch(0.97,0.02,12)", "rgbChannel": "255 241 242", "hslChannel": "355.7 100% 97.3%" }, { "scale": 100, "hex": "#ffe4e6", "rgb": "rgb(255,228,230)", "hsl": "hsl(355.6,100%,94.7%)", "oklch": "oklch(0.94,0.03,13)", "rgbChannel": "255 228 230", "hslChannel": "355.6 100% 94.7%" }, { "scale": 200, "hex": "#fecdd3", "rgb": "rgb(254,205,211)", "hsl": "hsl(352.7,96.1%,90%)", "oklch": "oklch(0.89,0.06,10)", "rgbChannel": "254 205 211", "hslChannel": "352.7 96.1% 90%" }, { "scale": 300, "hex": "#fda4af", "rgb": "rgb(253,164,175)", "hsl": "hsl(352.6,95.7%,81.8%)", "oklch": "oklch(0.81,0.11,12)", "rgbChannel": "253 164 175", "hslChannel": "352.6 95.7% 81.8%" }, { "scale": 400, "hex": "#fb7185", "rgb": "rgb(251,113,133)", "hsl": "hsl(351.3,94.5%,71.4%)", "oklch": "oklch(0.72,0.17,13)", "rgbChannel": "251 113 133", "hslChannel": "351.3 94.5% 71.4%" }, { "scale": 500, "hex": "#f43f5e", "rgb": "rgb(244,63,94)", "hsl": "hsl(349.7,89.2%,60.2%)", "oklch": "oklch(0.65,0.22,16)", "rgbChannel": "244 63 94", "hslChannel": "349.7 89.2% 60.2%" }, { "scale": 600, "hex": "#e11d48", "rgb": "rgb(225,29,72)", "hsl": "hsl(346.8,77.2%,49.8%)", "oklch": "oklch(0.59,0.22,18)", "rgbChannel": "225 29 72", "hslChannel": "346.8 77.2% 49.8%" }, { "scale": 700, "hex": "#be123c", "rgb": "rgb(190,18,60)", "hsl": "hsl(345.3,82.7%,40.8%)", "oklch": "oklch(0.51,0.20,17)", "rgbChannel": "190 18 60", "hslChannel": "345.3 82.7% 40.8%" }, { "scale": 800, "hex": "#9f1239", "rgb": "rgb(159,18,57)", "hsl": "hsl(343.4,79.7%,34.7%)", "oklch": "oklch(0.45,0.17,14)", "rgbChannel": "159 18 57", "hslChannel": "343.4 79.7% 34.7%" }, { "scale": 900, "hex": "#881337", "rgb": "rgb(136,19,55)", "hsl": "hsl(341.5,75.5%,30.4%)", "oklch": "oklch(0.41,0.15,10)", "rgbChannel": "136 19 55", "hslChannel": "341.5 75.5% 30.4%" }, { "scale": 950, "hex": "#4c0519", "rgb": "rgb(76,5,25)", "hsl": "hsl(343.1,87.7%,15.9%)", "oklch": "oklch(0.27,0.10,12)", "rgbChannel": "76 5 25", "hslChannel": "343.1 87.7% 15.9%" } ] } ================================================ FILE: crates/ui/src/theme/default-theme.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Default", "author": "shadcn", "url": "https://ui.shadcn.com", "_ref": "https://github.com/shadcn-ui/ui/blob/main/apps/v4/public/r/colors/neutral.json", "themes": [ { "is_default": true, "name": "Default Light", "mode": "light", "colors": { "accent.background": "neutral-100", "accent.foreground": "neutral-900", "accordion.background": "white", "background": "white", "border": "neutral-200", "group_box.background": "#f5f5f5", "group_box.foreground": "#171717", "caret": "#0a0a0a", "chart_1": "#93c5fd", "chart_2": "#3b82f6", "chart_3": "#2563eb", "chart_4": "#1d4ed8", "chart_5": "#1e40af", "chart_bullish": "green-600", "chart_bearish": "red-600", "danger.background": "red-500", "danger.foreground": "neutral-50", "description_list_label.foreground": "#171717", "drag_border": "#3b82f6", "drop_target.background": "#3b82f640", "foreground": "neutral-950", "info.background": "cyan-500", "info.foreground": "neutral-50", "input.border": "neutral-200", "link.foreground": "#0a0a0a", "link.active.foreground": "#0a0a0a", "link.hover.foreground": "#404040", "list.background": "white", "list.active.background": "#bfdbfe33", "list.active.border": "#60a5fa", "list.even.background": "#fafafa", "list.head.background": "#fafafa", "list.hover.background": "#f5f5f5", "muted.background": "neutral-100", "muted.foreground": "neutral-500", "popover.background": "white", "popover.foreground": "neutral-950", "primary.background": "neutral-900", "primary.active.background": "neutral-950", "primary.foreground": "neutral-50", "primary.hover.background": "neutral-800", "progress_bar.background": "#171717", "ring": "neutral-950", "scrollbar.background": "#fafafa00", "scrollbar.thumb.background": "#a3a3a3e6", "scrollbar.thumb.hover.background": "#a3a3a3", "secondary.background": "neutral-200", "secondary.active.background": "neutral-300", "secondary.foreground": "neutral-900", "secondary.hover.background": "neutral-200", "selection.background": "#55a0fc", "sidebar.background": "#fafafa", "sidebar.accent.background": "#e5e5e5", "sidebar.accent.foreground": "#171717", "sidebar.border": "#e5e5e5", "sidebar.foreground": "#171717", "sidebar.primary.background": "#171717", "sidebar.primary.foreground": "#fafafa", "skeleton.background": "#f5f5f5", "slider.bar.background": "#171717", "slider.thumb.background": "white", "success.background": "green-500", "success.foreground": "neutral-50", "switch.background": "#d4d4d4", "tab.background": "#00000000", "tab.active.background": "white", "tab.active.foreground": "#171717", "tab_bar.background": "#f5f5f5", "tab_bar.segmented.background": "#f5f5f5", "tab.foreground": "#404040", "table.background": "white", "table.active.background": "#bfdbfe33", "table.active.border": "#60a5fa", "table.even.background": "#fafafa", "table.head.background": "#fafafa", "table.head.foreground": "#737373", "table.hover.background": "#f5f5f5", "table.row.border": "#e5e5e5b3", "tiles.background": "#fafafa", "title_bar.background": "#F8F8F8", "title_bar.border": "#e5e5e5", "warning.background": "yellow-500", "warning.foreground": "neutral-50", "overlay": "#0000000d", "window.border": "#e5e5e5", "base.red": "red-600", "base.red.light": "red-400", "base.green": "green-600", "base.green.light": "green-400", "base.blue": "blue-600", "base.blue.light": "blue-400", "base.yellow": "yellow-600", "base.yellow.light": "yellow-400", "base.magenta": "purple-600", "base.magenta.light": "purple-400", "base.cyan": "cyan-600", "base.cyan.light": "cyan-400" }, "highlight": { "editor.foreground": "#000000", "editor.background": "#ffffff", "editor.active_line.background": "#F5F5F5", "editor.line_number": "#929292", "editor.active_line_number": "#000000", "editor.invisible": "#73737366", "conflict": "#C5060B", "created": "#1642FF", "hidden": "#6D6D6D", "hint": "#9e5dff", "modified": "#9e7008", "predictive": "#A4ABB6", "warning": "#C99401", "syntax": { "attribute": { "color": "#957931" }, "boolean": { "color": "#C5060B" }, "comment": { "color": "#007fff" }, "comment.doc": { "color": "#007fff" }, "constant": { "color": "#C5060B" }, "constructor": { "color": "#0433ff" }, "embedded": { "color": "#333333" }, "function": { "color": "#0000A2" }, "keyword": { "color": "#0433ff" }, "link_text": { "color": "#0000A2", "font_style": "normal" }, "link_uri": { "color": "#6A7293", "font_style": "italic" }, "number": { "color": "#0433ff" }, "string": { "color": "#036A07" }, "string.escape": { "color": "#036A07" }, "string.regex": { "color": "#036A07" }, "string.special": { "color": "#d21f07" }, "string.special.symbol": { "color": "#d21f07" }, "tag": { "color": "#0433ff" }, "text.literal": { "color": "#6F42C1" }, "title": { "color": "#0433FF" }, "type": { "color": "#6f42c1" }, "property": { "color": "#333333" }, "variable": { "color": "#333333" }, "variable.special": { "color": "#C5060B" } } } }, { "is_default": true, "name": "Default Dark", "mode": "dark", "colors": { "accent.background": "neutral-800", "accent.foreground": "neutral-50", "accordion.background": "#0a0a0a", "background": "neutral-950", "border": "neutral-800", "group_box.background": "neutral-950", "group_box.foreground": "neutral-50", "caret": "#fafafa", "chart_1": "#93c5fd", "chart_2": "#3b82f6", "chart_3": "#2563eb", "chart_4": "#1d4ed8", "chart_5": "#1e40af", "chart_bullish": "green-600", "chart_bearish": "red-600", "danger.background": "red-400", "danger.foreground": "red-600", "description_list_label.background": "#171717", "description_list_label.foreground": "#f5f5f5", "drag_border": "#3b82f6", "drop_target.background": "#3b82f619", "foreground": "neutral-50", "info.background": "cyan-400", "info.foreground": "cyan-600", "input.border": "#2f2f2f", "link.foreground": "#fafafa", "link.active.foreground": "#d4d4d4", "link.hover.foreground": "#ffffff", "list.background": "#0a0a0a", "list.active.background": "#1e40af33", "list.active.border": "#1d4ed8", "list.even.background": "#17171766", "list.head.background": "#17171766", "muted.background": "neutral-800", "muted.foreground": "neutral-400", "popover.background": "neutral-950", "popover.foreground": "neutral-50", "primary.background": "neutral-50", "primary.active.background": "neutral-200", "primary.foreground": "neutral-900", "primary.hover.background": "neutral-100", "progress_bar.background": "#f5f5f5", "ring": "neutral-300", "scrollbar.background": "#17171700", "scrollbar.thumb.background": "#525252e6", "scrollbar.thumb.hover.background": "#525252", "secondary.background": "neutral-800", "secondary.active.background": "#212121", "secondary.foreground": "neutral-50", "secondary.hover.background": "#292929", "selection.background": "#1d4ed8", "sidebar.background": "#0a0a0a", "sidebar.accent.background": "#262626", "sidebar.accent.foreground": "#f5f5f5", "sidebar.border": "#262626", "sidebar.foreground": "#f5f5f5", "sidebar.primary.background": "#f5f5f5", "sidebar.primary.foreground": "#0a0a0a", "skeleton.background": "#171717", "slider.bar.background": "#fafafa", "slider.thumb.background": "#0a0a0a", "success.background": "green-400", "success.foreground": "green-600", "switch.background": "#404040", "tab.background": "#00000000", "tab.active.background": "#0a0a0a", "tab.active.foreground": "#fafafa", "tab_bar.background": "#171717", "tab_bar.segmented.background": "#171717", "tab.foreground": "#d4d4d4", "table.background": "#0a0a0a", "table.head.foreground": "#525252", "table.row.border": "#262626b3", "tiles.background": "#171717", "title_bar.background": "#171717", "title_bar.border": "#262626", "warning.background": "yellow-400", "warning.foreground": "yellow-600", "overlay": "#ffffff08", "window.border": "#262626", "base.red": "red-400", "base.red.light": "red-300", "base.green": "green-400", "base.green.light": "green-300", "base.blue": "blue-400", "base.blue.light": "blue-300", "base.yellow": "yellow-400", "base.yellow.light": "yellow-300", "base.magenta": "purple-400", "base.magenta.light": "purple-300", "base.cyan": "cyan-400", "base.cyan.light": "cyan-300" }, "highlight": { "editor.foreground": "#CACCCA", "editor.background": "#0a0a0a", "editor.active_line.background": "#171717", "editor.line_number": "#8F8F8F", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#73737366", "conflict": "#D2602D", "created": "#3f72e2", "created.background": "#0C4619", "deleted.background": "#46190C", "error.background": "#46190C", "error.border": "#802207", "hidden": "#9E9E9E", "hint": "#b283f8", "hint.background": "#250c4b", "hint.border": "#3f0891", "info.background": "#0059D1", "info.border": "#0059D1", "modified": "#B0A878", "modified.background": "#3A310E", "predictive": "#5D5945", "success.background": "#0C4619", "warning.background": "#3A310E", "warning.border": "#7B6508", "syntax": { "attribute": { "color": "#e7cb8f" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#fdd888" }, "keyword": { "color": "#c28b12" }, "link_text": { "color": "#307BF6", "font_style": "normal" }, "link_uri": { "color": "#7faef9", "font_style": "italic" }, "number": { "color": "#E1D797" }, "string": { "color": "#62BA46" }, "string.escape": { "color": "#62BA46" }, "string.regex": { "color": "#62BA46" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#fdd888", "font_weight": 600 }, "type": { "color": "#c75828" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } } ] } ================================================ FILE: crates/ui/src/theme/mod.rs ================================================ use crate::{ highlighter::HighlightTheme, list::ListSettings, notification::NotificationSettings, scroll::ScrollbarShow, sheet::SheetSettings, }; use gpui::{App, Global, Hsla, Pixels, SharedString, Window, WindowAppearance, px}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ ops::{Deref, DerefMut}, rc::Rc, sync::Arc, }; mod color; mod registry; mod schema; mod theme_color; pub use color::*; pub use registry::*; pub use schema::*; pub use theme_color::*; pub fn init(cx: &mut App) { registry::init(cx); // Ensure theme is loaded directly on startup for WASM compatibility Theme::change(ThemeMode::Light, None, cx); Theme::sync_scrollbar_appearance(cx); } pub trait ActiveTheme { fn theme(&self) -> &Theme; } impl ActiveTheme for App { #[inline(always)] fn theme(&self) -> &Theme { Theme::global(self) } } /// The global theme configuration. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct Theme { pub colors: ThemeColor, pub highlight_theme: Arc, pub light_theme: Rc, pub dark_theme: Rc, pub mode: ThemeMode, /// The font family for the application, default is `.SystemUIFont`. pub font_family: SharedString, /// The base font size for the application, default is 16px. pub font_size: Pixels, /// The monospace font family for the application. /// /// Defaults to: /// /// - macOS: `Menlo` /// - Windows: `Consolas` /// - Linux: `DejaVu Sans Mono` pub mono_font_family: SharedString, /// The monospace font size for the application, default is 13px. pub mono_font_size: Pixels, /// Radius for the general elements. pub radius: Pixels, /// Radius for the large elements, e.g.: Dialog, Notification border radius. pub radius_lg: Pixels, pub shadow: bool, pub transparent: Hsla, /// Show the scrollbar mode, default: Scrolling pub scrollbar_show: ScrollbarShow, /// The notification setting. pub notification: NotificationSettings, /// Tile grid size, default is 4px. pub tile_grid_size: Pixels, /// The shadow of the tile panel. pub tile_shadow: bool, /// The border radius of the tile panel, default is 0px. pub tile_radius: Pixels, /// The list settings. pub list: ListSettings, /// The sheet settings. pub sheet: SheetSettings, } impl Default for Theme { fn default() -> Self { Self::from(&ThemeColor::default()) } } impl Deref for Theme { type Target = ThemeColor; fn deref(&self) -> &Self::Target { &self.colors } } impl DerefMut for Theme { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.colors } } impl Global for Theme {} impl Theme { /// Returns the global theme reference #[inline(always)] pub fn global(cx: &App) -> &Theme { cx.global::() } /// Returns the global theme mutable reference #[inline(always)] pub fn global_mut(cx: &mut App) -> &mut Theme { cx.global_mut::() } /// Returns true if the theme is dark. #[inline(always)] pub fn is_dark(&self) -> bool { self.mode.is_dark() } /// Returns the current theme name. pub fn theme_name(&self) -> &SharedString { if self.is_dark() { &self.dark_theme.name } else { &self.light_theme.name } } /// Sync the theme with the system appearance pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) { // Better use window.appearance() for avoid error on Linux. // https://github.com/longbridge/gpui-component/issues/104 let appearance = window .as_ref() .map(|window| window.appearance()) .unwrap_or_else(|| cx.window_appearance()); Self::change(appearance, window, cx); } /// Sync the Scrollbar showing behavior with the system pub fn sync_scrollbar_appearance(cx: &mut App) { Theme::global_mut(cx).scrollbar_show = if cx.should_auto_hide_scrollbars() { ScrollbarShow::Scrolling } else { ScrollbarShow::Hover }; } /// Change the theme mode. pub fn change(mode: impl Into, window: Option<&mut Window>, cx: &mut App) { let mode = mode.into(); if !cx.has_global::() { let mut theme = Theme::default(); theme.light_theme = ThemeRegistry::global(cx).default_light_theme().clone(); theme.dark_theme = ThemeRegistry::global(cx).default_dark_theme().clone(); cx.set_global(theme); } let theme = cx.global_mut::(); theme.mode = mode; if mode.is_dark() { theme.apply_config(&theme.dark_theme.clone()); } else { theme.apply_config(&theme.light_theme.clone()); } if let Some(window) = window { window.refresh(); } } /// Get the input background color. /// /// For dark, use a transparent color mixed with the input border: `cx.theme().input`, /// otherwise use the `cx.theme().background` color. #[inline] pub fn input_background(&self) -> Hsla { if self.is_dark() { self.input.mix_oklab(self.transparent, 0.3) } else { self.background } } /// Get the editor background color, if not set, use the input background color. #[inline] pub(crate) fn editor_background(&self) -> Hsla { self.highlight_theme .style .editor_background .unwrap_or_else(|| self.input_background()) } } impl From<&ThemeColor> for Theme { fn from(colors: &ThemeColor) -> Self { Theme { mode: ThemeMode::default(), transparent: Hsla::transparent_black(), font_family: ".SystemUIFont".into(), font_size: px(16.), mono_font_family: if cfg!(target_os = "macos") { // https://en.wikipedia.org/wiki/Menlo_(typeface) "Menlo".into() } else if cfg!(target_os = "windows") { "Consolas".into() } else { "DejaVu Sans Mono".into() }, mono_font_size: px(13.), radius: px(6.), radius_lg: px(8.), shadow: true, scrollbar_show: ScrollbarShow::default(), notification: NotificationSettings::default(), tile_grid_size: px(8.), tile_shadow: true, tile_radius: px(0.), list: ListSettings::default(), colors: *colors, light_theme: Rc::new(ThemeConfig::default()), dark_theme: Rc::new(ThemeConfig::default()), highlight_theme: HighlightTheme::default_light(), sheet: SheetSettings::default(), } } } #[derive( Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize, JsonSchema, )] #[serde(rename_all = "snake_case")] pub enum ThemeMode { #[default] Light, Dark, } impl ThemeMode { #[inline(always)] pub fn is_dark(&self) -> bool { matches!(self, Self::Dark) } /// Return lower_case theme name: `light`, `dark`. pub fn name(&self) -> &'static str { match self { ThemeMode::Light => "light", ThemeMode::Dark => "dark", } } } impl From for ThemeMode { fn from(appearance: WindowAppearance) -> Self { match appearance { WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark, WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light, } } } ================================================ FILE: crates/ui/src/theme/registry.rs ================================================ use crate::{Theme, ThemeColor, ThemeConfig, ThemeMode, ThemeSet, highlighter::HighlightTheme}; #[allow(unused)] use anyhow::Result; use gpui::{App, Global, SharedString}; use std::{ collections::HashMap, path::PathBuf, rc::Rc, sync::{Arc, LazyLock}, }; const DEFAULT_THEME: &str = include_str!("./default-theme.json"); pub(crate) static DEFAULT_THEME_COLORS: LazyLock< HashMap, Arc)>, > = LazyLock::new(|| { let mut colors = HashMap::new(); let themes: Vec = serde_json::from_str::(DEFAULT_THEME) .expect("Failed to parse themes/default.json") .themes; for theme in themes { let mut theme_color = ThemeColor::default(); theme_color.apply_config(&theme, &ThemeColor::default()); let highlight_theme = HighlightTheme { name: theme.name.to_string(), appearance: theme.mode, style: theme.highlight.unwrap_or_default(), }; colors.insert( theme.mode, (Arc::new(theme_color), Arc::new(highlight_theme)), ); } colors }); pub(super) fn init(cx: &mut App) { cx.set_global(ThemeRegistry::default()); ThemeRegistry::global_mut(cx).init_default_themes(); // Observe changes to the theme registry to apply changes to the active theme cx.observe_global::(|cx| { let mode = Theme::global(cx).mode; let light_theme = Theme::global(cx).light_theme.name.clone(); let dark_theme = Theme::global(cx).dark_theme.name.clone(); if let Some(theme) = ThemeRegistry::global(cx) .themes() .get(&light_theme) .cloned() { Theme::global_mut(cx).light_theme = theme; } if let Some(theme) = ThemeRegistry::global(cx).themes().get(&dark_theme).cloned() { Theme::global_mut(cx).dark_theme = theme; } let theme_name = if mode.is_dark() { dark_theme } else { light_theme }; tracing::info!("Reload active theme: {:?}...", theme_name); Theme::change(mode, None, cx); cx.refresh_windows(); }) .detach(); } #[derive(Default, Debug)] pub struct ThemeRegistry { themes_dir: PathBuf, default_themes: HashMap>, themes: HashMap>, has_custom_themes: bool, } impl Global for ThemeRegistry {} impl ThemeRegistry { pub fn global(cx: &App) -> &Self { cx.global::() } pub fn global_mut(cx: &mut App) -> &mut Self { cx.global_mut::() } /// Watch themes directory. /// /// And reload themes to trigger the `on_load` callback. #[cfg(not(target_family = "wasm"))] pub fn watch_dir(themes_dir: PathBuf, cx: &mut App, on_load: F) -> Result<()> where F: Fn(&mut App) + 'static, { Self::global_mut(cx).themes_dir = themes_dir.clone(); // Load theme in the background. cx.spawn(async move |cx| { _ = cx.update(|cx| { if let Err(err) = Self::_watch_themes_dir(themes_dir, cx) { tracing::error!("Failed to watch themes directory: {}", err); } Self::reload_themes(cx); on_load(cx); }); }) .detach(); Ok(()) } /// Returns a reference to the map of themes (including default themes). pub fn themes(&self) -> &HashMap> { &self.themes } /// Returns a sorted list of themes. pub fn sorted_themes(&self) -> Vec<&Rc> { let mut themes = self.themes.values().collect::>(); // sort by is_default true first, then light first dark later, then by name case-insensitive themes.sort_by(|a, b| { b.is_default .cmp(&a.is_default) .then(a.mode.cmp(&b.mode)) .then(a.name.to_lowercase().cmp(&b.name.to_lowercase())) }); themes } /// Returns a reference to the map of default themes. pub fn default_themes(&self) -> &HashMap> { &self.default_themes } pub fn default_light_theme(&self) -> &Rc { &self.default_themes[&ThemeMode::Light] } pub fn default_dark_theme(&self) -> &Rc { &self.default_themes[&ThemeMode::Dark] } pub fn load_themes_from_str(&mut self, content: &str) -> anyhow::Result<()> { let theme_set = serde_json::from_str::(content)?; for theme in theme_set.themes { if !self.themes.contains_key(&theme.name) { let theme_name = theme.name.clone(); self.themes.insert(theme_name, Rc::new(theme)); self.has_custom_themes = true; } } Ok(()) } fn init_default_themes(&mut self) { let default_themes: Vec = serde_json::from_str::(DEFAULT_THEME) .expect("failed to parse default theme.") .themes; for theme in default_themes.into_iter() { if theme.mode.is_dark() { self.default_themes.insert(ThemeMode::Dark, Rc::new(theme)); } else { self.default_themes.insert(ThemeMode::Light, Rc::new(theme)); } } self.themes_dir = PathBuf::from("./themes"); self.themes = self .default_themes .values() .map(|theme| { let name = theme.name.clone(); (name, Rc::clone(theme)) }) .collect(); } #[cfg(not(target_family = "wasm"))] fn _watch_themes_dir(themes_dir: PathBuf, cx: &mut App) -> anyhow::Result<()> { if !themes_dir.exists() { std::fs::create_dir_all(&themes_dir)?; } let (tx, rx) = smol::channel::bounded(100); let mut watcher = notify::recommended_watcher(move |res: notify::Result| { if let Ok(event) = &res { match event.kind { notify::EventKind::Create(_) | notify::EventKind::Modify(_) | notify::EventKind::Remove(_) => { if let Err(err) = tx.send_blocking(res) { tracing::error!("Failed to send theme event: {:?}", err); } } _ => {} } } })?; cx.spawn(async move |cx| { use notify::Watcher as _; if let Err(err) = watcher.watch(&themes_dir, notify::RecursiveMode::Recursive) { tracing::error!("Failed to watch themes directory: {:?}", err); } while (rx.recv().await).is_ok() { tracing::info!("Reloading themes..."); _ = cx.update(Self::reload_themes); } }) .detach(); Ok(()) } #[cfg(not(target_family = "wasm"))] fn reload_themes(cx: &mut App) { let registry = Self::global_mut(cx); match registry.reload() { Ok(_) => { tracing::info!("Themes reloaded successfully."); } Err(e) => tracing::error!("Failed to reload themes: {:?}", e), } } #[cfg(not(target_family = "wasm"))] /// Reload themes from the `themes_dir`. fn reload(&mut self) -> Result<()> { let mut themes = vec![]; if self.themes_dir.exists() { for entry in std::fs::read_dir(&self.themes_dir)? { let entry = entry?; let path = entry.path(); if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("json") { let file_content = std::fs::read_to_string(path.clone())?; match serde_json::from_str::(&file_content) { Ok(theme_set) => { themes.extend(theme_set.themes); } Err(e) => { tracing::error!( "ignored invalid theme file: {}, {}", path.display(), e ); } } } } } self.themes.clear(); for theme in self.default_themes.values() { self.themes .insert(theme.name.clone(), Rc::new((**theme).clone())); } for theme in themes.iter() { if self.themes.contains_key(&theme.name) { continue; } if theme.is_default { self.default_themes .insert(theme.mode, Rc::new(theme.clone())); } self.has_custom_themes = true; self.themes .insert(theme.name.clone(), Rc::new(theme.clone())); } Ok(()) } } ================================================ FILE: crates/ui/src/theme/schema.rs ================================================ use std::{rc::Rc, sync::Arc}; use gpui::{SharedString, px}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ Colorize, Theme, ThemeColor, ThemeMode, highlighter::{HighlightTheme, HighlightThemeStyle}, try_parse_color, }; /// Represents a theme configuration. #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(default)] pub struct ThemeSet { /// The name of the theme set. pub name: SharedString, /// The author of the theme. pub author: Option, /// The URL of the theme. pub url: Option, /// The theme list of the theme set. #[serde(rename = "themes")] pub themes: Vec, } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(default)] pub struct ThemeConfig { /// Whether this theme is the default theme. pub is_default: bool, /// The name of the theme. pub name: SharedString, /// The mode of the theme, default is light. pub mode: ThemeMode, /// The base font size, default is 16. #[serde(rename = "font.size")] pub font_size: Option, /// The base font family, default is system font: `.SystemUIFont`. #[serde(rename = "font.family")] pub font_family: Option, /// The monospace font family, default is platform specific: /// - macOS: `Menlo` /// - Windows: `Consolas` /// - Linux: `DejaVu Sans Mono` #[serde(rename = "mono_font.family")] pub mono_font_family: Option, /// The monospace font size, default is 13. #[serde(rename = "mono_font.size")] pub mono_font_size: Option, /// The border radius for general elements, default is 6. #[serde(rename = "radius")] pub radius: Option, /// The border radius for large elements like Dialogs and Notifications, default is 8. #[serde(rename = "radius.lg")] pub radius_lg: Option, /// Set shadows in the theme, for example the Input and Button, default is true. #[serde(rename = "shadow")] pub shadow: Option, /// The colors of the theme. pub colors: ThemeConfigColors, /// The highlight theme, this part is combilbility with `style` section in Zed theme. /// /// https://github.com/zed-industries/zed/blob/f50041779dcfd7a76c8aec293361c60c53f02d51/assets/themes/ayu/ayu.json#L9 pub highlight: Option, } #[derive(Debug, Default, Clone, JsonSchema, Serialize, Deserialize)] pub struct ThemeConfigColors { /// Used for accents such as hover background on MenuItem, ListItem, etc. #[serde(rename = "accent.background")] pub accent: Option, /// Used for accent text color. #[serde(rename = "accent.foreground")] pub accent_foreground: Option, /// Accordion background color. #[serde(rename = "accordion.background")] pub accordion: Option, /// Accordion hover background color. #[serde(rename = "accordion.hover.background")] pub accordion_hover: Option, /// Default background color. #[serde(rename = "background")] pub background: Option, /// Default border color #[serde(rename = "border")] pub border: Option, /// Button primary background color, fallback to `primary`. #[serde(rename = "button.primary.background")] pub button_primary: Option, /// Button primary active background color, fallback to `primary_active`. #[serde(rename = "button.primary.active.background")] pub button_primary_active: Option, /// Button primary text color, fallback to `primary_foreground`. #[serde(rename = "button.primary.foreground")] pub button_primary_foreground: Option, /// Button primary hover background color, fallback to `primary_hover`. #[serde(rename = "button.primary.hover.background")] pub button_primary_hover: Option, /// Background color for GroupBox. #[serde(rename = "group_box.background")] pub group_box: Option, /// Text color for GroupBox. #[serde(rename = "group_box.foreground")] pub group_box_foreground: Option, /// Title text color for GroupBox. #[serde(rename = "group_box.title.foreground")] pub group_box_title_foreground: Option, /// Input caret color (Blinking cursor). #[serde(rename = "caret")] pub caret: Option, /// Chart 1 color. #[serde(rename = "chart.1")] pub chart_1: Option, /// Chart 2 color. #[serde(rename = "chart.2")] pub chart_2: Option, /// Chart 3 color. #[serde(rename = "chart.3")] pub chart_3: Option, /// Chart 4 color. #[serde(rename = "chart.4")] pub chart_4: Option, /// Chart 5 color. #[serde(rename = "chart.5")] pub chart_5: Option, /// Bullish color for candlestick charts (upward price movement). #[serde(rename = "chart_bullish")] pub chart_bullish: Option, /// Bearish color for candlestick charts (downward price movement). #[serde(rename = "chart_bearish")] pub chart_bearish: Option, /// Danger background color. #[serde(rename = "danger.background")] pub danger: Option, /// Danger active background color. #[serde(rename = "danger.active.background")] pub danger_active: Option, /// Danger text color. #[serde(rename = "danger.foreground")] pub danger_foreground: Option, /// Danger hover background color. #[serde(rename = "danger.hover.background")] pub danger_hover: Option, /// Description List label background color. #[serde(rename = "description_list.label.background")] pub description_list_label: Option, /// Description List label foreground color. #[serde(rename = "description_list.label.foreground")] pub description_list_label_foreground: Option, /// Drag border color. #[serde(rename = "drag.border")] pub drag_border: Option, /// Drop target background color. #[serde(rename = "drop_target.background")] pub drop_target: Option, /// Default text color. #[serde(rename = "foreground")] pub foreground: Option, /// Info background color. #[serde(rename = "info.background")] pub info: Option, /// Info active background color. #[serde(rename = "info.active.background")] pub info_active: Option, /// Info text color. #[serde(rename = "info.foreground")] pub info_foreground: Option, /// Info hover background color. #[serde(rename = "info.hover.background")] pub info_hover: Option, /// Border color for inputs such as Input, Select, etc. #[serde(rename = "input.border")] pub input: Option, /// Link text color. #[serde(rename = "link")] pub link: Option, /// Active link text color. #[serde(rename = "link.active")] pub link_active: Option, /// Hover link text color. #[serde(rename = "link.hover")] pub link_hover: Option, /// Background color for List and ListItem. #[serde(rename = "list.background")] pub list: Option, /// Background color for active ListItem. #[serde(rename = "list.active.background")] pub list_active: Option, /// Border color for active ListItem. #[serde(rename = "list.active.border")] pub list_active_border: Option, /// Stripe background color for even ListItem. #[serde(rename = "list.even.background")] pub list_even: Option, /// Background color for List header. #[serde(rename = "list.head.background")] pub list_head: Option, /// Hover background color for ListItem. #[serde(rename = "list.hover.background")] pub list_hover: Option, /// Muted backgrounds such as Skeleton and Switch. #[serde(rename = "muted.background")] pub muted: Option, /// Muted text color, as used in disabled text. #[serde(rename = "muted.foreground")] pub muted_foreground: Option, /// Background color for Popover. #[serde(rename = "popover.background")] pub popover: Option, /// Text color for Popover. #[serde(rename = "popover.foreground")] pub popover_foreground: Option, /// Primary background color. #[serde(rename = "primary.background")] pub primary: Option, /// Active primary background color. #[serde(rename = "primary.active.background")] pub primary_active: Option, /// Primary text color. #[serde(rename = "primary.foreground")] pub primary_foreground: Option, /// Hover primary background color. #[serde(rename = "primary.hover.background")] pub primary_hover: Option, /// Progress bar background color. #[serde(rename = "progress.bar.background")] pub progress_bar: Option, /// Used for focus ring. #[serde(rename = "ring")] pub ring: Option, /// Scrollbar background color. #[serde(rename = "scrollbar.background")] pub scrollbar: Option, /// Scrollbar thumb background color. #[serde(rename = "scrollbar.thumb.background")] pub scrollbar_thumb: Option, /// Scrollbar thumb hover background color. #[serde(rename = "scrollbar.thumb.hover.background")] pub scrollbar_thumb_hover: Option, /// Secondary background color. #[serde(rename = "secondary.background")] pub secondary: Option, /// Active secondary background color. #[serde(rename = "secondary.active.background")] pub secondary_active: Option, /// Secondary text color, used for secondary Button text color or secondary text. #[serde(rename = "secondary.foreground")] pub secondary_foreground: Option, /// Hover secondary background color. #[serde(rename = "secondary.hover.background")] pub secondary_hover: Option, /// Input selection background color. #[serde(rename = "selection.background")] pub selection: Option, /// Sidebar background color. #[serde(rename = "sidebar.background")] pub sidebar: Option, /// Sidebar accent background color. #[serde(rename = "sidebar.accent.background")] pub sidebar_accent: Option, /// Sidebar accent text color. #[serde(rename = "sidebar.accent.foreground")] pub sidebar_accent_foreground: Option, /// Sidebar border color. #[serde(rename = "sidebar.border")] pub sidebar_border: Option, /// Sidebar text color. #[serde(rename = "sidebar.foreground")] pub sidebar_foreground: Option, /// Sidebar primary background color. #[serde(rename = "sidebar.primary.background")] pub sidebar_primary: Option, /// Sidebar primary text color. #[serde(rename = "sidebar.primary.foreground")] pub sidebar_primary_foreground: Option, /// Skeleton background color. #[serde(rename = "skeleton.background")] pub skeleton: Option, /// Slider bar background color. #[serde(rename = "slider.background")] pub slider_bar: Option, /// Slider thumb background color. #[serde(rename = "slider.thumb.background")] pub slider_thumb: Option, /// Success background color. #[serde(rename = "success.background")] pub success: Option, /// Success text color. #[serde(rename = "success.foreground")] pub success_foreground: Option, /// Success hover background color. #[serde(rename = "success.hover.background")] pub success_hover: Option, /// Success active background color. #[serde(rename = "success.active.background")] pub success_active: Option, /// Switch background color. #[serde(rename = "switch.background")] pub switch: Option, /// Switch thumb background color. #[serde(rename = "switch.thumb.background")] pub switch_thumb: Option, /// Tab background color. #[serde(rename = "tab.background")] pub tab: Option, /// Tab active background color. #[serde(rename = "tab.active.background")] pub tab_active: Option, /// Tab active text color. #[serde(rename = "tab.active.foreground")] pub tab_active_foreground: Option, /// TabBar background color. #[serde(rename = "tab_bar.background")] pub tab_bar: Option, /// TabBar segmented background color. #[serde(rename = "tab_bar.segmented.background")] pub tab_bar_segmented: Option, /// Tab text color. #[serde(rename = "tab.foreground")] pub tab_foreground: Option, /// Table background color. #[serde(rename = "table.background")] pub table: Option, /// Table active item background color. #[serde(rename = "table.active.background")] pub table_active: Option, /// Table active item border color. #[serde(rename = "table.active.border")] pub table_active_border: Option, /// Stripe background color for even TableRow. #[serde(rename = "table.even.background")] pub table_even: Option, /// Table header background color. #[serde(rename = "table.head.background")] pub table_head: Option, /// Table header text color. #[serde(rename = "table.head.foreground")] pub table_head_foreground: Option, /// Table footer background color. #[serde(rename = "table.foot.background")] pub table_foot: Option, /// Table footer text color. #[serde(rename = "table.foot.foreground")] pub table_foot_foreground: Option, /// Table item hover background color. #[serde(rename = "table.hover.background")] pub table_hover: Option, /// Table row border color. #[serde(rename = "table.row.border")] pub table_row_border: Option, /// TitleBar background color, use for Window title bar. #[serde(rename = "title_bar.background")] pub title_bar: Option, /// TitleBar border color. #[serde(rename = "title_bar.border")] pub title_bar_border: Option, /// Background color for Tiles. #[serde(rename = "tiles.background")] pub tiles: Option, /// Warning background color. #[serde(rename = "warning.background")] pub warning: Option, /// Warning active background color. #[serde(rename = "warning.active.background")] pub warning_active: Option, /// Warning hover background color. #[serde(rename = "warning.hover.background")] pub warning_hover: Option, /// Warning foreground color. #[serde(rename = "warning.foreground")] pub warning_foreground: Option, /// Overlay background color. #[serde(rename = "overlay")] pub overlay: Option, /// Window border color. /// /// # Platform specific: /// /// This is only works on Linux, other platforms we can't change the window border color. #[serde(rename = "window.border")] pub window_border: Option, /// Base blue color. #[serde(rename = "base.blue")] blue: Option, /// Base light blue color. #[serde(rename = "base.blue.light")] blue_light: Option, /// Base cyan color. #[serde(rename = "base.cyan")] cyan: Option, /// Base light cyan color. #[serde(rename = "base.cyan.light")] cyan_light: Option, /// Base green color. #[serde(rename = "base.green")] green: Option, /// Base light green color. #[serde(rename = "base.green.light")] green_light: Option, /// Base magenta color. #[serde(rename = "base.magenta")] magenta: Option, #[serde(rename = "base.magenta.light")] magenta_light: Option, /// Base red color. #[serde(rename = "base.red")] red: Option, /// Base light red color. #[serde(rename = "base.red.light")] red_light: Option, /// Base yellow color. #[serde(rename = "base.yellow")] yellow: Option, /// Base light yellow color. #[serde(rename = "base.yellow.light")] yellow_light: Option, } impl ThemeColor { /// Create a new `ThemeColor` from a `ThemeConfig`. pub(crate) fn apply_config(&mut self, config: &ThemeConfig, default_theme: &ThemeColor) { let colors = config.colors.clone(); macro_rules! apply_color { ($config_field:ident) => { if let Some(value) = colors.$config_field { if let Ok(color) = try_parse_color(&value) { self.$config_field = color; } else { self.$config_field = default_theme.$config_field; } } else { self.$config_field = default_theme.$config_field; } }; // With fallback ($config_field:ident, fallback = $fallback:expr) => { if let Some(value) = colors.$config_field { if let Ok(color) = try_parse_color(&value) { self.$config_field = color; } } else { self.$config_field = $fallback; } }; } apply_color!(background); // Base colors for fallback apply_color!(red); apply_color!( red_light, fallback = self.background.blend(self.red.opacity(0.8)) ); apply_color!(green); apply_color!( green_light, fallback = self.background.blend(self.green.opacity(0.8)) ); apply_color!(blue); apply_color!( blue_light, fallback = self.background.blend(self.blue.opacity(0.8)) ); apply_color!(magenta); apply_color!( magenta_light, fallback = self.background.blend(self.magenta.opacity(0.8)) ); apply_color!(yellow); apply_color!( yellow_light, fallback = self.background.blend(self.yellow.opacity(0.8)) ); apply_color!(cyan); apply_color!( cyan_light, fallback = self.background.blend(self.cyan.opacity(0.8)) ); apply_color!(border); apply_color!(foreground); apply_color!(muted); apply_color!( muted_foreground, fallback = self.muted.blend(self.foreground.opacity(0.7)) ); // Button colors let active_darken = if config.mode.is_dark() { 0.2 } else { 0.1 }; let hover_opacity = 0.9; apply_color!(primary); apply_color!(primary_foreground, fallback = self.foreground); apply_color!( primary_hover, fallback = self.background.blend(self.primary.opacity(hover_opacity)) ); apply_color!( primary_active, fallback = self.primary.darken(active_darken) ); apply_color!(button_primary, fallback = self.primary); apply_color!( button_primary_foreground, fallback = self.primary_foreground ); apply_color!(button_primary_hover, fallback = self.primary_hover); apply_color!(button_primary_active, fallback = self.primary_active); apply_color!(secondary); apply_color!(secondary_foreground, fallback = self.foreground); apply_color!( secondary_hover, fallback = self.background.blend(self.secondary.opacity(hover_opacity)) ); apply_color!( secondary_active, fallback = self.secondary.darken(active_darken) ); apply_color!(success, fallback = self.green); apply_color!(success_foreground, fallback = self.primary_foreground); apply_color!( success_hover, fallback = self.background.blend(self.success.opacity(hover_opacity)) ); apply_color!( success_active, fallback = self.success.darken(active_darken) ); apply_color!(info, fallback = self.cyan); apply_color!(info_foreground, fallback = self.primary_foreground); apply_color!( info_hover, fallback = self.background.blend(self.info.opacity(hover_opacity)) ); apply_color!(info_active, fallback = self.info.darken(active_darken)); apply_color!(warning, fallback = self.yellow); apply_color!(warning_foreground, fallback = self.primary_foreground); apply_color!( warning_hover, fallback = self.background.blend(self.warning.opacity(0.9)) ); apply_color!( warning_active, fallback = self.background.blend(self.warning.darken(active_darken)) ); // Other colors apply_color!(accent, fallback = self.secondary); apply_color!(accent_foreground, fallback = self.foreground); apply_color!(accordion, fallback = self.background); apply_color!(accordion_hover, fallback = self.accent.opacity(0.8)); apply_color!( group_box, fallback = self .background .blend( self.secondary .opacity(if config.mode.is_dark() { 0.3 } else { 0.4 }) ) ); apply_color!(group_box_foreground, fallback = self.foreground); apply_color!(caret, fallback = self.primary); apply_color!(chart_1, fallback = self.blue.lighten(0.4)); apply_color!(chart_2, fallback = self.blue.lighten(0.2)); apply_color!(chart_3, fallback = self.blue); apply_color!(chart_4, fallback = self.blue.darken(0.2)); apply_color!(chart_5, fallback = self.blue.darken(0.4)); apply_color!(chart_bullish, fallback = self.green); apply_color!(chart_bearish, fallback = self.red); apply_color!(danger, fallback = self.red); apply_color!(danger_active, fallback = self.danger.darken(active_darken)); apply_color!(danger_foreground, fallback = self.primary_foreground); apply_color!( danger_hover, fallback = self.background.blend(self.danger.opacity(0.9)) ); apply_color!( description_list_label, fallback = self.background.blend(self.border.opacity(0.2)) ); apply_color!( description_list_label_foreground, fallback = self.muted_foreground ); apply_color!(drag_border, fallback = self.primary.opacity(0.65)); apply_color!(drop_target, fallback = self.primary.opacity(0.2)); apply_color!(input, fallback = self.border); apply_color!(link, fallback = self.primary); apply_color!(link_active, fallback = self.link); apply_color!(link_hover, fallback = self.link); apply_color!(list, fallback = self.background); apply_color!( list_active, fallback = self.background.blend(self.primary.opacity(0.1)) ); apply_color!( list_active_border, fallback = self.background.blend(self.primary.opacity(0.6)) ); apply_color!(list_even, fallback = self.list); apply_color!(list_head, fallback = self.list); apply_color!(list_hover, fallback = self.accent.opacity(0.6)); apply_color!(popover, fallback = self.background); apply_color!(popover_foreground, fallback = self.foreground); apply_color!(progress_bar, fallback = self.primary); apply_color!(ring, fallback = self.blue); apply_color!(scrollbar, fallback = self.background); apply_color!(scrollbar_thumb, fallback = self.accent); apply_color!(scrollbar_thumb_hover, fallback = self.scrollbar_thumb); apply_color!(selection, fallback = self.primary); apply_color!( sidebar, fallback = self.background.blend(self.border.opacity(0.15)) ); apply_color!(sidebar_accent, fallback = self.accent); apply_color!(sidebar_accent_foreground, fallback = self.accent_foreground); apply_color!(sidebar_border, fallback = self.border); apply_color!(sidebar_foreground, fallback = self.foreground); apply_color!(sidebar_primary, fallback = self.primary); apply_color!( sidebar_primary_foreground, fallback = self.primary_foreground ); apply_color!(skeleton, fallback = self.secondary); apply_color!(slider_bar, fallback = self.primary); apply_color!(slider_thumb, fallback = self.primary_foreground); apply_color!(switch, fallback = self.secondary_active); apply_color!(switch_thumb, fallback = self.background); apply_color!(tab, fallback = self.background); apply_color!(tab_active, fallback = self.background); apply_color!(tab_active_foreground, fallback = self.foreground); apply_color!(tab_bar, fallback = self.background); apply_color!(tab_bar_segmented, fallback = self.secondary); apply_color!(tab_foreground, fallback = self.foreground); apply_color!(table, fallback = self.list); apply_color!(table_active, fallback = self.list_active); apply_color!(table_active_border, fallback = self.list_active_border); apply_color!(table_even, fallback = self.list_even); apply_color!(table_head, fallback = self.list_head); apply_color!(table_head_foreground, fallback = self.muted_foreground); apply_color!(table_foot, fallback = self.list_head); apply_color!(table_foot_foreground, fallback = self.muted_foreground); apply_color!(table_hover, fallback = self.list_hover); apply_color!(table_row_border, fallback = self.border); apply_color!(title_bar, fallback = self.background); apply_color!(title_bar_border, fallback = self.border); apply_color!(tiles, fallback = self.background); apply_color!(overlay); apply_color!(window_border, fallback = self.border); // TODO: Apply default fallback colors to highlight. // Ensure opacity for list_active, table_active self.list_active = self.list_active.alpha(self.list_active.a.min(0.2)); self.table_active = self.table_active.alpha(self.table_active.a.min(0.2)); self.selection = self.selection.alpha(self.selection.a.min(0.3)); } } impl Theme { /// Apply the given theme configuration to the current theme. pub fn apply_config(&mut self, config: &Rc) { if config.mode.is_dark() { self.dark_theme = config.clone(); } else { self.light_theme = config.clone(); } if let Some(style) = &config.highlight { let highlight_theme = Arc::new(HighlightTheme { name: config.name.to_string(), appearance: config.mode, style: style.clone(), }); self.highlight_theme = highlight_theme.clone(); } let default_theme = if config.mode.is_dark() { Self::from(ThemeColor::dark().as_ref()) } else { Self::from(ThemeColor::light().as_ref()) }; if let Some(font_size) = config.font_size { self.font_size = px(font_size); } else { self.font_size = default_theme.font_size; } if let Some(font_family) = &config.font_family { self.font_family = font_family.clone(); } else { self.font_family = default_theme.font_family.clone(); } if let Some(mono_font_family) = &config.mono_font_family { self.mono_font_family = mono_font_family.clone(); } else { self.mono_font_family = default_theme.mono_font_family.clone(); } if let Some(mono_font_size) = config.mono_font_size { self.mono_font_size = px(mono_font_size); } else { self.mono_font_size = default_theme.mono_font_size; } if let Some(radius) = config.radius { self.radius = px(radius as f32); } else { self.radius = default_theme.radius; } if let Some(radius_lg) = config.radius_lg { self.radius_lg = px(radius_lg as f32); } else { self.radius_lg = default_theme.radius_lg; } if let Some(shadow) = config.shadow { self.shadow = shadow; } else { self.shadow = default_theme.shadow; } self.colors.apply_config(&config, &default_theme.colors); self.mode = config.mode; } } ================================================ FILE: crates/ui/src/theme/theme_color.rs ================================================ use std::sync::Arc; use crate::{ThemeMode, theme::DEFAULT_THEME_COLORS}; use gpui::Hsla; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// Theme colors used throughout the UI components. #[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema)] pub struct ThemeColor { /// Used for accents such as hover background on MenuItem, ListItem, etc. pub accent: Hsla, /// Used for accent text color. pub accent_foreground: Hsla, /// Accordion background color. pub accordion: Hsla, /// Accordion hover background color. pub accordion_hover: Hsla, /// Default background color. pub background: Hsla, /// Default border color pub border: Hsla, /// Button primary background color, fallback to `primary`. pub button_primary: Hsla, /// Button primary active background color, fallback to `primary_active`. pub button_primary_active: Hsla, /// Button primary text color, fallback to `primary_foreground`. pub button_primary_foreground: Hsla, /// Button primary hover background color, fallback to `primary_hover`. pub button_primary_hover: Hsla, /// Background color for GroupBox. pub group_box: Hsla, /// Text color for GroupBox. pub group_box_foreground: Hsla, /// Input caret color (Blinking cursor). pub caret: Hsla, /// Chart 1 color. pub chart_1: Hsla, /// Chart 2 color. pub chart_2: Hsla, /// Chart 3 color. pub chart_3: Hsla, /// Chart 4 color. pub chart_4: Hsla, /// Chart 5 color. pub chart_5: Hsla, /// Bullish color for candlestick charts (upward price movement). pub chart_bullish: Hsla, /// Bearish color for candlestick charts (downward price movement). pub chart_bearish: Hsla, /// Danger background color. pub danger: Hsla, /// Danger active background color. pub danger_active: Hsla, /// Danger text color. pub danger_foreground: Hsla, /// Danger hover background color. pub danger_hover: Hsla, /// Description List label background color. pub description_list_label: Hsla, /// Description List label foreground color. pub description_list_label_foreground: Hsla, /// Drag border color. pub drag_border: Hsla, /// Drop target background color. pub drop_target: Hsla, /// Default text color. pub foreground: Hsla, /// Info background color. pub info: Hsla, /// Info active background color. pub info_active: Hsla, /// Info text color. pub info_foreground: Hsla, /// Info hover background color. pub info_hover: Hsla, /// Border color for inputs such as Input, Select, etc. pub input: Hsla, /// Link text color. pub link: Hsla, /// Active link text color. pub link_active: Hsla, /// Hover link text color. pub link_hover: Hsla, /// Background color for List and ListItem. pub list: Hsla, /// Background color for active ListItem. pub list_active: Hsla, /// Border color for active ListItem. pub list_active_border: Hsla, /// Stripe background color for even ListItem. pub list_even: Hsla, /// Background color for List header. pub list_head: Hsla, /// Hover background color for ListItem. pub list_hover: Hsla, /// Muted backgrounds such as Skeleton and Switch. pub muted: Hsla, /// Muted text color, as used in disabled text. pub muted_foreground: Hsla, /// Background color for Popover. pub popover: Hsla, /// Text color for Popover. pub popover_foreground: Hsla, /// Primary background color. pub primary: Hsla, /// Active primary background color. pub primary_active: Hsla, /// Primary text color. pub primary_foreground: Hsla, /// Hover primary background color. pub primary_hover: Hsla, /// Progress bar background color. pub progress_bar: Hsla, /// Used for focus ring. pub ring: Hsla, /// Scrollbar background color. pub scrollbar: Hsla, /// Scrollbar thumb background color. pub scrollbar_thumb: Hsla, /// Scrollbar thumb hover background color. pub scrollbar_thumb_hover: Hsla, /// Secondary background color. pub secondary: Hsla, /// Active secondary background color. pub secondary_active: Hsla, /// Secondary text color, used for secondary Button text color or secondary text. pub secondary_foreground: Hsla, /// Hover secondary background color. pub secondary_hover: Hsla, /// Input selection background color. pub selection: Hsla, /// Sidebar background color. pub sidebar: Hsla, /// Sidebar accent background color. pub sidebar_accent: Hsla, /// Sidebar accent text color. pub sidebar_accent_foreground: Hsla, /// Sidebar border color. pub sidebar_border: Hsla, /// Sidebar text color. pub sidebar_foreground: Hsla, /// Sidebar primary background color. pub sidebar_primary: Hsla, /// Sidebar primary text color. pub sidebar_primary_foreground: Hsla, /// Skeleton background color. pub skeleton: Hsla, /// Slider bar background color. pub slider_bar: Hsla, /// Slider thumb background color. pub slider_thumb: Hsla, /// Success background color. pub success: Hsla, /// Success text color. pub success_foreground: Hsla, /// Success hover background color. pub success_hover: Hsla, /// Success active background color. pub success_active: Hsla, /// Switch background color. pub switch: Hsla, /// Switch thumb background color. pub switch_thumb: Hsla, /// Tab background color. pub tab: Hsla, /// Tab active background color. pub tab_active: Hsla, /// Tab active text color. pub tab_active_foreground: Hsla, /// TabBar background color. pub tab_bar: Hsla, /// TabBar segmented background color. pub tab_bar_segmented: Hsla, /// Tab text color. pub tab_foreground: Hsla, /// Table background color. pub table: Hsla, /// Table active item background color. pub table_active: Hsla, /// Table active item border color. pub table_active_border: Hsla, /// Stripe background color for even TableRow. pub table_even: Hsla, /// Table head background color. pub table_head: Hsla, /// Table head text color. pub table_head_foreground: Hsla, /// Table footer background color. pub table_foot: Hsla, /// Table footer text color. pub table_foot_foreground: Hsla, /// Table item hover background color. pub table_hover: Hsla, /// Table row border color. pub table_row_border: Hsla, /// TitleBar background color, use for Window title bar. pub title_bar: Hsla, /// TitleBar border color. pub title_bar_border: Hsla, /// Background color for Tiles. pub tiles: Hsla, /// Warning background color. pub warning: Hsla, /// Warning active background color. pub warning_active: Hsla, /// Warning hover background color. pub warning_hover: Hsla, /// Warning foreground color. pub warning_foreground: Hsla, /// Overlay background color. pub overlay: Hsla, /// Window border color. /// /// # Platform specific: /// /// This is only works on Linux, other platforms we can't change the window border color. pub window_border: Hsla, /// The base red color. pub red: Hsla, /// The base red light color. pub red_light: Hsla, /// The base green color. pub green: Hsla, /// The base green light color. pub green_light: Hsla, /// The base blue color. pub blue: Hsla, /// The base blue light color. pub blue_light: Hsla, /// The base yellow color. pub yellow: Hsla, /// The base yellow light color. pub yellow_light: Hsla, /// The base magenta color. pub magenta: Hsla, /// The base magenta light color. pub magenta_light: Hsla, /// The base cyan color. pub cyan: Hsla, /// The base cyan light color. pub cyan_light: Hsla, } impl ThemeColor { /// Get the default light theme colors. pub fn light() -> Arc { DEFAULT_THEME_COLORS[&ThemeMode::Light].0.clone() } /// Get the default dark theme colors. pub fn dark() -> Arc { DEFAULT_THEME_COLORS[&ThemeMode::Dark].0.clone() } } ================================================ FILE: crates/ui/src/time/calendar.rs ================================================ use std::{borrow::Cow, rc::Rc}; use chrono::{Datelike, Local, NaiveDate}; use gpui::{ App, ClickEvent, Context, Div, ElementId, Empty, Entity, EventEmitter, FocusHandle, InteractiveElement, IntoElement, ParentElement, Render, RenderOnce, SharedString, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window, prelude::FluentBuilder as _, px, relative, }; use rust_i18n::t; use crate::{ ActiveTheme, Disableable as _, IconName, Selectable, Sizable, Size, StyledExt as _, button::{Button, ButtonVariants as _}, h_flex, v_flex, }; use super::utils::days_in_month; /// Events emitted by the calendar. pub enum CalendarEvent { /// The user selected a date. Selected(Date), } /// The date of the calendar. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Date { Single(Option), Range(Option, Option), } impl std::fmt::Display for Date { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Single(Some(date)) => write!(f, "{}", date), Self::Single(None) => write!(f, "nil"), Self::Range(Some(start), Some(end)) => write!(f, "{} - {}", start, end), Self::Range(None, None) => write!(f, "nil"), Self::Range(Some(start), None) => write!(f, "{} - nil", start), Self::Range(None, Some(end)) => write!(f, "nil - {}", end), } } } impl From for Date { fn from(date: NaiveDate) -> Self { Self::Single(Some(date)) } } impl From<(NaiveDate, NaiveDate)> for Date { fn from((start, end): (NaiveDate, NaiveDate)) -> Self { Self::Range(Some(start), Some(end)) } } impl Date { /// Check if the date is set. pub fn is_some(&self) -> bool { match self { Self::Single(Some(_)) | Self::Range(Some(_), _) => true, _ => false, } } /// Check if the date is complete. pub fn is_complete(&self) -> bool { match self { Self::Range(Some(_), Some(_)) => true, Self::Single(Some(_)) => true, _ => false, } } /// Get the start date. pub fn start(&self) -> Option { match self { Self::Single(Some(date)) => Some(*date), Self::Range(Some(start), _) => Some(*start), _ => None, } } /// Get the end date. pub fn end(&self) -> Option { match self { Self::Range(_, Some(end)) => Some(*end), _ => None, } } /// Return formatted date string. pub fn format(&self, format: &str) -> Option { match self { Self::Single(Some(date)) => Some(date.format(format).to_string().into()), Self::Range(Some(start), Some(end)) => { Some(format!("{} - {}", start.format(format), end.format(format)).into()) } _ => None, } } fn is_active(&self, v: &NaiveDate) -> bool { let v = *v; match self { Self::Single(d) => Some(v) == *d, Self::Range(start, end) => Some(v) == *start || Some(v) == *end, } } fn is_single(&self) -> bool { matches!(self, Self::Single(_)) } fn is_in_range(&self, v: &NaiveDate) -> bool { let v = *v; match self { Self::Range(start, end) => { if let Some(start) = start { if let Some(end) = end { v >= *start && v <= *end } else { false } } else { false } } _ => false, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ViewMode { Day, Month, Year, } impl ViewMode { fn is_day(&self) -> bool { matches!(self, Self::Day) } fn is_month(&self) -> bool { matches!(self, Self::Month) } fn is_year(&self) -> bool { matches!(self, Self::Year) } } /// Matcher to match dates before and after the interval. pub struct IntervalMatcher { before: Option, after: Option, } /// Matcher to match dates within the range. pub struct RangeMatcher { from: Option, to: Option, } /// Matcher to match dates. pub enum Matcher { /// Match declare days of the week. /// /// Matcher::DayOfWeek(vec![0, 6]) /// Will match the days of the week that are Sunday and Saturday. DayOfWeek(Vec), /// Match the included days, except for those before and after the interval. /// /// Matcher::Interval(IntervalMatcher { /// before: Some(NaiveDate::from_ymd(2020, 1, 2)), /// after: Some(NaiveDate::from_ymd(2020, 1, 3)), /// }) /// Will match the days that are not between 2020-01-02 and 2020-01-03. Interval(IntervalMatcher), /// Match the days within the range. /// /// Matcher::Range(RangeMatcher { /// from: Some(NaiveDate::from_ymd(2020, 1, 1)), /// to: Some(NaiveDate::from_ymd(2020, 1, 3)), /// }) /// Will match the days that are between 2020-01-01 and 2020-01-03. Range(RangeMatcher), /// Match dates using a custom function. /// /// let matcher = Matcher::Custom(Box::new(|date: &NaiveDate| { /// date.day0() < 5 /// })); /// Will match first 5 days of each month Custom(Box bool + Send + Sync>), } impl From> for Matcher { fn from(days: Vec) -> Self { Matcher::DayOfWeek(days) } } impl From for Matcher where F: Fn(&NaiveDate) -> bool + Send + Sync + 'static, { fn from(f: F) -> Self { Matcher::Custom(Box::new(f)) } } impl Matcher { /// Create a new interval matcher. pub fn interval(before: Option, after: Option) -> Self { Matcher::Interval(IntervalMatcher { before, after }) } /// Create a new range matcher. pub fn range(from: Option, to: Option) -> Self { Matcher::Range(RangeMatcher { from, to }) } /// Create a new custom matcher. pub fn custom(f: F) -> Self where F: Fn(&NaiveDate) -> bool + Send + Sync + 'static, { Matcher::Custom(Box::new(f)) } /// Check if the date matches the matcher. pub fn is_match(&self, date: &Date) -> bool { match date { Date::Single(Some(date)) => self.matched(date), Date::Range(Some(start), Some(end)) => self.matched(start) || self.matched(end), _ => false, } } fn matched(&self, date: &NaiveDate) -> bool { match self { Matcher::DayOfWeek(days) => days.contains(&date.weekday().num_days_from_sunday()), Matcher::Interval(interval) => { let before_check = interval.before.map_or(false, |before| date < &before); let after_check = interval.after.map_or(false, |after| date > &after); before_check || after_check } Matcher::Range(range) => { let from_check = range.from.map_or(false, |from| date < &from); let to_check = range.to.map_or(false, |to| date > &to); !from_check && !to_check } Matcher::Custom(f) => f(date), } } } #[derive(IntoElement)] pub struct Calendar { id: ElementId, size: Size, state: Entity, style: StyleRefinement, /// Number of the months view to show. number_of_months: usize, } /// Use to store the state of the calendar. pub struct CalendarState { focus_handle: FocusHandle, view_mode: ViewMode, date: Date, current_year: i32, current_month: u8, years: Vec>, year_page: i32, today: NaiveDate, /// Number of the months view to show. number_of_months: usize, pub(crate) disabled_matcher: Option>, } impl CalendarState { /// Create a new calendar state. pub fn new(_: &mut Window, cx: &mut Context) -> Self { let today = Local::now().naive_local().date(); Self { focus_handle: cx.focus_handle(), view_mode: ViewMode::Day, date: Date::Single(None), current_month: today.month() as u8, current_year: today.year(), years: vec![], year_page: 0, today, number_of_months: 1, disabled_matcher: None, } .year_range((today.year() - 50, today.year() + 50)) } /// Set the disabled matcher of the calendar state. pub fn disabled_matcher(mut self, matcher: impl Into) -> Self { self.disabled_matcher = Some(Rc::new(matcher.into())); self } /// Set the disabled matcher of the calendar. /// /// The disabled matcher will be used to disable the days that match the matcher. pub fn set_disabled_matcher( &mut self, disabled: impl Into, _: &mut Window, _: &mut Context, ) { self.disabled_matcher = Some(Rc::new(disabled.into())); } /// Set the date of the calendar. /// /// When you set a range date, the mode will be automatically set to `Mode::Range`. pub fn set_date(&mut self, date: impl Into, _: &mut Window, cx: &mut Context) { let date = date.into(); let invalid = self .disabled_matcher .as_ref() .map_or(false, |matcher| matcher.is_match(&date)); if invalid { return; } self.date = date; match self.date { Date::Single(Some(date)) => { self.current_month = date.month() as u8; self.current_year = date.year(); } Date::Range(Some(start), _) => { self.current_month = start.month() as u8; self.current_year = start.year(); } _ => {} } cx.notify() } /// Get the date of the calendar. pub fn date(&self) -> Date { self.date } /// Set number of months to show. pub fn set_number_of_months( &mut self, number_of_months: usize, _: &mut Window, cx: &mut Context, ) { self.number_of_months = number_of_months; cx.notify(); } /// Set the year range of the calendar, default is 50 years before and after the current year. /// /// Each year page contains 20 years, so the range will be divided into chunks of 20 years is better. pub fn year_range(mut self, range: (i32, i32)) -> Self { self.years = (range.0..range.1) .collect::>() .chunks(20) .map(|chunk| chunk.to_vec()) .collect::>(); self.year_page = self .years .iter() .position(|years| years.contains(&self.current_year)) .unwrap_or(0) as i32; self } /// Get year and month by offset month. fn offset_year_month(&self, offset_month: usize) -> (i32, u32) { let mut month = self.current_month as i32 + offset_month as i32; let mut year = self.current_year; while month < 1 { month += 12; year -= 1; } while month > 12 { month -= 12; year += 1; } (year, month as u32) } /// Returns the days of the month in a 2D vector to render on calendar. fn days(&self) -> Vec> { (0..self.number_of_months) .flat_map(|offset| { days_in_month(self.current_year, self.current_month as u32 + offset as u32) }) .collect() } fn has_prev_year_page(&self) -> bool { self.year_page > 0 } fn has_next_year_page(&self) -> bool { self.year_page < self.years.len() as i32 - 1 } fn prev_year_page(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { if !self.has_prev_year_page() { return; } self.year_page -= 1; cx.notify() } fn next_year_page(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { if !self.has_next_year_page() { return; } self.year_page += 1; cx.notify() } fn prev_month(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { self.current_month = if self.current_month == 1 { 12 } else { self.current_month - 1 }; self.current_year = if self.current_month == 12 { self.current_year - 1 } else { self.current_year }; cx.notify() } fn next_month(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { self.current_month = if self.current_month == 12 { 1 } else { self.current_month + 1 }; self.current_year = if self.current_month == 1 { self.current_year + 1 } else { self.current_year }; cx.notify() } fn month_name(&self, offset_month: usize) -> SharedString { let (_, month) = self.offset_year_month(offset_month); match month { 1 => t!("Calendar.month.January"), 2 => t!("Calendar.month.February"), 3 => t!("Calendar.month.March"), 4 => t!("Calendar.month.April"), 5 => t!("Calendar.month.May"), 6 => t!("Calendar.month.June"), 7 => t!("Calendar.month.July"), 8 => t!("Calendar.month.August"), 9 => t!("Calendar.month.September"), 10 => t!("Calendar.month.October"), 11 => t!("Calendar.month.November"), 12 => t!("Calendar.month.December"), _ => Cow::Borrowed(""), } .into() } fn year_name(&self, offset_month: usize) -> SharedString { let (year, _) = self.offset_year_month(offset_month); year.to_string().into() } fn set_view_mode(&mut self, mode: ViewMode, _: &mut Window, cx: &mut Context) { self.view_mode = mode; cx.notify(); } fn months(&self) -> Vec { [ t!("Calendar.month.January"), t!("Calendar.month.February"), t!("Calendar.month.March"), t!("Calendar.month.April"), t!("Calendar.month.May"), t!("Calendar.month.June"), t!("Calendar.month.July"), t!("Calendar.month.August"), t!("Calendar.month.September"), t!("Calendar.month.October"), t!("Calendar.month.November"), t!("Calendar.month.December"), ] .iter() .map(|s| s.clone().into()) .collect() } } impl Render for CalendarState { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { Empty } } impl Calendar { /// Create a new calendar element with [`CalendarState`]. pub fn new(state: &Entity) -> Self { Self { id: ("calendar", state.entity_id()).into(), size: Size::default(), state: state.clone(), style: StyleRefinement::default(), number_of_months: 1, } } /// Set number of months to show, default is 1. pub fn number_of_months(mut self, number_of_months: usize) -> Self { self.number_of_months = number_of_months; self } fn render_day( &self, d: &NaiveDate, offset_month: usize, window: &mut Window, cx: &mut App, ) -> Stateful
{ let state = self.state.read(cx); let (_, month) = state.offset_year_month(offset_month); let day = d.day(); let is_current_month = d.month() == month; let is_active = state.date.is_active(d); let is_in_range = state.date.is_in_range(d); let date = *d; let is_today = *d == state.today; let disabled = state .disabled_matcher .as_ref() .map_or(false, |disabled| disabled.matched(&date)); let date_id: SharedString = format!("{}_{}", date.format("%Y-%m-%d"), offset_month).into(); self.item_button( date_id.clone(), day.to_string(), is_active, is_in_range, !is_current_month || disabled, disabled, window, cx, ) .when(is_today && !is_active, |this| { this.border_1().border_color(cx.theme().border) }) // Add border for today .when(!disabled, |this| { this.on_click(window.listener_for( &self.state, move |view, _: &ClickEvent, window, cx| { if view.date.is_single() { view.set_date(date, window, cx); cx.emit(CalendarEvent::Selected(view.date())); } else { let start = view.date.start(); let end = view.date.end(); if start.is_none() && end.is_none() { view.set_date(Date::Range(Some(date), None), window, cx); } else if start.is_some() && end.is_none() { if date < start.unwrap() { view.set_date(Date::Range(Some(date), None), window, cx); } else { view.set_date( Date::Range(Some(start.unwrap()), Some(date)), window, cx, ); } } else { view.set_date(Date::Range(Some(date), None), window, cx); } if view.date.is_complete() { cx.emit(CalendarEvent::Selected(view.date())); } } }, )) }) } fn render_header(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { let state = self.state.read(cx); let current_year = state.current_year; let view_mode = state.view_mode; let disabled = view_mode.is_month(); let multiple_months = self.number_of_months > 1; let icon_size = match self.size { Size::Small => Size::Small, Size::Large => Size::Medium, _ => Size::Medium, }; h_flex() .gap_0p5() .justify_between() .items_center() .child( Button::new("prev") .icon(IconName::ArrowLeft) .tab_stop(false) .ghost() .disabled(disabled) .with_size(icon_size) .when(view_mode.is_day(), |this| { this.on_click(window.listener_for(&self.state, CalendarState::prev_month)) }) .when(view_mode.is_year(), |this| { this.when(!state.has_prev_year_page(), |this| this.disabled(true)) .on_click( window.listener_for(&self.state, CalendarState::prev_year_page), ) }), ) .when(!multiple_months, |this| { this.child( h_flex() .justify_center() .gap_3() .child( Button::new("month") .ghost() .label(state.month_name(0)) .compact() .tab_stop(false) .with_size(self.size) .selected(view_mode.is_month()) .on_click(window.listener_for( &self.state, move |view, _, window, cx| { if view_mode.is_month() { view.set_view_mode(ViewMode::Day, window, cx); } else { view.set_view_mode(ViewMode::Month, window, cx); } cx.notify(); }, )), ) .child( Button::new("year") .ghost() .label(current_year.to_string()) .compact() .tab_stop(false) .with_size(self.size) .selected(view_mode.is_year()) .on_click(window.listener_for( &self.state, |view, _, window, cx| { if view.view_mode.is_year() { view.set_view_mode(ViewMode::Day, window, cx); } else { view.set_view_mode(ViewMode::Year, window, cx); } cx.notify(); }, )), ), ) }) .when(multiple_months, |this| { this.child(h_flex().flex_1().justify_around().children( (0..self.number_of_months).map(|n| { h_flex() .justify_center() .map(|this| match self.size { Size::Small => this.gap_2(), Size::Large => this.gap_4(), _ => this.gap_3(), }) .child(state.month_name(n)) .child(state.year_name(n)) }), )) }) .child( Button::new("next") .icon(IconName::ArrowRight) .ghost() .tab_stop(false) .disabled(disabled) .with_size(icon_size) .when(view_mode.is_day(), |this| { this.on_click(window.listener_for(&self.state, CalendarState::next_month)) }) .when(view_mode.is_year(), |this| { this.when(!state.has_next_year_page(), |this| this.disabled(true)) .on_click( window.listener_for(&self.state, CalendarState::next_year_page), ) }), ) } #[allow(clippy::too_many_arguments)] fn item_button( &self, id: impl Into, label: impl Into, active: bool, secondary_active: bool, muted: bool, disabled: bool, _: &mut Window, cx: &mut App, ) -> Stateful
{ h_flex() .id(id.into()) .map(|this| match self.size { Size::Small => this.size_7().rounded(cx.theme().radius / 2.), Size::Large => this.size_10().rounded(cx.theme().radius * 2.), _ => this.size_9().rounded(cx.theme().radius), }) .justify_center() .when(muted, |this| { this.text_color(if disabled { cx.theme().muted_foreground.opacity(0.3) } else { cx.theme().muted_foreground }) }) .when(secondary_active, |this| { this.bg(if muted { cx.theme().accent.opacity(0.5) } else { cx.theme().accent }) .text_color(cx.theme().accent_foreground) }) .when(!active && !disabled, |this| { this.hover(|this| { this.bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) }) }) .when(active, |this| { this.bg(cx.theme().primary) .text_color(cx.theme().primary_foreground) }) .child(label.into()) } fn render_days(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { let state = self.state.read(cx); let weeks = [ t!("Calendar.week.0"), t!("Calendar.week.1"), t!("Calendar.week.2"), t!("Calendar.week.3"), t!("Calendar.week.4"), t!("Calendar.week.5"), t!("Calendar.week.6"), ]; h_flex() .map(|this| match self.size { Size::Small => this.gap_3().text_sm(), Size::Large => this.gap_5().text_base(), _ => this.gap_4().text_sm(), }) .justify_between() .children( state .days() .chunks(5) .enumerate() .map(|(offset_month, days)| { v_flex() .gap_0p5() .child( h_flex().gap_0p5().justify_between().children( weeks .iter() .map(|week| self.render_week(week.clone(), window, cx)), ), ) .children(days.iter().map(|week| { h_flex().gap_0p5().justify_between().children( week.iter() .map(|d| self.render_day(d, offset_month, window, cx)), ) })) }), ) } fn render_week(&self, week: impl Into, _: &mut Window, cx: &mut App) -> Div { h_flex() .map(|this| match self.size { Size::Small => this.size_7().rounded(cx.theme().radius / 2.0), Size::Large => this.size_10().rounded(cx.theme().radius), _ => this.size_9().rounded(cx.theme().radius), }) .justify_center() .text_color(cx.theme().muted_foreground) .text_sm() .child(week.into()) } fn render_months(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { let state = self.state.read(cx); let months = state.months(); let current_month = state.current_month; h_flex() .mt_3() .gap_0p5() .gap_y_3() .map(|this| match self.size { Size::Small => this.mt_2().gap_y_2().w(px(208.)), Size::Large => this.mt_4().gap_y_4().w(px(292.)), _ => this.mt_3().gap_y_3().w(px(264.)), }) .justify_between() .flex_wrap() .children( months .iter() .enumerate() .map(|(ix, month)| { let active = (ix + 1) as u8 == current_month; self.item_button( ix, month.to_string(), active, false, false, false, window, cx, ) .w(relative(0.3)) .text_sm() .on_click(window.listener_for( &self.state, move |view, _, window, cx| { view.current_month = (ix + 1) as u8; view.set_view_mode(ViewMode::Day, window, cx); cx.notify(); }, )) }) .collect::>(), ) } fn render_years(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { let state = self.state.read(cx); let current_year = state.current_year; let current_page_years = &self.state.read(cx).years[state.year_page as usize].clone(); h_flex() .id("years") .gap_0p5() .map(|this| match self.size { Size::Small => this.mt_2().gap_y_2().w(px(208.)), Size::Large => this.mt_4().gap_y_4().w(px(292.)), _ => this.mt_3().gap_y_3().w(px(264.)), }) .justify_between() .flex_wrap() .children( current_page_years .iter() .enumerate() .map(|(ix, year)| { let year = *year; let active = year == current_year; self.item_button( ix, year.to_string(), active, false, false, false, window, cx, ) .w(relative(0.2)) .on_click(window.listener_for( &self.state, move |view, _, window, cx| { view.current_year = year; view.set_view_mode(ViewMode::Day, window, cx); cx.notify(); }, )) }) .collect::>(), ) } } impl Sizable for Calendar { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); self } } impl Styled for Calendar { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl EventEmitter for CalendarState {} impl RenderOnce for Calendar { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let view_mode = self.state.read(cx).view_mode; let number_of_months = self.number_of_months; self.state.update(cx, |state, _| { state.number_of_months = number_of_months; }); v_flex() .id(self.id.clone()) .track_focus(&self.state.read(cx).focus_handle) .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius_lg) .p_3() .gap_0p5() .refine_style(&self.style) .child(self.render_header(window, cx)) .child( v_flex() .when(view_mode.is_day(), |this| { this.child(self.render_days(window, cx)) }) .when(view_mode.is_month(), |this| { this.child(self.render_months(window, cx)) }) .when(view_mode.is_year(), |this| { this.child(self.render_years(window, cx)) }), ) } } #[cfg(test)] mod tests { use chrono::NaiveDate; use super::Date; #[test] fn test_date_to_string() { let date = Date::Single(Some(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap())); assert_eq!(date.to_string(), "2024-08-03"); let date = Date::Single(None); assert_eq!(date.to_string(), "nil"); let date = Date::Range( Some(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap()), Some(NaiveDate::from_ymd_opt(2024, 8, 5).unwrap()), ); assert_eq!(date.to_string(), "2024-08-03 - 2024-08-05"); let date = Date::Range(Some(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap()), None); assert_eq!(date.to_string(), "2024-08-03 - nil"); let date = Date::Range(None, Some(NaiveDate::from_ymd_opt(2024, 8, 5).unwrap())); assert_eq!(date.to_string(), "nil - 2024-08-05"); let date = Date::Range(None, None); assert_eq!(date.to_string(), "nil"); } } ================================================ FILE: crates/ui/src/time/date_picker.rs ================================================ use std::rc::Rc; use chrono::NaiveDate; use gpui::{ App, AppContext, ClickEvent, Context, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement as _, Render, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled, Subscription, Window, anchored, deferred, div, prelude::FluentBuilder as _, px, }; use rust_i18n::t; use crate::{ ActiveTheme, Disableable, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt as _, actions::{Cancel, Confirm}, button::{Button, ButtonVariants as _}, h_flex, input::{Delete, clear_button, input_style}, v_flex, }; use super::calendar::{Calendar, CalendarEvent, CalendarState, Date, Matcher}; const CONTEXT: &'static str = "DatePicker"; pub(crate) fn init(cx: &mut App) { cx.bind_keys([ KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)), KeyBinding::new("escape", Cancel, Some(CONTEXT)), KeyBinding::new("delete", Delete, Some(CONTEXT)), KeyBinding::new("backspace", Delete, Some(CONTEXT)), ]) } /// Events emitted by the DatePicker. #[derive(Clone)] pub enum DatePickerEvent { Change(Date), } /// Preset value for DateRangePreset. #[derive(Clone)] pub enum DateRangePresetValue { Single(NaiveDate), Range(NaiveDate, NaiveDate), } /// Preset for date range selection. #[derive(Clone)] pub struct DateRangePreset { label: SharedString, value: DateRangePresetValue, } impl DateRangePreset { /// Creates a new DateRangePreset with a date. pub fn single(label: impl Into, date: NaiveDate) -> Self { DateRangePreset { label: label.into(), value: DateRangePresetValue::Single(date), } } /// Creates a new DateRangePreset with a range of dates. pub fn range(label: impl Into, start: NaiveDate, end: NaiveDate) -> Self { DateRangePreset { label: label.into(), value: DateRangePresetValue::Range(start, end), } } } /// Use to store the state of the date picker. pub struct DatePickerState { focus_handle: FocusHandle, date: Date, open: bool, calendar: Entity, date_format: SharedString, number_of_months: usize, disabled_matcher: Option>, _subscriptions: Vec, } impl Focusable for DatePickerState { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl EventEmitter for DatePickerState {} impl DatePickerState { /// Create a date state. pub fn new(window: &mut Window, cx: &mut Context) -> Self { Self::new_with_range(false, window, cx) } /// Create a date state with range mode. pub fn range(window: &mut Window, cx: &mut Context) -> Self { Self::new_with_range(true, window, cx) } fn new_with_range(is_range: bool, window: &mut Window, cx: &mut Context) -> Self { let date = if is_range { Date::Range(None, None) } else { Date::Single(None) }; let calendar = cx.new(|cx| { let mut this = CalendarState::new(window, cx); this.set_date(date, window, cx); this }); let _subscriptions = vec![cx.subscribe_in( &calendar, window, |this, _, ev: &CalendarEvent, window, cx| match ev { CalendarEvent::Selected(date) => { this.update_date(*date, true, window, cx); this.focus_handle.focus(window, cx); } }, )]; Self { focus_handle: cx.focus_handle(), date, calendar, open: false, date_format: "%Y/%m/%d".into(), number_of_months: 1, disabled_matcher: None, _subscriptions, } } /// Set the date format of the date picker to display in Input, default: "%Y/%m/%d". pub fn date_format(mut self, format: impl Into) -> Self { self.date_format = format.into(); self } /// Set the number of months calendar view to display, default is 1. pub fn number_of_months(mut self, number_of_months: usize) -> Self { self.number_of_months = number_of_months; self } /// Get the date of the date picker. pub fn date(&self) -> Date { self.date } /// Set the date of the date picker. pub fn set_date(&mut self, date: impl Into, window: &mut Window, cx: &mut Context) { self.update_date(date.into(), false, window, cx); } /// Set the disabled match for the calendar. pub fn disabled_matcher(mut self, disabled: impl Into) -> Self { self.disabled_matcher = Some(Rc::new(disabled.into())); self } fn update_date(&mut self, date: Date, emit: bool, window: &mut Window, cx: &mut Context) { self.date = date; self.calendar.update(cx, |view, cx| { view.set_date(date, window, cx); }); self.open = false; if emit { cx.emit(DatePickerEvent::Change(date)); } cx.notify(); } /// Set the disabled matcher of the date picker. fn set_canlendar_disabled_matcher(&mut self, _: &mut Window, cx: &mut Context) { let matcher = self.disabled_matcher.clone(); self.calendar.update(cx, |state, _| { state.disabled_matcher = matcher; }); } fn on_escape(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { if !self.open { cx.propagate(); } self.focus_back_if_need(window, cx); self.open = false; cx.notify(); } fn on_enter(&mut self, _: &Confirm, _: &mut Window, cx: &mut Context) { if !self.open { self.open = true; cx.notify(); } } fn on_delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { self.clean(&ClickEvent::default(), window, cx); } // To focus the Picker Input, if current focus in is on the container. // // This is because mouse down out the Calendar, GPUI will move focus to the container. // So we need to move focus back to the Picker Input. // // But if mouse down target is some other focusable element (e.g.: [`crate::Input`]), we should not move focus. fn focus_back_if_need(&mut self, window: &mut Window, cx: &mut Context) { if !self.open { return; } if let Some(focused) = window.focused(cx) { if focused.contains(&self.focus_handle, window) { self.focus_handle.focus(window, cx); } } } fn clean(&mut self, _: &gpui::ClickEvent, window: &mut Window, cx: &mut Context) { cx.stop_propagation(); match self.date { Date::Single(_) => { self.update_date(Date::Single(None), true, window, cx); } Date::Range(_, _) => { self.update_date(Date::Range(None, None), true, window, cx); } } } fn toggle_calendar(&mut self, _: &gpui::ClickEvent, _: &mut Window, cx: &mut Context) { self.open = !self.open; cx.notify(); } fn select_preset( &mut self, preset: &DateRangePreset, window: &mut Window, cx: &mut Context, ) { match preset.value { DateRangePresetValue::Single(single) => { self.update_date(Date::Single(Some(single)), true, window, cx) } DateRangePresetValue::Range(start, end) => { self.update_date(Date::Range(Some(start), Some(end)), true, window, cx) } } } } /// A DatePicker element. #[derive(IntoElement)] pub struct DatePicker { id: ElementId, style: StyleRefinement, state: Entity, cleanable: bool, placeholder: Option, size: Size, number_of_months: usize, presets: Option>, appearance: bool, disabled: bool, } impl Sizable for DatePicker { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); self } } impl Focusable for DatePicker { fn focus_handle(&self, cx: &App) -> FocusHandle { self.state.focus_handle(cx) } } impl Styled for DatePicker { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl Disableable for DatePicker { fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } } impl Render for DatePickerState { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl gpui::IntoElement { Empty } } impl DatePicker { /// Create a new DatePicker with the given [`DatePickerState`]. pub fn new(state: &Entity) -> Self { Self { id: ("date-picker", state.entity_id()).into(), state: state.clone(), cleanable: false, placeholder: None, size: Size::default(), style: StyleRefinement::default(), number_of_months: 2, presets: None, appearance: true, disabled: false, } } /// Set the placeholder of the date picker, default: "". pub fn placeholder(mut self, placeholder: impl Into) -> Self { self.placeholder = Some(placeholder.into()); self } /// Set whether to show the clear button when the input field is not empty, default is false. pub fn cleanable(mut self, cleanable: bool) -> Self { self.cleanable = cleanable; self } /// Set preset ranges for the date picker. pub fn presets(mut self, presets: Vec) -> Self { self.presets = Some(presets); self } /// Set number of months to display in the calendar, default is 2. pub fn number_of_months(mut self, number_of_months: usize) -> Self { self.number_of_months = number_of_months; self } /// Set appearance of the date picker, if false, the date picker will be in a minimal style. pub fn appearance(mut self, appearance: bool) -> Self { self.appearance = appearance; self } } impl RenderOnce for DatePicker { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { self.state.update(cx, |state, cx| { state.set_canlendar_disabled_matcher(window, cx); }); // This for keep focus border style, when click on the popup. let is_focused = self.focus_handle(cx).contains_focused(window, cx); let state = self.state.read(cx); let show_clean = self.cleanable && state.date.is_some(); let placeholder = self .placeholder .clone() .unwrap_or_else(|| t!("DatePicker.placeholder").into()); let display_title = state .date .format(&state.date_format) .unwrap_or(placeholder.clone()); let (bg, fg) = input_style(self.disabled, cx); div() .id(self.id.clone()) .key_context(CONTEXT) .track_focus(&self.focus_handle(cx).tab_stop(true)) .on_action(window.listener_for(&self.state, DatePickerState::on_enter)) .on_action(window.listener_for(&self.state, DatePickerState::on_delete)) .when(state.open, |this| { this.on_action(window.listener_for(&self.state, DatePickerState::on_escape)) }) .flex_none() .w_full() .relative() .input_text_size(self.size) .refine_style(&self.style) .child( div() .id("date-picker-input") .relative() .flex() .items_center() .justify_between() .when(self.appearance, |this| { this.bg(bg) .text_color(fg) .when(self.disabled, |this| this.opacity(0.5)) .border_1() .border_color(cx.theme().input) .rounded(cx.theme().radius) .when(cx.theme().shadow, |this| this.shadow_xs()) .when(is_focused, |this| this.focused_border(cx)) }) .overflow_hidden() .input_text_size(self.size) .input_size(self.size) .when(!state.open && !self.disabled, |this| { this.on_click( window.listener_for(&self.state, DatePickerState::toggle_calendar), ) }) .child( h_flex() .w_full() .items_center() .justify_between() .gap_1() .child( div() .w_full() .overflow_hidden() .when(!state.date.is_some(), |this| { this.text_color(cx.theme().muted_foreground) }) .child(display_title), ) .when(!self.disabled, |this| { this.when(show_clean, |this| { this.child(clear_button(cx).on_click( window.listener_for(&self.state, DatePickerState::clean), )) }) .when(!show_clean, |this| { this.child( Icon::new(IconName::Calendar) .xsmall() .text_color(cx.theme().muted_foreground), ) }) }), ), ) .when(state.open, |this| { this.child( deferred( anchored().snap_to_window_with_margin(px(8.)).child( div() .occlude() .mt_1p5() .p_3() .border_1() .border_color(cx.theme().border) .shadow_lg() .rounded((cx.theme().radius * 2.).min(px(8.))) .bg(cx.theme().popover) .text_color(cx.theme().popover_foreground) .on_mouse_up_out( MouseButton::Left, window.listener_for(&self.state, |view, _, window, cx| { view.on_escape(&Cancel, window, cx); }), ) .child( h_flex() .gap_3() .h_full() .items_start() .when_some(self.presets.clone(), |this, presets| { this.child( v_flex().my_1().gap_2().justify_end().children( presets.into_iter().enumerate().map( |(i, preset)| { Button::new(("preset", i)) .small() .ghost() .tab_stop(false) .label(preset.label.clone()) .on_click(window.listener_for( &self.state, move |this, _, window, cx| { this.select_preset( &preset, window, cx, ); }, )) }, ), ), ) }) .child( Calendar::new(&state.calendar) .number_of_months(self.number_of_months) .border_0() .rounded_none() .p_0() .with_size(self.size), ), ), ), ) .with_priority(2), ) }) } } ================================================ FILE: crates/ui/src/time/mod.rs ================================================ pub mod calendar; pub mod date_picker; mod utils; ================================================ FILE: crates/ui/src/time/utils.rs ================================================ use chrono::{Datelike, Duration, NaiveDate}; trait NaiveDateExt { fn days_in_month(&self) -> i32; fn is_leap_year(&self) -> bool; } impl NaiveDateExt for chrono::NaiveDate { fn days_in_month(&self) -> i32 { let month = self.month(); match month { 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 => { if self.is_leap_year() { 29 } else { 28 } } _ => panic!("Invalid month: {}", month), } } fn is_leap_year(&self) -> bool { let year = self.year(); return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); } } pub(crate) fn days_in_month(year: i32, month: u32) -> Vec> { let mut year = year; let mut month = month; if month > 12 { year += 1; month = 1; } if month < 1 { year -= 1; month = 12; } let date = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); let num_days = date.days_in_month(); let start_weekday = date.weekday().num_days_from_sunday(); // Get the days in the month, 2023-02 will returns // "29|30|31| 1| 2| 3| 4", // " 5| 6| 7| 8| 9|10|11", // "12|13|14|15|16|17|18", // "19|20|21|22|23|24|25", // "26|27|28| 1| 2| 3| 4", let mut days = vec![]; for n in 0..5 { let mut week_days = vec![]; for weekday in 0..7 { let (mut y, mut m) = (year, month); // If the day is less than the start weekday, we need to go back to the previous month. if n == 0 && weekday < start_weekday { m = if m == 1 { 12 } else { m - 1 }; y = if m == 1 { year - 1 } else { y }; } // If start_weekday is 3, and n is 0 and weekday is 3, then day is 1. // If start_weekday is 3, and n is 1 and weekday is 4, then day is 9. let day = n * 7 + weekday as i32 - start_weekday as i32; // If the day is greater than the number of days in the month, we need to go to the next month. if day > num_days { m = if m == 12 { 1 } else { m + 1 }; y = if m == 1 { year + 1 } else { y }; } #[allow(clippy::expect_fun_call)] let date = date .checked_add_signed(Duration::days(day as i64)) .expect(&format!("invalid date {}-{} days {}", y, m, day)); week_days.push(date); } days.push(week_days); } days } #[cfg(test)] mod tests { use chrono::{Datelike, NaiveDate}; use super::{days_in_month, NaiveDateExt}; #[test] fn test_days_in_month() { assert_eq!( NaiveDate::from_ymd_opt(2024, 2, 1).unwrap().days_in_month(), 29 ); assert_eq!( NaiveDate::from_ymd_opt(2023, 2, 1).unwrap().days_in_month(), 28 ); assert_eq!( NaiveDate::from_ymd_opt(2023, 1, 1).unwrap().days_in_month(), 31 ); assert_eq!( NaiveDate::from_ymd_opt(2023, 4, 1).unwrap().days_in_month(), 30 ); } #[test] fn test_days() { #[track_caller] fn assert_case(date: NaiveDate, expected: Vec<&str>) { let out = days_in_month(date.year(), date.month()) .iter() .map(|week| { week.iter() .map(|d| { if d.year() == date.year() && d.month() == date.month() { format!("{:2}", d.day()) } else if d.year() == date.year() { format!("{}-{}", d.month(), d.day()) } else { format!("{}-{}-{}", d.year(), d.month(), d.day()) } }) .collect::>() .join("|") }) .collect::>(); assert_eq!(out, expected); } assert_case( NaiveDate::from_ymd_opt(2024, 8, 1).unwrap(), vec![ "7-28|7-29|7-30|7-31| 1| 2| 3", " 4| 5| 6| 7| 8| 9|10", "11|12|13|14|15|16|17", "18|19|20|21|22|23|24", "25|26|27|28|29|30|31", ], ); assert_case( NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(), vec![ "2024-12-29|2024-12-30|2024-12-31| 1| 2| 3| 4", " 5| 6| 7| 8| 9|10|11", "12|13|14|15|16|17|18", "19|20|21|22|23|24|25", "26|27|28|29|30|31|2-1", ], ); assert_case( NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(), vec![ "1-28|1-29|1-30|1-31| 1| 2| 3", " 4| 5| 6| 7| 8| 9|10", "11|12|13|14|15|16|17", "18|19|20|21|22|23|24", "25|26|27|28|29|3-1|3-2", ], ); assert_case( NaiveDate::from_ymd_opt(2023, 2, 20).unwrap(), vec![ "1-29|1-30|1-31| 1| 2| 3| 4", " 5| 6| 7| 8| 9|10|11", "12|13|14|15|16|17|18", "19|20|21|22|23|24|25", "26|27|28|3-1|3-2|3-3|3-4", ], ); } } ================================================ FILE: crates/ui/src/title_bar.rs ================================================ use std::rc::Rc; use crate::{ ActiveTheme, Icon, IconName, InteractiveElementExt as _, Sizable as _, StyledExt, h_flex, }; use gpui::{ AnyElement, App, ClickEvent, Context, Decorations, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Render, RenderOnce, StatefulInteractiveElement as _, StyleRefinement, Styled, TitlebarOptions, Window, WindowControlArea, div, prelude::FluentBuilder as _, px, }; use smallvec::SmallVec; pub const TITLE_BAR_HEIGHT: Pixels = px(34.); #[cfg(target_os = "macos")] const TITLE_BAR_LEFT_PADDING: Pixels = px(80.); #[cfg(not(target_os = "macos"))] const TITLE_BAR_LEFT_PADDING: Pixels = px(12.); /// TitleBar used to customize the appearance of the title bar. /// /// We can put some elements inside the title bar. #[derive(IntoElement)] pub struct TitleBar { style: StyleRefinement, children: SmallVec<[AnyElement; 1]>, on_close_window: Option>>, } impl TitleBar { /// Create a new TitleBar. pub fn new() -> Self { Self { style: StyleRefinement::default(), children: SmallVec::new(), on_close_window: None, } } /// Returns the default title bar options for compatible with the [`crate::TitleBar`]. pub fn title_bar_options() -> TitlebarOptions { TitlebarOptions { title: None, appears_transparent: true, traffic_light_position: Some(gpui::point(px(9.0), px(9.0))), } } /// Add custom for close window event, default is None, then click X button will call `window.remove_window()`. /// Linux only, this will do nothing on other platforms. pub fn on_close_window( mut self, f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> Self { if cfg!(target_os = "linux") { self.on_close_window = Some(Rc::new(Box::new(f))); } self } } // The Windows control buttons have a fixed width of 35px. // // We don't need implementation the click event for the control buttons. // If user clicked in the bounds, the window event will be triggered. #[derive(IntoElement, Clone)] enum ControlIcon { Minimize, Restore, Maximize, Close { on_close_window: Option>>, }, } impl ControlIcon { fn minimize() -> Self { Self::Minimize } fn restore() -> Self { Self::Restore } fn maximize() -> Self { Self::Maximize } fn close(on_close_window: Option>>) -> Self { Self::Close { on_close_window } } fn id(&self) -> &'static str { match self { Self::Minimize => "minimize", Self::Restore => "restore", Self::Maximize => "maximize", Self::Close { .. } => "close", } } fn icon(&self) -> IconName { match self { Self::Minimize => IconName::WindowMinimize, Self::Restore => IconName::WindowRestore, Self::Maximize => IconName::WindowMaximize, Self::Close { .. } => IconName::WindowClose, } } fn window_control_area(&self) -> WindowControlArea { match self { Self::Minimize => WindowControlArea::Min, Self::Restore | Self::Maximize => WindowControlArea::Max, Self::Close { .. } => WindowControlArea::Close, } } fn is_close(&self) -> bool { matches!(self, Self::Close { .. }) } #[inline] fn hover_fg(&self, cx: &App) -> Hsla { if self.is_close() { cx.theme().danger_foreground } else { cx.theme().secondary_foreground } } #[inline] fn hover_bg(&self, cx: &App) -> Hsla { if self.is_close() { cx.theme().danger } else { cx.theme().secondary_hover } } #[inline] fn active_bg(&self, cx: &mut App) -> Hsla { if self.is_close() { cx.theme().danger_active } else { cx.theme().secondary_active } } } impl RenderOnce for ControlIcon { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let is_linux = cfg!(target_os = "linux"); let is_windows = cfg!(target_os = "windows"); let hover_fg = self.hover_fg(cx); let hover_bg = self.hover_bg(cx); let active_bg = self.active_bg(cx); let icon = self.clone(); let on_close_window = match &self { ControlIcon::Close { on_close_window } => on_close_window.clone(), _ => None, }; div() .id(self.id()) .flex() .w(TITLE_BAR_HEIGHT) .h_full() .flex_shrink_0() .justify_center() .content_center() .items_center() .text_color(cx.theme().foreground) .hover(|style| style.bg(hover_bg).text_color(hover_fg)) .active(|style| style.bg(active_bg).text_color(hover_fg)) .when(is_windows, |this| { this.window_control_area(self.window_control_area()) }) .when(is_linux, |this| { this.on_mouse_down(MouseButton::Left, move |_, window, cx| { window.prevent_default(); cx.stop_propagation(); }) .on_click(move |_, window, cx| { cx.stop_propagation(); match icon { Self::Minimize => window.minimize_window(), Self::Restore | Self::Maximize => window.zoom_window(), Self::Close { .. } => { if let Some(f) = on_close_window.clone() { f(&ClickEvent::default(), window, cx); } else { window.remove_window(); } } } }) }) .child(Icon::new(self.icon()).small()) } } #[derive(IntoElement)] struct WindowControls { on_close_window: Option>>, } impl RenderOnce for WindowControls { fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement { if cfg!(target_os = "macos") || cfg!(target_family = "wasm") { return div().id("window-controls"); } h_flex() .id("window-controls") .items_center() .flex_shrink_0() .h_full() .child(ControlIcon::minimize()) .child(if window.is_maximized() { ControlIcon::restore() } else { ControlIcon::maximize() }) .child(ControlIcon::close(self.on_close_window)) } } impl Styled for TitleBar { fn style(&mut self) -> &mut gpui::StyleRefinement { &mut self.style } } impl ParentElement for TitleBar { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } struct TitleBarState { should_move: bool, } // TODO: Remove this when GPUI has released v0.2.3 impl Render for TitleBarState { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() } } impl RenderOnce for TitleBar { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let is_client_decorated = matches!(window.window_decorations(), Decorations::Client { .. }); let is_web = cfg!(target_family = "wasm"); let is_linux = cfg!(target_os = "linux"); let is_macos = cfg!(target_os = "macos"); let state = window.use_state(cx, |_, _| TitleBarState { should_move: false }); div().flex_shrink_0().child( div() .id("title-bar") .flex() .flex_row() .items_center() .justify_between() .h(TITLE_BAR_HEIGHT) .pl(TITLE_BAR_LEFT_PADDING) .border_b_1() .border_color(cx.theme().title_bar_border) .bg(cx.theme().title_bar) .refine_style(&self.style) .when(is_linux, |this| { this.on_double_click(|_, window, _| window.zoom_window()) }) .when(is_macos, |this| { this.on_double_click(|_, window, _| window.titlebar_double_click()) }) .on_mouse_down_out(window.listener_for(&state, |state, _, _, _| { state.should_move = false; })) .on_mouse_down( MouseButton::Left, window.listener_for(&state, |state, _, _, _| { state.should_move = true; }), ) .on_mouse_up( MouseButton::Left, window.listener_for(&state, |state, _, _, _| { state.should_move = false; }), ) .on_mouse_move(window.listener_for(&state, |state, _, window, _| { if state.should_move { state.should_move = false; window.start_window_move(); } })) .child( h_flex() .id("bar") .h_full() .justify_between() .flex_shrink_0() .flex_1() .when(!is_web, |this| { this.window_control_area(WindowControlArea::Drag) .when(window.is_fullscreen(), |this| this.pl_3()) .when(is_linux && is_client_decorated, |this| { this.child( div() .top_0() .left_0() .absolute() .size_full() .h_full() .on_mouse_down( MouseButton::Right, move |ev, window, _| { window.show_window_menu(ev.position) }, ), ) }) }) .children(self.children), ) .child(WindowControls { on_close_window: self.on_close_window, }), ) } } ================================================ FILE: crates/ui/src/tooltip.rs ================================================ use gpui::{ div, prelude::FluentBuilder, px, Action, AnyElement, AnyView, App, AppContext, Context, IntoElement, ParentElement, Render, SharedString, StyleRefinement, Styled, Window, }; use crate::{h_flex, kbd::Kbd, text::Text, ActiveTheme, StyledExt}; enum TooltipContext { Text(Text), Element(Box AnyElement>), } /// A Tooltip element that can display text or custom content, /// with optional key binding information. pub struct Tooltip { style: StyleRefinement, content: TooltipContext, key_binding: Option, action: Option<(Box, Option)>, } impl Tooltip { /// Create a Tooltip with a text content. pub fn new(text: impl Into) -> Self { Self { style: StyleRefinement::default(), content: TooltipContext::Text(text.into()), key_binding: None, action: None, } } /// Create a Tooltip with a custom element. pub fn element(builder: F) -> Self where E: IntoElement, F: Fn(&mut Window, &mut App) -> E + 'static, { Self { style: StyleRefinement::default(), key_binding: None, action: None, content: TooltipContext::Element(Box::new(move |window, cx| { builder(window, cx).into_any_element() })), } } /// Set Action to display key binding information for the tooltip if it exists. pub fn action(mut self, action: &dyn Action, context: Option<&str>) -> Self { self.action = Some((action.boxed_clone(), context.map(SharedString::new))); self } /// Set KeyBinding information for the tooltip. pub fn key_binding(mut self, key_binding: Option) -> Self { self.key_binding = key_binding; self } /// Build the tooltip and return it as an `AnyView`. pub fn build(self, _: &mut Window, cx: &mut App) -> AnyView { cx.new(|_| self).into() } } impl FluentBuilder for Tooltip {} impl Styled for Tooltip { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl Render for Tooltip { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let key_binding = if let Some(key_binding) = &self.key_binding { Some(key_binding.clone()) } else { if let Some((action, context)) = &self.action { Kbd::binding_for_action( action.as_ref(), context.as_ref().map(|s| s.as_ref()), window, ) } else { None } }; div().child( // Wrap in a child, to ensure the left margin is applied to the tooltip h_flex() .font_family(cx.theme().font_family.clone()) .m_3() .bg(cx.theme().popover) .text_color(cx.theme().popover_foreground) .bg(cx.theme().popover) .border_1() .border_color(cx.theme().border) .shadow_md() .rounded(px(6.)) .justify_between() .py_0p5() .px_2() .text_sm() .gap_3() .refine_style(&self.style) .map(|this| { this.child(div().map(|this| match self.content { TooltipContext::Text(ref text) => this.child(text.clone()), TooltipContext::Element(ref builder) => this.child(builder(window, cx)), })) }) .when_some(key_binding, |this, kbd| { this.child( div() .text_xs() .flex_shrink_0() .text_color(cx.theme().muted_foreground) .child(kbd.appearance(false)), ) }), ) } } ================================================ FILE: crates/ui/src/tree.rs ================================================ use std::{cell::RefCell, ops::Range, rc::Rc}; use gpui::{ App, Context, ElementId, Entity, FocusHandle, InteractiveElement as _, IntoElement, KeyBinding, ListSizingBehavior, MouseButton, ParentElement, Render, RenderOnce, SharedString, StyleRefinement, Styled, UniformListScrollHandle, Window, div, prelude::FluentBuilder as _, uniform_list, }; use crate::{ StyledExt, actions::{Confirm, SelectDown, SelectLeft, SelectRight, SelectUp}, list::ListItem, scroll::ScrollableElement, }; const CONTEXT: &str = "Tree"; pub(crate) fn init(cx: &mut App) { cx.bind_keys([ KeyBinding::new("up", SelectUp, Some(CONTEXT)), KeyBinding::new("down", SelectDown, Some(CONTEXT)), KeyBinding::new("left", SelectLeft, Some(CONTEXT)), KeyBinding::new("right", SelectRight, Some(CONTEXT)), ]); } /// Create a [`Tree`]. /// /// # Arguments /// /// * `state` - The shared state managing the tree items. /// * `render_item` - A closure to render each tree item. /// /// ```ignore /// let state = cx.new(|_| { /// TreeState::new().items(vec![ /// TreeItem::new("src") /// .child(TreeItem::new("lib.rs"), /// TreeItem::new("Cargo.toml"), /// TreeItem::new("README.md"), /// ]) /// }); /// /// tree(&state, |ix, entry, selected, window, cx| { /// let item = entry.item(); /// ListItem::new(ix).pl(px(16.) * entry.depth()).child(item.label.clone()) /// }) /// ``` pub fn tree(state: &Entity, render_item: R) -> Tree where R: Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem + 'static, { Tree::new(state, render_item) } struct TreeItemState { expanded: bool, disabled: bool, } /// A tree item with a label, children, and an expanded state. #[derive(Clone)] pub struct TreeItem { pub id: SharedString, pub label: SharedString, pub children: Vec, state: Rc>, } /// A flat representation of a tree item with its depth. #[derive(Clone)] pub struct TreeEntry { item: TreeItem, depth: usize, } impl TreeEntry { /// Get the source tree item. #[inline] pub fn item(&self) -> &TreeItem { &self.item } /// The depth of this item in the tree. #[inline] pub fn depth(&self) -> usize { self.depth } #[inline] fn is_root(&self) -> bool { self.depth == 0 } /// Whether this item is a folder (has children). #[inline] pub fn is_folder(&self) -> bool { self.item.is_folder() } /// Return true if the item is expanded. #[inline] pub fn is_expanded(&self) -> bool { self.item.is_expanded() } #[inline] pub fn is_disabled(&self) -> bool { self.item.is_disabled() } } impl TreeItem { /// Create a new tree item with the given label. /// /// - The `id` for you to uniquely identify this item, then later you can use it for selection or other purposes. /// - The `label` is the text to display for this item. /// /// For example, the `id` is the full file path, and the `label` is the file name. /// /// ```ignore /// TreeItem::new("src/ui/button.rs", "button.rs") /// ``` pub fn new(id: impl Into, label: impl Into) -> Self { Self { id: id.into(), label: label.into(), children: Vec::new(), state: Rc::new(RefCell::new(TreeItemState { expanded: false, disabled: false, })), } } /// Add a child item to this tree item. pub fn child(mut self, child: TreeItem) -> Self { self.children.push(child); self } /// Add multiple child items to this tree item. pub fn children(mut self, children: impl IntoIterator) -> Self { self.children.extend(children); self } /// Set expanded state for this tree item. pub fn expanded(self, expanded: bool) -> Self { self.state.borrow_mut().expanded = expanded; self } /// Set disabled state for this tree item. pub fn disabled(self, disabled: bool) -> Self { self.state.borrow_mut().disabled = disabled; self } /// Whether this item is a folder (has children). #[inline] pub fn is_folder(&self) -> bool { self.children.len() > 0 } /// Return true if the item is disabled. pub fn is_disabled(&self) -> bool { self.state.borrow().disabled } /// Return true if the item is expanded. #[inline] pub fn is_expanded(&self) -> bool { self.state.borrow().expanded } fn find_ancestors(&self, target_id: &SharedString) -> Option> { if self.id == *target_id { return Some(vec![]); } for child in &self.children { if let Some(mut path) = child.find_ancestors(target_id) { path.push(self.clone()); return Some(path); } } None } } /// State for managing tree items. pub struct TreeState { focus_handle: FocusHandle, entries: Vec, scroll_handle: UniformListScrollHandle, selected_ix: Option, render_item: Rc ListItem>, } impl TreeState { /// Create a new empty tree state. pub fn new(cx: &mut App) -> Self { Self { selected_ix: None, focus_handle: cx.focus_handle(), scroll_handle: UniformListScrollHandle::default(), entries: Vec::new(), render_item: Rc::new(|_, _, _, _, _| ListItem::new(0)), } } /// Set the tree items. pub fn items(mut self, items: impl Into>) -> Self { let items = items.into(); self.entries.clear(); for item in items.into_iter() { self.add_entry(item, 0); } self } /// Set the tree items. pub fn set_items(&mut self, items: impl Into>, cx: &mut Context) { let items = items.into(); self.entries.clear(); for item in items.into_iter() { self.add_entry(item, 0); } self.selected_ix = None; cx.notify(); } /// Get the currently selected index, if any. pub fn selected_index(&self) -> Option { self.selected_ix } /// Set the selected index, or `None` to clear selection. pub fn set_selected_index(&mut self, ix: Option, cx: &mut Context) { self.selected_ix = ix; cx.notify(); } /// Set the selected index by tree item, or `None` to clear selection. pub fn set_selected_item(&mut self, item: Option<&TreeItem>, cx: &mut Context) { if let Some(item) = item { let ix = self .entries .iter() .position(|entry| entry.item.id == item.id); if ix.is_some() { self.selected_ix = ix; } else { self.expand_ancestors(item.id.clone()); self.selected_ix = self .entries .iter() .position(|entry| entry.item.id == item.id); } } else { self.selected_ix = None; } cx.notify(); } /// Get the currently selected tree item, if any. pub fn selected_item(&self) -> Option<&TreeItem> { self.selected_ix .and_then(|ix| self.entries.get(ix).map(|entry| &entry.item)) } pub fn scroll_to_item(&mut self, ix: usize, strategy: gpui::ScrollStrategy) { self.scroll_handle.scroll_to_item(ix, strategy); } /// Get the currently selected entry, if any. pub fn selected_entry(&self) -> Option<&TreeEntry> { self.selected_ix.and_then(|ix| self.entries.get(ix)) } fn expand_ancestors(&mut self, target_id: SharedString) { let mut ancestors = Vec::new(); for entry in &self.entries { if let Some(found_ancestors) = entry.item.find_ancestors(&target_id) { ancestors = found_ancestors; break; } } if ancestors.is_empty() { return; } for ancestor in ancestors { ancestor.state.borrow_mut().expanded = true; } self.rebuild_entries(); } fn add_entry(&mut self, item: TreeItem, depth: usize) { self.entries.push(TreeEntry { item: item.clone(), depth, }); if item.is_expanded() { for child in &item.children { self.add_entry(child.clone(), depth + 1); } } } fn toggle_expand(&mut self, ix: usize) { let Some(entry) = self.entries.get_mut(ix) else { return; }; if !entry.is_folder() { return; } entry.item.state.borrow_mut().expanded = !entry.is_expanded(); self.rebuild_entries(); } fn rebuild_entries(&mut self) { let root_items: Vec = self .entries .iter() .filter(|e| e.is_root()) .map(|e| e.item.clone()) .collect(); self.entries.clear(); for item in root_items.into_iter() { self.add_entry(item, 0); } } pub fn focus(&mut self, window: &mut Window, cx: &mut App) { self.focus_handle.focus(window, cx); } fn on_action_confirm(&mut self, _: &Confirm, _: &mut Window, cx: &mut Context) { if let Some(selected_ix) = self.selected_ix { if let Some(entry) = self.entries.get(selected_ix) { if entry.is_folder() { self.toggle_expand(selected_ix); cx.notify(); } } } } fn on_action_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context) { if let Some(selected_ix) = self.selected_ix { if let Some(entry) = self.entries.get(selected_ix) { if entry.is_folder() && entry.is_expanded() { self.toggle_expand(selected_ix); cx.notify(); } } } } fn on_action_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context) { if let Some(selected_ix) = self.selected_ix { if let Some(entry) = self.entries.get(selected_ix) { if entry.is_folder() && !entry.is_expanded() { self.toggle_expand(selected_ix); cx.notify(); } } } } fn on_action_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context) { let mut selected_ix = self.selected_ix.unwrap_or(0); if selected_ix > 0 { selected_ix = selected_ix - 1; } else { selected_ix = self.entries.len().saturating_sub(1); } self.selected_ix = Some(selected_ix); self.scroll_handle .scroll_to_item(selected_ix, gpui::ScrollStrategy::Top); cx.notify(); } fn on_action_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context) { let mut selected_ix = self.selected_ix.unwrap_or(0); if selected_ix + 1 < self.entries.len() { selected_ix = selected_ix + 1; } else { selected_ix = 0; } self.selected_ix = Some(selected_ix); self.scroll_handle .scroll_to_item(selected_ix, gpui::ScrollStrategy::Bottom); cx.notify(); } fn on_entry_click(&mut self, ix: usize, _: &mut Window, cx: &mut Context) { self.selected_ix = Some(ix); self.toggle_expand(ix); cx.notify(); } } impl Render for TreeState { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let render_item = self.render_item.clone(); div().id("tree-state").size_full().relative().child( uniform_list("entries", self.entries.len(), { cx.processor(move |state, visible_range: Range, window, cx| { let mut items = Vec::with_capacity(visible_range.len()); for ix in visible_range { let entry = &state.entries[ix]; let selected = Some(ix) == state.selected_ix; let item = (render_item)(ix, entry, selected, window, cx); let el = div() .id(ix) .child(item.disabled(entry.item().is_disabled()).selected(selected)) .when(!entry.item().is_disabled(), |this| { this.on_mouse_down( MouseButton::Left, cx.listener({ move |this, _, window, cx| { this.on_entry_click(ix, window, cx); } }), ) }); items.push(el) } items }) }) .flex_grow() .size_full() .track_scroll(&self.scroll_handle) .with_sizing_behavior(ListSizingBehavior::Auto) .into_any_element(), ) } } /// A tree view element that displays hierarchical data. #[derive(IntoElement)] pub struct Tree { id: ElementId, state: Entity, style: StyleRefinement, render_item: Rc ListItem>, } impl Tree { pub fn new(state: &Entity, render_item: R) -> Self where R: Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem + 'static, { Self { id: ElementId::Name(format!("tree-{}", state.entity_id()).into()), state: state.clone(), style: StyleRefinement::default(), render_item: Rc::new(move |ix, item, selected, window, app| { render_item(ix, item, selected, window, app) }), } } } impl Styled for Tree { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl RenderOnce for Tree { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let focus_handle = self.state.read(cx).focus_handle.clone(); let scroll_handle = self.state.read(cx).scroll_handle.clone(); self.state .update(cx, |state, _| state.render_item = self.render_item); div() .id(self.id) .key_context(CONTEXT) .track_focus(&focus_handle) .on_action(window.listener_for(&self.state, TreeState::on_action_confirm)) .on_action(window.listener_for(&self.state, TreeState::on_action_left)) .on_action(window.listener_for(&self.state, TreeState::on_action_right)) .on_action(window.listener_for(&self.state, TreeState::on_action_up)) .on_action(window.listener_for(&self.state, TreeState::on_action_down)) .size_full() .child(self.state) .refine_style(&self.style) .vertical_scrollbar(&scroll_handle) } } #[cfg(test)] mod tests { use indoc::indoc; use super::TreeState; use gpui::AppContext as _; fn assert_entries(entries: &Vec, expected: &str) { let actual: Vec = entries .iter() .map(|e| { let mut s = String::new(); s.push_str(&" ".repeat(e.depth)); s.push_str(e.item().label.as_str()); s }) .collect(); let actual = actual.join("\n"); assert_eq!(actual.trim(), expected.trim()); } #[gpui::test] fn test_tree_entry(cx: &mut gpui::TestAppContext) { use super::TreeItem; let items = vec![ TreeItem::new("src", "src") .expanded(true) .child( TreeItem::new("src/ui", "ui") .expanded(true) .child(TreeItem::new("src/ui/button.rs", "button.rs")) .child(TreeItem::new("src/ui/icon.rs", "icon.rs")) .child(TreeItem::new("src/ui/mod.rs", "mod.rs")), ) .child(TreeItem::new("src/lib.rs", "lib.rs")), TreeItem::new("Cargo.toml", "Cargo.toml"), TreeItem::new("Cargo.lock", "Cargo.lock").disabled(true), TreeItem::new("README.md", "README.md"), ]; let state = cx.new(|cx| TreeState::new(cx).items(items)); state.update(cx, |state, _| { assert_entries( &state.entries, indoc! { r#" src ui button.rs icon.rs mod.rs lib.rs Cargo.toml Cargo.lock README.md "# }, ); let entry = state.entries.get(0).unwrap(); assert_eq!(entry.depth(), 0); assert_eq!(entry.is_root(), true); assert_eq!(entry.is_folder(), true); assert_eq!(entry.is_expanded(), true); let entry = state.entries.get(1).unwrap(); assert_eq!(entry.depth(), 1); assert_eq!(entry.is_root(), false); assert_eq!(entry.is_folder(), true); assert_eq!(entry.is_expanded(), true); assert_eq!(entry.item().label.as_str(), "ui"); state.toggle_expand(1); let entry = state.entries.get(1).unwrap(); assert_eq!(entry.is_expanded(), false); assert_entries( &state.entries, indoc! { r#" src ui lib.rs Cargo.toml Cargo.lock README.md "# }, ); }) } } ================================================ FILE: crates/ui/src/virtual_list.rs ================================================ //! Virtual List for render a large number of differently sized rows/columns. //! //! > NOTE: This must ensure each column width or row height. //! //! Only visible range are rendered for performance reasons. //! //! Inspired by `gpui::uniform_list`. //! https://github.com/zed-industries/zed/blob/0ae1603610ab6b265bdfbee7b8dbc23c5ab06edc/crates/gpui/src/elements/uniform_list.rs //! //! Unlike the `uniform_list`, the each item can have different size. //! //! This is useful for more complex layout, for example, a table with different row height. use std::{ cell::RefCell, cmp, ops::{Deref, Range}, rc::Rc, }; use gpui::{ Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask, Context, DeferredScrollToItem, Div, Element, ElementId, Entity, GlobalElementId, Half, Hitbox, InteractiveElement, IntoElement, IsZero as _, ListSizingBehavior, Pixels, Point, Render, ScrollHandle, ScrollStrategy, Size, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window, div, point, px, size, }; use smallvec::SmallVec; use crate::{AxisExt, scroll::ScrollbarHandle}; struct VirtualListScrollHandleState { axis: Axis, items_count: usize, pub deferred_scroll_to_item: Option, } /// A scroll handle for [`VirtualList`]. /// /// See also [`ScrollHandle`]. #[derive(Clone)] pub struct VirtualListScrollHandle { state: Rc>, base_handle: ScrollHandle, } impl From for VirtualListScrollHandle { fn from(handle: ScrollHandle) -> Self { let mut this = VirtualListScrollHandle::new(); this.base_handle = handle; this } } impl AsRef for VirtualListScrollHandle { fn as_ref(&self) -> &ScrollHandle { &self.base_handle } } impl ScrollbarHandle for VirtualListScrollHandle { fn offset(&self) -> Point { self.base_handle.offset() } fn set_offset(&self, offset: Point) { self.base_handle.set_offset(offset); } fn content_size(&self) -> Size { self.base_handle.content_size() } } impl Deref for VirtualListScrollHandle { type Target = ScrollHandle; fn deref(&self) -> &Self::Target { &self.base_handle } } impl VirtualListScrollHandle { /// Create a new VirtualListScrollHandle. pub fn new() -> Self { VirtualListScrollHandle { state: Rc::new(RefCell::new(VirtualListScrollHandleState { axis: Axis::Vertical, items_count: 0, deferred_scroll_to_item: None, })), base_handle: ScrollHandle::default(), } } /// Get the base scroll handle. pub fn base_handle(&self) -> &ScrollHandle { &self.base_handle } /// Scroll to the item at the given index. pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) { self.scroll_to_item_with_offset(ix, strategy, 0); } /// Scroll to the item at the given index, with an additional offset items. fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) { let mut state = self.state.borrow_mut(); state.deferred_scroll_to_item = Some(DeferredScrollToItem { item_index: ix, strategy, offset, scroll_strict: false, }); } /// Scrolls to the bottom of the list. pub fn scroll_to_bottom(&self) { let items_count = self.state.borrow().items_count; self.scroll_to_item(items_count.saturating_sub(1), ScrollStrategy::Top); } } /// Create a [`VirtualList`] in vertical direction. /// /// This is like `uniform_list` in GPUI, but support two axis. /// /// The `item_sizes` is the size of each column, /// only the `height` is used, `width` is ignored and VirtualList will measure the first item width. /// /// See also [`h_virtual_list`] #[inline] pub fn v_virtual_list( view: Entity, id: impl Into, item_sizes: Rc>>, f: impl 'static + Fn(&mut V, Range, &mut Window, &mut Context) -> Vec, ) -> VirtualList where R: IntoElement, V: Render, { virtual_list(view, id, Axis::Vertical, item_sizes, f) } /// Create a [`VirtualList`] in horizontal direction. /// /// The `item_sizes` is the size of each column, /// only the `width` is used, `height` is ignored and VirtualList will measure the first item height. /// /// See also [`v_virtual_list`] #[inline] pub fn h_virtual_list( view: Entity, id: impl Into, item_sizes: Rc>>, f: impl 'static + Fn(&mut V, Range, &mut Window, &mut Context) -> Vec, ) -> VirtualList where R: IntoElement, V: Render, { virtual_list(view, id, Axis::Horizontal, item_sizes, f) } pub(crate) fn virtual_list( view: Entity, id: impl Into, axis: Axis, item_sizes: Rc>>, f: impl 'static + Fn(&mut V, Range, &mut Window, &mut Context) -> Vec, ) -> VirtualList where R: IntoElement, V: Render, { let id: ElementId = id.into(); let scroll_handle = VirtualListScrollHandle::new(); let render_range = move |visible_range, window: &mut Window, cx: &mut App| { view.update(cx, |this, cx| { f(this, visible_range, window, cx) .into_iter() .map(|component| component.into_any_element()) .collect() }) }; VirtualList { id: id.clone(), axis, base: div() .id(id) .size_full() .overflow_scroll() .track_scroll(&scroll_handle), scroll_handle, items_count: item_sizes.len(), item_sizes, render_items: Box::new(render_range), sizing_behavior: ListSizingBehavior::default(), } } /// VirtualList component for rendering a large number of differently sized items. pub struct VirtualList { id: ElementId, axis: Axis, base: Stateful
, scroll_handle: VirtualListScrollHandle, items_count: usize, item_sizes: Rc>>, render_items: Box< dyn for<'a> Fn(Range, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>, >, sizing_behavior: ListSizingBehavior, } impl Styled for VirtualList { fn style(&mut self) -> &mut StyleRefinement { self.base.style() } } impl VirtualList { pub fn track_scroll(mut self, scroll_handle: &VirtualListScrollHandle) -> Self { self.base = self.base.track_scroll(&scroll_handle); self.scroll_handle = scroll_handle.clone(); self } /// Set the sizing behavior for the list. pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self { self.sizing_behavior = behavior; self } /// Specify for table. /// /// Table is special, because the `scroll_handle` is based on Table head (That is not a virtual list). pub(crate) fn with_scroll_handle(mut self, scroll_handle: &VirtualListScrollHandle) -> Self { self.base = div().id(self.id.clone()).size_full(); self.scroll_handle = scroll_handle.clone(); self } fn scroll_to_deferred_item( &self, scroll_offset: Point, items_bounds: &[Bounds], content_bounds: &Bounds, scroll_to_item: DeferredScrollToItem, ) -> Point { let Some(bounds) = items_bounds .get(scroll_to_item.item_index + scroll_to_item.offset) .cloned() else { return scroll_offset; }; let mut scroll_offset = scroll_offset; match scroll_to_item.strategy { ScrollStrategy::Center => { if self.axis.is_vertical() { scroll_offset.y = content_bounds.top() + content_bounds.size.height.half() - bounds.top() - bounds.size.height.half() } else { scroll_offset.x = content_bounds.left() + content_bounds.size.width.half() - bounds.left() - bounds.size.width.half() } } _ => { // Ref: https://github.com/zed-industries/zed/blob/0d145289e0867a8d5d63e5e1397a5ca69c9d49c3/crates/gpui/src/elements/div.rs#L3026 if self.axis.is_vertical() { if bounds.top() + scroll_offset.y < content_bounds.top() { scroll_offset.y = content_bounds.top() - bounds.top() } else if bounds.bottom() + scroll_offset.y > content_bounds.bottom() { scroll_offset.y = content_bounds.bottom() - bounds.bottom(); } } else { if bounds.left() + scroll_offset.x < content_bounds.left() { scroll_offset.x = content_bounds.left() - bounds.left(); } else if bounds.right() + scroll_offset.x > content_bounds.right() { scroll_offset.x = content_bounds.right() - bounds.right(); } } } } self.scroll_handle.set_offset(scroll_offset); scroll_offset } /// Ref from: https://github.com/zed-industries/zed/blob/83f9f9d9e3f5914392cab9a09e3472711a1d7b38/crates/gpui/src/elements/uniform_list.rs#L660 fn measure_item( &self, list_width: Option, window: &mut Window, cx: &mut App, ) -> Size { if self.items_count == 0 { return Size::default(); } let item_ix = 0; let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx); let Some(mut item_to_measure) = items.pop() else { return Size::default(); }; let available_space = size( list_width.map_or(AvailableSpace::MinContent, |width| { AvailableSpace::Definite(width) }), AvailableSpace::MinContent, ); item_to_measure.layout_as_root(available_space, window, cx) } } /// Frame state used by the [VirtualItem]. pub struct VirtualListFrameState { /// Visible items to be painted. items: SmallVec<[AnyElement; 32]>, size_layout: ItemSizeLayout, } #[derive(Default, Clone)] pub struct ItemSizeLayout { items_sizes: Rc>>, content_size: Size, sizes: Vec, origins: Vec, last_layout_bounds: Bounds, } impl IntoElement for VirtualList { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for VirtualList { type RequestLayoutState = VirtualListFrameState; type PrepaintState = Option; fn id(&self) -> Option { Some(self.id.clone()) } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (gpui::LayoutId, Self::RequestLayoutState) { let rem_size = window.rem_size(); let font_size = window.text_style().font_size.to_pixels(rem_size); let mut size_layout = ItemSizeLayout::default(); let longest_item_size = self.measure_item(None, window, cx); let layout_id = self.base.interactivity().request_layout( global_id, inspector_id, window, cx, |style, window, cx| { size_layout = window.with_element_state( global_id.unwrap(), |state: Option, _window| { let mut state = state.unwrap_or(ItemSizeLayout::default()); // Including the gap between items for calculate the item size let gap = style .gap .along(self.axis) .to_pixels(font_size.into(), rem_size); if state.items_sizes != self.item_sizes { state.items_sizes = self.item_sizes.clone(); // Prepare each item's size by axis state.sizes = self .item_sizes .iter() .enumerate() .map(|(i, size)| { let size = size.along(self.axis); if i + 1 == self.items_count { size } else { size + gap } }) .collect::>(); // Prepare each item's origin by axis state.origins = state .sizes .iter() .scan(px(0.), |cumulative, size| match self.axis { Axis::Horizontal => { let x = *cumulative; *cumulative += *size; Some(x) } Axis::Vertical => { let y = *cumulative; *cumulative += *size; Some(y) } }) .collect::>(); state.content_size = if self.axis.is_horizontal() { Size { width: px(state .sizes .iter() .map(|size| size.as_f32()) .sum::()), height: longest_item_size.height, } } else { Size { width: longest_item_size.width, height: px(state .sizes .iter() .map(|size| size.as_f32()) .sum::()), } }; } (state.clone(), state) }, ); let axis = self.axis; let layout_id = match self.sizing_behavior { ListSizingBehavior::Infer => { window.with_text_style(style.text_style().cloned(), |window| { let size_layout = size_layout.clone(); window.request_measured_layout(style, { move |known_dimensions, available_space, _, _| { let mut size = Size::default(); if axis.is_horizontal() { size.width = known_dimensions.width.unwrap_or( match available_space.width { AvailableSpace::Definite(x) => x, AvailableSpace::MinContent | AvailableSpace::MaxContent => { size_layout.content_size.width } }, ); size.height = known_dimensions.width.unwrap_or( match available_space.height { AvailableSpace::Definite(x) => x, AvailableSpace::MinContent | AvailableSpace::MaxContent => { size_layout.content_size.height } }, ); } else { size.width = known_dimensions.width.unwrap_or( match available_space.width { AvailableSpace::Definite(x) => x, AvailableSpace::MinContent | AvailableSpace::MaxContent => { size_layout.content_size.width } }, ); size.height = known_dimensions.height.unwrap_or( match available_space.height { AvailableSpace::Definite(x) => x, AvailableSpace::MinContent | AvailableSpace::MaxContent => { size_layout.content_size.height } }, ); } size } }) }) } ListSizingBehavior::Auto => window .with_text_style(style.text_style().cloned(), |window| { window.request_layout(style, None, cx) }), }; layout_id }, ); ( layout_id, VirtualListFrameState { items: SmallVec::new(), size_layout, }, ) } fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, bounds: Bounds, layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { layout.size_layout.last_layout_bounds = bounds; let style = self .base .interactivity() .compute_style(global_id, None, window, cx); let border_widths = style.border_widths.to_pixels(window.rem_size()); let paddings = style .padding .to_pixels(bounds.size.into(), window.rem_size()); let item_sizes = &layout.size_layout.sizes; let item_origins = &layout.size_layout.origins; let content_bounds = Bounds::from_corners( bounds.origin + point( border_widths.left + paddings.left, border_widths.top + paddings.top, ), bounds.bottom_right() - point( border_widths.right + paddings.right, border_widths.bottom + paddings.bottom, ), ); // Update scroll_handle with the item bounds let items_bounds = item_origins .iter() .enumerate() .map(|(i, &origin)| { let item_size = item_sizes[i]; Bounds { origin: match self.axis { Axis::Horizontal => point(content_bounds.left() + origin, px(0.)), Axis::Vertical => point(px(0.), content_bounds.top() + origin), }, size: match self.axis { Axis::Horizontal => size(item_size, content_bounds.size.height), Axis::Vertical => size(content_bounds.size.width, item_size), }, } }) .collect::>(); let axis = self.axis; let mut scroll_state = self.scroll_handle.state.borrow_mut(); scroll_state.axis = axis; scroll_state.items_count = self.items_count; let mut scroll_offset = self.scroll_handle.offset(); if let Some(scroll_to_item) = scroll_state.deferred_scroll_to_item.take() { scroll_offset = self.scroll_to_deferred_item( scroll_offset, &items_bounds, &content_bounds, scroll_to_item, ); } scroll_offset = scroll_offset .max(&point( content_bounds.size.width - layout.size_layout.content_size.width, content_bounds.size.height - layout.size_layout.content_size.height, )) .min(&point(px(0.), px(0.))); if scroll_offset != self.scroll_handle.offset() { self.scroll_handle.set_offset(scroll_offset); } self.base.interactivity().prepaint( global_id, inspector_id, bounds, layout.size_layout.content_size, window, cx, |_style, _, hitbox, window, cx| { if self.items_count > 0 { let min_scroll_offset = content_bounds.size.along(self.axis) - layout.size_layout.content_size.along(self.axis); let is_scrolled = !scroll_offset.along(self.axis).is_zero(); if is_scrolled { match self.axis { Axis::Horizontal if scroll_offset.x < min_scroll_offset => { scroll_offset.x = min_scroll_offset; self.scroll_handle.set_offset(scroll_offset); } Axis::Vertical if scroll_offset.y < min_scroll_offset => { scroll_offset.y = min_scroll_offset; self.scroll_handle.set_offset(scroll_offset); } _ => {} } } let (first_visible_element_ix, last_visible_element_ix) = match self.axis { Axis::Horizontal => { let mut cumulative_size = px(0.); let mut first_visible_element_ix = 0; for (i, &size) in item_sizes.iter().enumerate() { cumulative_size += size; if cumulative_size > -(scroll_offset.x + paddings.left) { first_visible_element_ix = i; break; } } cumulative_size = px(0.); let mut last_visible_element_ix = 0; for (i, &size) in item_sizes.iter().enumerate() { cumulative_size += size; if cumulative_size > (-scroll_offset.x + content_bounds.size.width) { last_visible_element_ix = i + 1; break; } } if last_visible_element_ix == 0 { last_visible_element_ix = self.items_count; } else { last_visible_element_ix += 1; } (first_visible_element_ix, last_visible_element_ix) } Axis::Vertical => { let mut cumulative_size = px(0.); let mut first_visible_element_ix = 0; for (i, &size) in item_sizes.iter().enumerate() { cumulative_size += size; if cumulative_size > -(scroll_offset.y + paddings.top) { first_visible_element_ix = i; break; } } cumulative_size = px(0.); let mut last_visible_element_ix = 0; for (i, &size) in item_sizes.iter().enumerate() { cumulative_size += size; if cumulative_size > (-scroll_offset.y + content_bounds.size.height) { last_visible_element_ix = i + 1; break; } } if last_visible_element_ix == 0 { last_visible_element_ix = self.items_count; } else { last_visible_element_ix += 1; } (first_visible_element_ix, last_visible_element_ix) } }; let visible_range = first_visible_element_ix ..cmp::min(last_visible_element_ix, self.items_count); let items = (self.render_items)(visible_range.clone(), window, cx); let content_mask = ContentMask { bounds }; window.with_content_mask(Some(content_mask), |window| { for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { let item_origin = match self.axis { Axis::Horizontal => { content_bounds.origin + point(item_origins[ix] + scroll_offset.x, scroll_offset.y) } Axis::Vertical => { content_bounds.origin + point(scroll_offset.x, item_origins[ix] + scroll_offset.y) } }; let available_space = match self.axis { Axis::Horizontal => size( AvailableSpace::Definite(item_sizes[ix]), AvailableSpace::Definite(content_bounds.size.height), ), Axis::Vertical => size( AvailableSpace::Definite(content_bounds.size.width), AvailableSpace::Definite(item_sizes[ix]), ), }; item.layout_as_root(available_space, window, cx); item.prepaint_at(item_origin, window, cx); layout.items.push(item); } }); } hitbox }, ) } fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&gpui::InspectorElementId>, bounds: Bounds, layout: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { self.base.interactivity().paint( global_id, inspector_id, bounds, hitbox.as_ref(), window, cx, |_, window, cx| { for item in &mut layout.items { item.paint(window, cx); } }, ) } } ================================================ FILE: crates/ui/src/window_border.rs ================================================ // From: // https://github.com/zed-industries/zed/blob/56daba28d40301ee4c05546fadb691d070b7b2b6/crates/gpui/examples/window_shadow.rs use gpui::{ AnyElement, App, Bounds, CursorStyle, Decorations, Edges, HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, Point, RenderOnce, ResizeEdge, Size, Styled as _, Window, canvas, div, point, prelude::FluentBuilder as _, px, }; use crate::ActiveTheme; #[cfg(not(target_os = "linux"))] pub(crate) const SHADOW_SIZE: Pixels = px(0.0); #[cfg(target_os = "linux")] pub(crate) const SHADOW_SIZE: Pixels = px(12.0); const BORDER_SIZE: Pixels = px(1.0); pub(crate) const BORDER_RADIUS: Pixels = px(0.0); /// Create a new window border. pub fn window_border() -> WindowBorder { WindowBorder::new() } /// Window border use to render a custom window border and shadow for Linux. #[derive(IntoElement)] pub struct WindowBorder { shadow_size: Pixels, children: Vec, } impl Default for WindowBorder { fn default() -> Self { Self { shadow_size: SHADOW_SIZE, children: Vec::new(), } } } impl WindowBorder { pub fn new() -> Self { Self::default() } /// Set the shadow size for typical Linux client-side decorations. /// /// Default: [`SHADOW_SIZE`] pub fn shadow_size(mut self, size: impl Into) -> Self { self.shadow_size = size.into(); self } } /// Get the window paddings. pub fn window_paddings(window: &Window) -> Edges { let shadow_size = window.client_inset().unwrap_or(SHADOW_SIZE); match window.window_decorations() { Decorations::Server => Edges::all(px(0.0)), Decorations::Client { tiling } => { let mut paddings = Edges::all(shadow_size); if tiling.top { paddings.top = px(0.0); } if tiling.bottom { paddings.bottom = px(0.0); } if tiling.left { paddings.left = px(0.0); } if tiling.right { paddings.right = px(0.0); } paddings } } } impl ParentElement for WindowBorder { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } impl RenderOnce for WindowBorder { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let decorations = window.window_decorations(); let shadow_size = match decorations { Decorations::Client { tiling } if tiling.top && tiling.bottom && tiling.left && tiling.right => { px(0.0) } _ => self.shadow_size, }; window.set_client_inset(shadow_size); div() .id("window-backdrop") .bg(gpui::transparent_black()) .map(|div| match decorations { Decorations::Server => div, Decorations::Client { tiling, .. } => div .bg(gpui::transparent_black()) .child( canvas( |_bounds, window, _| { window.insert_hitbox( Bounds::new( point(px(0.0), px(0.0)), window.window_bounds().get_bounds().size, ), HitboxBehavior::Normal, ) }, move |_bounds, hitbox, window, _| { let mouse = window.mouse_position(); let size = window.window_bounds().get_bounds().size; let Decorations::Client { tiling } = window.window_decorations() else { return; }; if tiling.top && tiling.bottom && tiling.left && tiling.right { return; } let Some(edge) = resize_edge(mouse, shadow_size, size) else { return; }; window.set_cursor_style( match edge { ResizeEdge::Top | ResizeEdge::Bottom => { CursorStyle::ResizeUpDown } ResizeEdge::Left | ResizeEdge::Right => { CursorStyle::ResizeLeftRight } ResizeEdge::TopLeft | ResizeEdge::BottomRight => { CursorStyle::ResizeUpLeftDownRight } ResizeEdge::TopRight | ResizeEdge::BottomLeft => { CursorStyle::ResizeUpRightDownLeft } }, &hitbox, ); }, ) .size_full() .absolute(), ) .when(!(tiling.top || tiling.right), |div| { div.rounded_tr(BORDER_RADIUS) }) .when(!(tiling.top || tiling.left), |div| { div.rounded_tl(BORDER_RADIUS) }) .when(!tiling.top, |div| div.pt(shadow_size)) .when(!tiling.bottom, |div| div.pb(shadow_size)) .when(!tiling.left, |div| div.pl(shadow_size)) .when(!tiling.right, |div| div.pr(shadow_size)) .on_mouse_down(MouseButton::Left, move |_, window, _| { let Decorations::Client { tiling } = window.window_decorations() else { return; }; if tiling.top && tiling.bottom && tiling.left && tiling.right { return; } let size = window.window_bounds().get_bounds().size; let pos = window.mouse_position(); match resize_edge(pos, shadow_size, size) { Some(edge) => window.start_window_resize(edge), None => {} }; }), }) .size_full() .child( div() .cursor(CursorStyle::default()) .map(|div| match decorations { Decorations::Server => div, Decorations::Client { tiling } => div .when(!(tiling.top || tiling.right), |div| { div.rounded_tr(BORDER_RADIUS) }) .when(!(tiling.top || tiling.left), |div| { div.rounded_tl(BORDER_RADIUS) }) .border_color(cx.theme().window_border) .when(!tiling.top, |div| div.border_t(BORDER_SIZE)) .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE)) .when(!tiling.left, |div| div.border_l(BORDER_SIZE)) .when(!tiling.right, |div| div.border_r(BORDER_SIZE)) .when(!tiling.is_tiled(), |div| { div.shadow(vec![gpui::BoxShadow { color: Hsla { h: 0., s: 0., l: 0., a: 0.3, }, blur_radius: shadow_size / 2., spread_radius: px(0.), offset: point(px(0.0), px(0.0)), }]) }), }) .on_mouse_move(|_e, _, cx| { cx.stop_propagation(); }) .bg(gpui::transparent_black()) .size_full() .children(self.children), ) } } fn resize_edge(pos: Point, shadow_size: Pixels, size: Size) -> Option { let edge = if pos.y < shadow_size && pos.x < shadow_size { ResizeEdge::TopLeft } else if pos.y < shadow_size && pos.x > size.width - shadow_size { ResizeEdge::TopRight } else if pos.y < shadow_size { ResizeEdge::Top } else if pos.y > size.height - shadow_size && pos.x < shadow_size { ResizeEdge::BottomLeft } else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size { ResizeEdge::BottomRight } else if pos.y > size.height - shadow_size { ResizeEdge::Bottom } else if pos.x < shadow_size { ResizeEdge::Left } else if pos.x > size.width - shadow_size { ResizeEdge::Right } else { return None; }; Some(edge) } ================================================ FILE: crates/ui/src/window_ext.rs ================================================ use crate::{ Placement, Root, dialog::{AlertDialog, Dialog}, input::InputState, notification::Notification, sheet::Sheet, }; use gpui::{App, Entity, Window}; use std::rc::Rc; /// Extension trait for [`Window`] to add dialog, sheet .. functionality. pub trait WindowExt: Sized { /// Opens a Sheet at right placement. fn open_sheet(&mut self, cx: &mut App, build: F) where F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static; /// Opens a Sheet at the given placement. fn open_sheet_at(&mut self, placement: Placement, cx: &mut App, build: F) where F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static; /// Return true, if there is an active Sheet. fn has_active_sheet(&mut self, cx: &mut App) -> bool; /// Closes the active Sheet. fn close_sheet(&mut self, cx: &mut App); /// Opens a Dialog. fn open_dialog(&mut self, cx: &mut App, build: F) where F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static; /// Opens an AlertDialog. /// /// This is a convenience method for opening an alert dialog with opinionated defaults. /// The footer buttons are center-aligned and include an icon based on the variant. /// /// # Examples /// /// ```ignore /// use gpui_component::{AlertDialog, alert::AlertVariant}; /// /// window.open_alert_dialog(cx, |alert, _, _| { /// alert.warning() /// .title("Unsaved Changes") /// .description("You have unsaved changes. Are you sure you want to leave?") /// .show_cancel(true) /// }); /// ``` fn open_alert_dialog(&mut self, cx: &mut App, build: F) where F: Fn(AlertDialog, &mut Window, &mut App) -> AlertDialog + 'static; /// Return true, if there is an active Dialog. fn has_active_dialog(&mut self, cx: &mut App) -> bool; /// Closes the last active Dialog. fn close_dialog(&mut self, cx: &mut App); /// Closes all active Dialogs. fn close_all_dialogs(&mut self, cx: &mut App); /// Pushes a notification to the notification list. fn push_notification(&mut self, note: impl Into, cx: &mut App); /// Removes the notification with the given id. fn remove_notification(&mut self, cx: &mut App); /// Clears all notifications. fn clear_notifications(&mut self, cx: &mut App); /// Returns number of notifications. fn notifications(&mut self, cx: &mut App) -> Rc>>; /// Return current focused Input entity. fn focused_input(&mut self, cx: &mut App) -> Option>; /// Returns true if there is a focused Input entity. fn has_focused_input(&mut self, cx: &mut App) -> bool; } impl WindowExt for Window { #[inline] fn open_sheet(&mut self, cx: &mut App, build: F) where F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static, { self.open_sheet_at(Placement::Right, cx, build) } #[inline] fn open_sheet_at(&mut self, placement: Placement, cx: &mut App, build: F) where F: Fn(Sheet, &mut Window, &mut App) -> Sheet + 'static, { Root::update(self, cx, move |root, window, cx| { root.open_sheet_at(placement, build, window, cx); }) } #[inline] fn has_active_sheet(&mut self, cx: &mut App) -> bool { Root::read(self, cx).active_sheet.is_some() } #[inline] fn close_sheet(&mut self, cx: &mut App) { Root::update(self, cx, |root, window, cx| { root.close_sheet(window, cx); }) } #[inline] fn open_dialog(&mut self, cx: &mut App, build: F) where F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static, { Root::update(self, cx, move |root, window, cx| { root.open_dialog(build, window, cx); }) } #[inline] fn open_alert_dialog(&mut self, cx: &mut App, build: F) where F: Fn(AlertDialog, &mut Window, &mut App) -> AlertDialog + 'static, { self.open_dialog(cx, move |_, window, cx| { build(AlertDialog::new(cx), window, cx).into_dialog(window, cx) }) } #[inline] fn has_active_dialog(&mut self, cx: &mut App) -> bool { Root::read(self, cx).active_dialogs.len() > 0 } #[inline] fn close_dialog(&mut self, cx: &mut App) { Root::update(self, cx, |root, window, cx| { root.close_dialog(window, cx); }) } #[inline] fn close_all_dialogs(&mut self, cx: &mut App) { Root::update(self, cx, |root, window, cx| { root.close_all_dialogs(window, cx); }) } #[inline] fn push_notification(&mut self, note: impl Into, cx: &mut App) { let note = note.into(); Root::update(self, cx, |root, window, cx| { root.push_notification(note, window, cx); }) } #[inline] fn remove_notification(&mut self, cx: &mut App) { Root::update(self, cx, |root, window, cx| { root.remove_notification::(window, cx); }) } #[inline] fn clear_notifications(&mut self, cx: &mut App) { Root::update(self, cx, |root, window, cx| { root.clear_notifications(window, cx); }) } #[inline] fn notifications(&mut self, cx: &mut App) -> Rc>> { Rc::new(Root::read(self, cx).notification.read(cx).notifications()) } #[inline] fn has_focused_input(&mut self, cx: &mut App) -> bool { Root::read(self, cx).focused_input.is_some() } #[inline] fn focused_input(&mut self, cx: &mut App) -> Option> { Root::read(self, cx).focused_input.clone() } } ================================================ FILE: crates/webview/Cargo.toml ================================================ [package] name = "gpui-wry" description = "WebView support for GPUI, based on Wry." keywords = ["webview", "gpui","wry"] license = "Apache-2.0" documentation = "https://docs.rs/gpui-component" homepage = "https://longbridge.github.io/gpui-component" repository = "https://github.com/longbridge/gpui-component/tree/main/crates/webview" readme = "README.md" edition.workspace = true publish = true version = "0.5.0" [features] inspector = ["wry/devtools"] [lib] doctest = false [dependencies] anyhow.workspace = true gpui.workspace = true wry = { version = "0.53.3", package = "lb-wry" } ================================================ FILE: crates/webview/README.md ================================================ # Wry for GPUI A webview supports for GPUI, based on [Wry](https://github.com/tauri-apps/wry). This still a experimental with limited features, please file issues for any bugs or missing features. - The WebView will render on top of the GPUI window, any GPUI elements behind the WebView bounds will be covered. - Only supports macOS and Windows currently. So, we recommend using the webview in a separate window or in a Popup layer. ## Run Example In the root of the repository, run: ``` cargo run -p webview ``` ## License Apache-2.0 ================================================ FILE: crates/webview/src/lib.rs ================================================ use std::{ops::Deref, rc::Rc}; use wry::{ Rect, dpi::{self, LogicalSize}, }; use gpui::{ App, Bounds, ContentMask, DismissEvent, Element, ElementId, Entity, EventEmitter, FocusHandle, Focusable, GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId, MouseDownEvent, ParentElement as _, Pixels, Render, Size, Style, Styled as _, Window, canvas, div, }; /// A webview based on wry WebView. /// /// [experimental] pub struct WebView { focus_handle: FocusHandle, webview: Rc, visible: bool, bounds: Bounds, } impl Drop for WebView { fn drop(&mut self) { self.hide(); } } impl WebView { /// Create a new WebView from a wry WebView. pub fn new(webview: wry::WebView, _: &mut Window, cx: &mut App) -> Self { let _ = webview.set_bounds(Rect::default()); Self { focus_handle: cx.focus_handle(), visible: true, bounds: Bounds::default(), webview: Rc::new(webview), } } /// Show the webview. pub fn show(&mut self) { let _ = self.webview.set_visible(true); self.visible = true; } /// Hide the webview. pub fn hide(&mut self) { _ = self.webview.focus_parent(); _ = self.webview.set_visible(false); self.visible = false; } /// Get whether the webview is visible. pub fn visible(&self) -> bool { self.visible } /// Get the current bounds of the webview. pub fn bounds(&self) -> Bounds { self.bounds } /// Go back in the webview history. pub fn back(&mut self) -> anyhow::Result<()> { Ok(self.webview.evaluate_script("history.back();")?) } /// Load a URL in the webview. pub fn load_url(&mut self, url: &str) { let _ = self.webview.load_url(url); } /// Get the raw wry webview. pub fn raw(&self) -> &wry::WebView { &self.webview } } impl Deref for WebView { type Target = wry::WebView; fn deref(&self) -> &Self::Target { &self.webview } } impl Focusable for WebView { fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { self.focus_handle.clone() } } impl EventEmitter for WebView {} impl Render for WebView { fn render( &mut self, window: &mut gpui::Window, cx: &mut gpui::Context, ) -> impl IntoElement { let view = cx.entity().clone(); div() .track_focus(&self.focus_handle) .size_full() .child({ let view = cx.entity().clone(); canvas( move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds), |_, _, _, _| {}, ) .absolute() .size_full() }) .child(WebViewElement::new(self.webview.clone(), view, window, cx)) } } /// A webview element can display a wry webview. pub struct WebViewElement { parent: Entity, view: Rc, } impl WebViewElement { /// Create a new webview element from a wry WebView. pub fn new( view: Rc, parent: Entity, _window: &mut Window, _cx: &mut App, ) -> Self { Self { view, parent } } } impl IntoElement for WebViewElement { type Element = WebViewElement; fn into_element(self) -> Self::Element { self } } impl Element for WebViewElement { type RequestLayoutState = (); type PrepaintState = Option; fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, _: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let style = Style { size: Size::full(), flex_shrink: 1., ..Default::default() }; // If the parent view is no longer visible, we don't need to layout the webview let id = window.request_layout(style, [], cx); (id, ()) } fn prepaint( &mut self, _: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { if !self.parent.read(cx).visible() { return None; } let _ = self.view.set_bounds(Rect { size: dpi::Size::Logical(LogicalSize { width: bounds.size.width.into(), height: bounds.size.height.into(), }), position: dpi::Position::Logical(dpi::LogicalPosition::new( bounds.origin.x.into(), bounds.origin.y.into(), )), }); // Create a hitbox to handle mouse event Some(window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal)) } fn paint( &mut self, _: Option<&GlobalElementId>, _: Option<&gpui::InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, window: &mut Window, _: &mut App, ) { let bounds = hitbox.clone().map(|h| h.bounds).unwrap_or(bounds); window.with_content_mask(Some(ContentMask { bounds }), |window| { let webview = self.view.clone(); window.on_mouse_event(move |event: &MouseDownEvent, _, _, _| { if !bounds.contains(&event.position) { // Click white space to blur the input focus let _ = webview.focus_parent(); } }); }); } } ================================================ FILE: docs/.gitignore ================================================ .vitepress/cache .vitepress/dist bun.lockb ================================================ FILE: docs/.vitepress/config.mts ================================================ import { defineConfig } from "vitepress"; import type { UserConfig } from "vitepress"; import { generateSidebar } from "vitepress-sidebar"; import llmstxt from "vitepress-plugin-llms"; import tailwindcss from "@tailwindcss/vite"; import { lightTheme, darkTheme } from "./language"; import { ViteToml } from "vite-plugin-toml"; /** * https://github.com/jooy2/vitepress-sidebar */ const sidebar = generateSidebar([ { scanStartPath: "/docs/", rootGroupText: "Introduction", collapsed: false, useTitleFromFrontmatter: true, useTitleFromFileHeading: true, sortMenusByFrontmatterOrder: true, includeRootIndexFile: false, }, ]); // https://vitepress.dev/reference/site-config const config: UserConfig = { title: "GPUI Component", base: "/gpui-component/", description: "Rust GUI components for building fantastic cross-platform desktop application by using GPUI.", cleanUrls: true, head: [ [ "link", { rel: "icon", href: "/gpui-component/logo.svg", media: "(prefers-color-scheme: light)", }, ], [ "link", { rel: "icon", href: "/gpui-component/logo-dark.svg", media: "(prefers-color-scheme: dark)", }, ], ], vite: { plugins: [llmstxt(), tailwindcss(), ViteToml()], }, themeConfig: { logo: { light: "/logo.svg", dark: "/logo-dark.svg", }, footer: { message: `GPUI Component is an open source project under the Apache-2.0 License, developed by Longbridge.`, copyright: ` GPUI | Gallery | Contributors | Skills | llms-full.txt | Report Bug | Discussion
Icon resources are used Lucide, Isocons. `, }, // https://vitepress.dev/reference/default-theme-config nav: [ { text: "Home", link: "/" }, { text: "Getting Started", link: "/docs/getting-started" }, { text: "Components", link: "/docs/components" }, { text: "Gallery", link: "/gallery/", target: "_blank" }, { text: "API Doc", link: "https://docs.rs/gpui-component" }, { text: "Resources", items: [ { text: "Contributors", link: "/contributors", }, { text: "Releases", link: "https://github.com/longbridge/gpui-component/releases", }, { text: "Issues", link: "https://github.com/longbridge/gpui-component/issues", }, { text: "Discussion", link: "https://github.com/longbridge/gpui-component/discussions", }, ], }, { component: "GitHubStar", }, ], sidebar: sidebar as any, socialLinks: null, editLink: { pattern: "https://github.com/longbridge/gpui-component/edit/main/docs/:path", }, search: { provider: "local", }, }, markdown: { math: true, defaultHighlightLang: "rs", theme: { light: lightTheme, dark: darkTheme, }, }, }; export default defineConfig(config); ================================================ FILE: docs/.vitepress/language.ts ================================================ import { readFileSync } from "fs"; const lightTheme = JSON.parse(readFileSync("src/light.theme.json").toString()); const darkTheme = JSON.parse(readFileSync("src/dark.theme.json").toString()); export { darkTheme, lightTheme }; ================================================ FILE: docs/.vitepress/theme/components/GitHubStar.vue ================================================ ================================================ FILE: docs/.vitepress/theme/index.ts ================================================ // https://vitepress.dev/guide/custom-theme import { h } from "vue"; import type { Theme } from "vitepress"; import DefaultTheme from "vitepress/theme"; import "./style.css"; import GitHubStar from "./components/GitHubStar.vue"; import config from "../../../crates/ui/Cargo.toml"; /** @type {import('vitepress').Theme} */ export default { extends: DefaultTheme, Layout: () => { return h(DefaultTheme.Layout, null, { // https://vitepress.dev/guide/extending-default-theme#layout-slots }); }, enhanceApp({ app, router, siteData }) { // ... app.component("GitHubStar", GitHubStar); app.config.globalProperties.GPUI_VERSION = "0.2.2"; app.config.globalProperties.VERSION = config.package.version; }, } satisfies Theme; ================================================ FILE: docs/.vitepress/theme/style.css ================================================ /** * Customize default theme styling by overriding CSS variables: * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */ @import "tailwindcss"; @custom-variant dark (&:where(.dark, .dark *)); :root { --radius: 0.875rem; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); --primary: oklch(0.205 0 0); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); --muted: oklch(0.97 0 0); --muted-foreground: oklch(0.556 0 0); --accent: oklch(0.97 0 0); --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); --ring: oklch(0.708 0 0); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); --sidebar-primary: oklch(0.205 0 0); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.97 0 0); --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); --vp-c-bg: var(--background); --vp-c-bg-alt: var(--secondary); --vp-c-bg-elv: var(--popover); --vp-c-bg-soft: var(--secondary); --vp-c-text-1: var(--foreground); --vp-c-brand-1: var(--primary); --vp-c-default-1: var(--secondary); --vp-c-default-2: var(--muted); --vp-nav-bg-color: var(--background); --vp-custom-block-tip-border: var(--vp-c-default-3); --vp-custom-block-tip-text: var(--vp-c-text-1); --vp-custom-block-tip-bg: transparent; --vp-custom-block-tip-code-bg: var(--vp-c-default-soft); --vp-input-switch-bg-color: var(--secondary); --vp-input-border-color: var(--input); --vp-c-divider: var(--border); } .dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); --card: oklch(0.205 0 0); --card-foreground: oklch(0.985 0 0); --popover: oklch(0.269 0 0); --popover-foreground: oklch(0.985 0 0); --primary: oklch(0.922 0 0); --primary-foreground: oklch(0.205 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); --muted-foreground: oklch(0.708 0 0); --accent: oklch(0.371 0 0); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); --sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.269 0 0); --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.439 0 0); } #app { .VPMenu { @apply p-1 rounded-md bg-(--popover); .link { @apply text-sm py-1; } } .DocSearch-Button { @apply rounded-full py-0 px-2 h-8 lg:w-56 xl:w-64; .DocSearch-Button-Keys { @apply rounded-full p-1; } } .VPNav { @apply bg-(--background) border-b border-(--border); .divider { @apply hidden; } } .VPSwitch { @apply border-(--secondary) bg-(--secondary) hover:border-(--border); } .VPFooter { @apply border-dashed; } .VPSidebar { @apply border-r border-(--border) bg-(--background); } .VPNavBarTitle { a.title { border-bottom: 0 !important; } } .prev-next { @apply border-0 pt-0; .pager-link { @apply border-dashed; } } } .VPContent.has-sidebar { .VPDoc { @apply pt-32; background: url("/components.svg") no-repeat; background-position: top -45px right 320px; } } .vp-doc { h2 { @apply border-t-0 mt-10 pt-0 pb-3 border-b border-dashed border-(--border); .header-anchor { @apply top-0; } } [class*="language-"] pre { @apply rounded-md py-5 text-base font-mono; } [class*="language-"] pre code { @apply px-5; } [class*="language-"] > button.copy { /*rtl:ignore*/ direction: ltr; position: absolute; top: 12px; /*rtl:ignore*/ z-index: 3; border: 1px solid var(--vp-code-copy-code-border-color); border-radius: 4px; background-color: var(--vp-code-copy-code-bg); opacity: 0; cursor: pointer; background-image: var(--vp-icon-copy); background-position: 50%; background-repeat: no-repeat; transition: border-color 0.25s, background-color 0.25s, opacity 0.25s; width: 24px; height: 24px; background-size: 14px; &:hover, &.copy.copied { border-color: var(--vp-code-copy-code-hover-border-color); background-color: var(--vp-code-copy-code-hover-bg); } &:hover.copied::before, &.copied::before { height: 24px; line-height: 24px; padding: 0 8px; } } [class*="language-"] > button.copy { right: 12px; } } ================================================ FILE: docs/README.md ================================================ # gpui-component-docs To install dependencies: ```bash bun install ``` To run: ```bash bun run dev ``` This project was created using `bun init` in bun v1.2.23. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. ================================================ FILE: docs/contributors.md ================================================ --- layout: home --- ================================================ FILE: docs/contributors.vue ================================================ ================================================ FILE: docs/data/contributors.data.js ================================================ const IGNORE_LOGINS = ["dependabot[bot]", "copilot"]; const API_URL = "https://api.github.com/repos/longbridge/gpui-component/contributors"; export default { async load() { return await fetch(API_URL) .then((res) => res.json()) .then((items) => { let filtered = items.filter( (item) => !IGNORE_LOGINS.includes(item.login.toLowerCase()), ); return filtered.slice(0, 24); }); }, }; ================================================ FILE: docs/data/repo.data.js ================================================ const API_URL = "https://api.github.com/repos/longbridge/gpui-component"; export default { async load() { return await fetch(API_URL).then((res) => res.json()); }, }; ================================================ FILE: docs/data/skills.data.js ================================================ import { readdirSync, readFileSync } from "fs"; import { join } from "path"; import { fileURLToPath } from "url"; import { dirname } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); function parseFrontmatter(content) { const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/; const match = content.match(frontmatterRegex); if (!match) { return { frontmatter: {}, content: content.trim() }; } const frontmatterText = match[1]; const body = match[2]; const frontmatter = {}; frontmatterText.split('\n').forEach(line => { const colonIndex = line.indexOf(':'); if (colonIndex > 0) { const key = line.substring(0, colonIndex).trim(); let value = line.substring(colonIndex + 1).trim(); // Remove quotes if present if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } frontmatter[key] = value; } }); return { frontmatter, content: body.trim() }; } export default { async load() { const skillsDir = join(__dirname, "../../.claude/skills"); const skills = []; try { const skillDirs = readdirSync(skillsDir, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name); for (const skillDir of skillDirs) { const skillPath = join(skillsDir, skillDir, "SKILL.md"); try { const content = readFileSync(skillPath, "utf-8"); const { frontmatter, content: body } = parseFrontmatter(content); skills.push({ id: skillDir, name: frontmatter.name || skillDir, description: frontmatter.description || "", content: body, path: skillPath, }); } catch (err) { console.warn(`Failed to read skill ${skillDir}:`, err.message); } } // Sort skills by name skills.sort((a, b) => a.name.localeCompare(b.name)); return skills; } catch (err) { console.error("Failed to load skills:", err); return []; } }, }; ================================================ FILE: docs/docs/assets.md ================================================ --- title: Icons & Assets order: -4 --- # Icons & Assets The [IconName] and [Icon] in GPUI Component provide a comprehensive set of icons and assets that can be easily integrated into your GPUI applications. But for minimal size applications, **we have not embedded any icon assets by default** in `gpui-component` crate. We split the icon assets into a separate crate [gpui-component-assets] to allow developers to choose whether to include the icon assets in their applications or if you don't need the icons at all, you can build your own assets. ## Use default bundled assets The [gpui-component-assets] crate provides a default bundled assets implementation that includes all the icon files in the `assets/icons` folder. To use the default bundled assets, you need to add the `gpui-component-assets` crate as a dependency in your `Cargo.toml`: ```toml-vue [dependencies] gpui-component = "{{ VERSION }}" gpui-component-assets = "{{ VERSION }}" ``` Then we need call the `with_assets` method when creating the GPUI application to register the asset source: ```rs use gpui::*; use gpui_component_assets::Assets; let app = gpui_platform::application().with_assets(Assets); ``` Now, we can use `IconName` and `Icon` in our application as usual, the all icon assets are loaded from the default bundled assets. Continue [Use the icons](#use-the-icons) section to see how to use the icons in your application. ## Build you own assets You may have a specific set of icons that you want to use in your application, or you may want to reduce the size of your application binary by including only the icons you need. In this case, you can build your own assets by following these steps. The [assets](https://github.com/longbridge/gpui-component/tree/main/crates/assets/assets/) folder in source code contains all the available icons in SVG format, every file is that GPUI Component support, it matched with the [IconName] enum. You can download the SVG files you need from the [assets] folder, or you can use your own SVG files by following the [IconName] naming convention. In GPUI application, we can use the [rust-embed] crate to embed the SVG files into the application binary. And GPUI Application providers an `AssetSource` trait to load the assets. ```rs use anyhow::anyhow; use gpui::*; use gpui_component::{v_flex, IconName, Root}; use rust_embed::RustEmbed; use std::borrow::Cow; /// An asset source that loads assets from the `./assets` folder. #[derive(RustEmbed)] #[folder = "./assets"] #[include = "icons/**/*.svg"] pub struct Assets; impl AssetSource for Assets { fn load(&self, path: &str) -> Result>> { if path.is_empty() { return Ok(None); } Self::get(path) .map(|f| Some(f.data)) .ok_or_else(|| anyhow!("could not find asset at path \"{path}\"")) } fn list(&self, path: &str) -> Result> { Ok(Self::iter() .filter_map(|p| p.starts_with(path).then(|| p.into())) .collect()) } } ``` We need call the `with_assets` method when creating the GPUI application to register the asset source: ```rs fn main() { // Register Assets to GPUI application. let app = gpui_platform::application().with_assets(Assets); app.run(move |cx| { // We must initialize gpui_component before using it. gpui_component::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| Example); // The first level on the window must be Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` ## Use the icons Now we can use the icons in our application: ```rs pub struct Example; impl Render for Example { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .size_full() .items_center() .justify_center() .text_center() .child(IconName::Inbox) .child(IconName::Bot) } } ``` ## Resources - [Lucide Icons](https://lucide.dev/) - The icon set used in GPUI Component is based on the open-source Lucide Icons library, which provides a wide range of customizable SVG icons. [rust-embed]: https://docs.rs/rust-embed/latest/rust_embed/ [IconName]: https://docs.rs/gpui_component/latest/gpui_component/icon/enum.IconName.html [Icon]: https://docs.rs/gpui_component/latest/gpui_component/icon/struct.Icon.html [assets]: https://github.com/longbridge/gpui-component/tree/main/crates/assets/assets/ [gpui-component-assets]: https://crates.io/crates/gpui-component-assets ================================================ FILE: docs/docs/components/accordion.md ================================================ --- title: Accordion description: The accordion uses collapse internally to make it collapsible. --- # Accordion An accordion component that allows users to show and hide sections of content. It uses collapse functionality internally to create collapsible panels. ## Import ```rust use gpui_component::accordion::Accordion; ``` ## Usage ### Basic Accordion ```rust Accordion::new("my-accordion") .item(|item| { item.title("Section 1") .child("Content for section 1") }) .item(|item| { item.title("Section 2") .child("Content for section 2") }) .item(|item| { item.title("Section 3") .child("Content for section 3") }) ``` ### Multiple Open Items By default, only one accordion item can be open at a time. Use `multiple()` to allow multiple items to be open: ```rust Accordion::new("my-accordion") .multiple(true) .item(|item| item.title("Section 1").child("Content 1")) .item(|item| item.title("Section 2").child("Content 2")) ``` ### With Borders ```rust Accordion::new("my-accordion") .bordered(true) .item(|item| item.title("Section 1").child("Content 1")) ``` ### Different Sizes ```rust use gpui_component::{Sizable as _, Size}; Accordion::new("my-accordion") .small() .item(|item| item.title("Small Section").child("Content")) Accordion::new("my-accordion") .large() .item(|item| item.title("Large Section").child("Content")) ``` ### Handle Toggle Events ```rust Accordion::new("my-accordion") .on_toggle_click(|open_indices, window, cx| { println!("Open items: {:?}", open_indices); }) .item(|item| item.title("Section 1").child("Content 1")) ``` ### Disabled State ```rust Accordion::new("my-accordion") .disabled(true) .item(|item| item.title("Disabled Section").child("Content")) ``` ## API Reference - [Accordion] - [AccordionItem] ### Sizing Implements [Sizable] trait: - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size - `xsmall()` - Extra small size ## Examples ### With Custom Icons ```rust Accordion::new("my-accordion") .item(|item| { item.title( h_flex() .gap_2() .child(Icon::new(IconName::Settings)) .child("Settings") ) .child("Settings content here") }) ``` ### Nested Accordions ```rust Accordion::new("outer") .item(|item| { item.title("Parent Section") .child( Accordion::new("inner") .item(|item| item.title("Child 1").child("Content")) .item(|item| item.title("Child 2").child("Content")) ) }) ``` [Accordion]: https://docs.rs/gpui-component/latest/gpui_component/accordion/struct.Accordion.html [AccordionItem]: https://docs.rs/gpui-component/latest/gpui_component/accordion/struct.AccordionItem.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ================================================ FILE: docs/docs/components/alert-dialog.md ================================================ --- title: AlertDialog description: A modal dialog that interrupts the user with important content and expects a response. --- # AlertDialog AlertDialog is a modal dialog component that interrupts the user with important content and expects a response. It is built on top of the [Dialog] component with opinionated defaults and a simplified API. ## Differences from Dialog AlertDialog provides these defaults on top of Dialog: - Not overlay closable by default (can be changed with `overlay_closable(true)`) - No close button by default (can be changed with `close_button(true)`) - Footer buttons are center-aligned (Dialog uses right-alignment) - Simplified API focused on alert and confirmation scenarios ## Import ```rust use gpui_component::dialog::{AlertDialog, DialogAction, DialogClose}; use gpui_component::WindowExt; ``` ## Usage ### Setup Application Root View Like Dialog, you need to set up your application's root view to render the dialog layer. See [Dialog documentation](./dialog.md#setup-application-root-view) for details. ### Basic AlertDialog (Declarative API) Create a fully declarative AlertDialog using `trigger` and `content`: ```rust use gpui_component::dialog::{AlertDialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter}; AlertDialog::new(cx) .trigger( Button::new("show-alert") .outline() .label("Show Alert") ) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Are you absolutely sure?")) .child(DialogDescription::new().child( "This action cannot be undone. \ This will permanently delete your account from our servers." )) ) .child( DialogFooter::new() .child( Button::new("cancel") .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("ok") .primary() .label("Continue") .on_click(|_, window, cx| { window.push_notification("Confirmed", cx); window.close_dialog(cx); }) ) ) }) ``` ### Using DialogAction and DialogClose `DialogAction` and `DialogClose` are wrapper components that simplify button click handling by automatically triggering the appropriate actions: - **DialogClose**: Wraps a button to trigger the `Cancel` action, invoking `on_cancel` callback - **DialogAction**: Wraps a button to trigger the `Confirm` action, invoking `on_ok` callback These components eliminate the need to manually call `window.close_dialog(cx)`: ```rust AlertDialog::new(cx) .trigger(Button::new("show-alert").outline().label("Show Alert")) .on_ok(|_, window, cx| { window.push_notification("You confirmed!", cx); true // Return true to close dialog }) .on_cancel(|_, window, cx| { window.push_notification("You cancelled!", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Confirm Action")) .child(DialogDescription::new().child("Do you want to proceed?")) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("ok").primary().label("Confirm") ) ) ) }) ``` **Benefits:** - No need to manually close the dialog - Automatically connects to `on_ok` and `on_cancel` callbacks - Cleaner, more declarative code - Supports returning `false` from callbacks to prevent closing ### Basic AlertDialog (Imperative API) Open a dialog imperatively using `WindowExt::open_alert_dialog`: ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete File") .description("Are you sure you want to delete this file? This action cannot be undone.") .show_cancel(true) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true // Return true to close dialog }) }) ``` ### Custom Button Props Use `button_props` to customize button text and styles: ```rust use gpui_component::dialog::DialogButtonProps; use gpui_component::button::ButtonVariant; window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete Account") .description("This will permanently delete your account and all associated data.") .button_props( DialogButtonProps::default() .ok_text("Delete") .ok_variant(ButtonVariant::Danger) .cancel_text("Keep") .show_cancel(true) ) .on_ok(|_, window, cx| { window.push_notification("Account deleted", cx); true }) }) ``` ### AlertDialog with Icon Using icon in declarative API: ```rust use gpui_component::{Icon, IconName, ActiveTheme}; AlertDialog::new(cx) .w(px(320.)) .trigger(Button::new("permission").outline().label("Request Permission")) .on_ok(|_, window, cx| { window.push_notification("Permission granted", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .items_center() .child( Icon::new(IconName::TriangleAlert) .size_10() .text_color(cx.theme().warning) ) .child(DialogTitle::new().child("Network Permission Required")) .child(DialogDescription::new().child( "We need your permission to access the network to provide better services." )) ) .child( DialogFooter::new() .v_flex() .child( DialogAction::new().child( Button::new("allow").w_full().primary().label("Allow") ) ) .child( DialogClose::new().child( Button::new("deny").w_full().outline().label("Don't Allow") ) ) ) }) ``` Using icon in imperative API: ```rust window.open_alert_dialog(cx, |alert, _, cx| { alert .title("Warning") .description("This action requires confirmation.") .icon( Icon::new(IconName::AlertTriangle) .size_8() .text_color(cx.theme().warning) ) }) ``` ### Destructive Action Confirmation ```rust AlertDialog::new(cx) .trigger( Button::new("delete-account") .outline() .danger() .label("Delete Account") ) .on_ok(|_, window, cx| { window.push_notification("Account deletion initiated", cx); true }) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Delete Account")) .child(DialogDescription::new().child( "This will permanently delete your account \ and all associated data. This action cannot be undone." )) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").flex_1().outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("delete") .flex_1() .outline() .danger() .label("Delete Forever") ) ) ) }) ``` ### Custom Width ```rust AlertDialog::new(cx) .width(px(500.)) .trigger(Button::new("custom-width").label("Custom Width")) .content(|content, _, _| { // ... dialog content }) ``` ### Controlling Dialog Close Behavior #### Allow Overlay Click to Close ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Notice") .description("Click outside this dialog or press ESC to close it.") .overlay_closable(true) }) ``` #### Disable Keyboard ESC to Close ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Important Notice") .description("Please read this carefully before proceeding.") .keyboard(false) }) ``` #### Show Close Button ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Information") .description("Some information...") .close_button(true) }) ``` ### Prevent Dialog from Closing Return `false` from `on_ok` or `on_cancel` callbacks to prevent the dialog from closing: ```rust use gpui_component::dialog::DialogButtonProps; window.open_alert_dialog(cx, |alert, _, _| { alert .title("Processing") .description("A process is running. Click Continue to stop it or Cancel to keep waiting.") .button_props( DialogButtonProps::default() .ok_text("Continue") .show_cancel(true) ) .on_ok(|_, window, cx| { // Return false to prevent closing window.push_notification("Cannot close: Process still running", cx); false }) .on_cancel(|_, window, cx| { window.push_notification("Waiting...", cx); false }) }) ``` ### Dialog Close Callback Use `on_close` to execute actions after the dialog closes (called after `on_ok` or `on_cancel`): ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Confirm") .description("Are you sure?") .on_close(|_, window, cx| { window.push_notification("Dialog closed", cx); }) }) ``` ## API Reference ### AlertDialog | Method | Description | | ------------------------ | ------------------------------------------------------------- | | `new(cx)` | Create a new AlertDialog | | `trigger(element)` | Set trigger element that opens the dialog when clicked | | `content(builder)` | Set dialog content using a builder function (declarative API) | | `title(title)` | Set dialog title (imperative API) | | `description(desc)` | Set dialog description (imperative API) | | `icon(icon)` | Set dialog icon (imperative API) | | `button_props(props)` | Set button properties (text, style, visibility) | | `show_cancel(bool)` | Show/hide cancel button, default `false` | | `width(px)` | Set dialog width, default `420px` | | `overlay_closable(bool)` | Allow clicking overlay to close, default `false` | | `close_button(bool)` | Show/hide close button, default `false` | | `keyboard(bool)` | Support ESC key to close, default `true` | | `on_ok(callback)` | Set OK button callback, return `true` to close dialog | | `on_cancel(callback)` | Set cancel button callback, return `true` to close dialog | | `on_close(callback)` | Set callback after dialog closes | ### DialogButtonProps | Method | Description | | ------------------------- | ---------------------------------------- | | `ok_text(text)` | Set OK button text, default "OK" | | `cancel_text(text)` | Set cancel button text, default "Cancel" | | `ok_variant(variant)` | Set OK button variant | | `cancel_variant(variant)` | Set cancel button variant | | `show_cancel(bool)` | Show/hide cancel button | | `on_ok(callback)` | Set OK callback | | `on_cancel(callback)` | Set cancel callback | ### DialogAction A wrapper component that automatically triggers the `Confirm` action when its child element is clicked. This invokes the `on_ok` callback set on the AlertDialog. **Usage:** ```rust DialogAction::new().child( Button::new("ok").primary().label("Confirm") ) ``` **Behavior:** - Dispatches `Confirm` action on click - Invokes the `on_ok` callback - Dialog closes if callback returns `true` - Dialog stays open if callback returns `false` ### DialogClose A wrapper component that automatically triggers the `Cancel` action when its child element is clicked. This invokes the `on_cancel` callback set on the AlertDialog. **Usage:** ```rust DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ``` **Behavior:** - Dispatches `Cancel` action on click - Invokes the `on_cancel` callback - Dialog closes if callback returns `true` (or if no callback is set) - Dialog stays open if callback returns `false` ## Examples ### Delete Confirmation Using imperative API with button props: ```rust Button::new("delete") .danger() .label("Delete") .on_click(|_, window, cx| { window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete File?") .description("This action cannot be undone.") .button_props( DialogButtonProps::default() .ok_text("Delete") .ok_variant(ButtonVariant::Danger) .show_cancel(true) ) .on_ok(|_, window, cx| { // Perform delete operation window.push_notification("File deleted", cx); true }) }); }) ``` Or using declarative API with DialogAction/DialogClose: ```rust AlertDialog::new(cx) .trigger(Button::new("delete").danger().label("Delete")) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Delete File?")) .child(DialogDescription::new().child("This action cannot be undone.")) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("delete-confirm").danger().label("Delete") ) ) ) }) ``` ### Session Timeout ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .content(|content, _, _| { content .child( DialogHeader::new() .items_center() .child(DialogTitle::new().child("Session Expired")) .child(DialogDescription::new().child( "Your session has expired due to inactivity. \ Please log in again to continue." )) ) .child( DialogFooter::new() .child( Button::new("sign-in") .label("Sign in") .primary() .flex_1() .on_click(|_, window, cx| { window.push_notification("Redirecting to login...", cx); window.close_dialog(cx); }) ) ) }) }) ``` ### Update Available ```rust AlertDialog::new(cx) .trigger(Button::new("update").outline().label("Update Available")) .on_cancel(|_, window, cx| { window.push_notification("Update postponed", cx); true }) .on_ok(|_, window, cx| { window.push_notification("Starting update...", cx); true }) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Update Available")) .child(DialogDescription::new().child( "A new version (v2.0.0) is available. \ This update includes new features and bug fixes." )) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("later").flex_1().outline().label("Later") ) ) .child( DialogAction::new().child( Button::new("update-now").flex_1().primary().label("Update Now") ) ) ) }) ``` ## Best Practices 1. **Choose the Right API**: Use imperative API (`open_alert_dialog`) for simple confirmations; use declarative API (`trigger` + `content`) for complex layouts or integration with other components 2. **Use DialogAction and DialogClose**: Prefer wrapping buttons with `DialogAction` and `DialogClose` over manual `window.close_dialog()` calls for cleaner, more declarative code 3. **Clarify Intent**: Use appropriate button variants (e.g., `ButtonVariant::Danger` for delete operations) to communicate the importance of actions 4. **Provide Clear Descriptions**: Ensure users understand the consequences of their actions, especially for destructive operations 5. **Use Icons Wisely**: Icons can enhance attention for warnings and errors, but use them appropriately 6. **Prevent Closing Carefully**: Only prevent dialog closing when user confirmation is truly necessary (e.g., a process is running) 7. **Maintain Consistency**: Keep dialog button order and styles consistent throughout your application ## Related Components - [Dialog] - More flexible dialog component - [DialogHeader] - Dialog header component - [DialogTitle] - Dialog title component - [DialogDescription] - Dialog description component - [DialogFooter] - Dialog footer component - [DialogAction] - Wrapper component for confirm/OK buttons - [DialogClose] - Wrapper component for cancel/close buttons [AlertDialog]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.AlertDialog.html [Dialog]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.Dialog.html [DialogHeader]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogHeader.html [DialogTitle]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogTitle.html [DialogDescription]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogDescription.html [DialogFooter]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogFooter.html [DialogAction]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogAction.html [DialogClose]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogClose.html ================================================ FILE: docs/docs/components/alert.md ================================================ --- title: Alert description: Displays a callout for user attention. --- # Alert A versatile alert component for displaying important messages to users. Supports multiple variants (info, success, warning, error), custom icons, optional titles, closable functionality, and banner mode. Perfect for notifications, status messages, and user feedback. ## Import ```rust use gpui_component::alert::Alert; ``` ## Usage ### Basic Alert ```rust Alert::new("alert-id", "This is a basic alert message.") ``` ### Alert with Title ```rust Alert::new("alert-with-title", "Your changes have been saved successfully.") .title("Success!") ``` ### Alert Variants ```rust // Info alert (blue) Alert::info("info-alert", "This is an informational message.") .title("Information") // Success alert (green) Alert::success("success-alert", "Your operation completed successfully.") .title("Success!") // Warning alert (yellow/orange) Alert::warning("warning-alert", "Please review your settings before proceeding.") .title("Warning") // Error alert (red) Alert::error("error-alert", "An error occurred while processing your request.") .title("Error") ``` ### Alert Sizes ```rust use gpui_component::{alert::Alert, Sizable as _}; Alert::info("alert", "Message content") .xsmall() .title("XSmall Alert") Alert::info("alert", "Message content") .small() .title("Small Alert") Alert::info("alert", "Message content") .title("Medium Alert") Alert::info("alert", "Message content") .large() .title("Large Alert") ``` ### Closable Alerts When you add an `on_close` handler, a close button appears on the alert: ```rust Alert::info("closable-alert", "This alert can be dismissed.") .title("Dismissible") .on_close(|_event, _window, _cx| { println!("Alert was closed"); // Handle alert dismissal }) ``` ### Banner Mode Banner alerts take full width and don't display titles: ```rust Alert::info("banner-alert", "This is a banner alert that spans the full width.") .banner() Alert::success("banner-success", "Operation completed successfully!") .banner() Alert::warning("banner-warning", "System maintenance scheduled for tonight.") .banner() Alert::error("banner-error", "Service temporarily unavailable.") .banner() ``` ### Custom Icons ```rust use gpui_component::IconName; Alert::new("custom-icon", "Meeting scheduled for tomorrow at 3 PM.") .title("Calendar Reminder") .icon(IconName::Calendar) ``` ### With Markdown Content We can use `TextView` to render formatted (Markdown or HTML) text within the alert, for displaying lists, bold text, links, etc. ```rust use gpui_component::text::markdown; Alert::error( "error-with-markdown", markdown( "Please verify your billing information and try again.\n\ - Check your card details\n\ - Ensure sufficient funds\n\ - Verify billing address" ), ) .title("Payment Failed") ``` ### Conditional Visibility ```rust Alert::info("conditional-alert", "This alert may be hidden.") .title("Conditional") .visible(should_show_alert) // boolean condition ``` ## API Reference - [Alert] ## Examples ### Form Validation Errors ```rust Alert::error( "validation-error", "Please correct the following errors before submitting:\n\ - Email address is required\n\ - Password must be at least 8 characters\n\ - Terms of service must be accepted" ) .title("Validation Failed") ``` ### Success Notification ```rust Alert::success("save-success", "Your profile has been updated successfully.") .title("Changes Saved") .on_close(|_, _, _| { // Auto-dismiss after showing }) ``` ### System Status Banner ```rust Alert::warning( "maintenance-banner", "Scheduled maintenance will occur tonight from 2:00 AM to 4:00 AM EST. \ Some services may be temporarily unavailable." ) .banner() .large() ``` ### Interactive Alert with Custom Action ```rust Alert::info("update-available", "A new version of the application is available.") .title("Update Available") .icon(IconName::Download) .on_close(cx.listener(|this, _, _, cx| { // Handle update or dismiss this.handle_update_notification(cx); })) ``` ### Multi-line Content with Formatting ```rust use gpui_component::text::markdown; Alert::warning( "security-alert", markdown( "**Security Notice**: Unusual activity detected on your account.\n\n\ Recent activity:\n\ - Login from new device (Chrome on Windows)\n\ - Location: San Francisco, CA\n\ - Time: Today at 2:30 PM\n\n\ If this wasn't you, please [change your password](/) immediately." ) ) .title("Security Alert") .icon(IconName::Shield) ``` [Alert]: https://docs.rs/gpui-component/latest/gpui_component/alert/struct.Alert.html ================================================ FILE: docs/docs/components/avatar.md ================================================ --- title: Avatar description: Displays a user avatar image with fallback options. --- # Avatar The Avatar component displays user profile images with intelligent fallbacks. When no image is provided, it shows user initials or a placeholder icon. The component supports various sizes and can be grouped together for team displays. ## Import ```rust use gpui_component::avatar::{Avatar, AvatarGroup}; ``` ## Usage ### Basic Avatar You can create an [Avatar] by providing an image source URL and a user name: ```rust Avatar::new() .name("John Doe") .src("https://example.com/avatar.jpg") ``` ### Avatar with Fallback Text When no image source is provided, the Avatar displays user initials with an automatically generated color background: ```rust // Shows "JD" initials with colored background Avatar::new() .name("John Doe") // Shows "JS" initials Avatar::new() .name("Jane Smith") ``` ### Avatar Placeholder For anonymous users or when no name is provided: ```rust use gpui_component::IconName; // Default user icon placeholder Avatar::new() // Custom placeholder icon Avatar::new() .placeholder(IconName::Building2) ``` ### Avatar Sizes ```rust Avatar::new() .name("John Doe") .xsmall() Avatar::new() .name("John Doe") .small() Avatar::new() .name("John Doe") // 48px (default medium) Avatar::new() .name("John Doe") .large() // Custom size Avatar::new() .name("John Doe") .with_size(px(100.)) ``` ### Custom Styling ```rust Avatar::new() .src("https://example.com/avatar.jpg") .with_size(px(100.)) .border_3() .border_color(cx.theme().foreground) .shadow_sm() .rounded(px(20.)) // Custom border radius ``` ## AvatarGroup The [AvatarGroup] component allows you to display multiple avatars in a compact, overlapping layout: ### Basic Group ```rust AvatarGroup::new() .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().name("John Doe")) ``` ### Group with Limit ```rust AvatarGroup::new() .limit(3) // Show maximum 3 avatars .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().src("https://example.com/user4.jpg")) // Hidden .child(Avatar::new().src("https://example.com/user5.jpg")) // Hidden ``` ### Group with Ellipsis Show an ellipsis indicator when avatars are hidden due to the limit. In this example, only 3 avatars are shown, and "..." indicates there are more: ```rust AvatarGroup::new() .limit(3) .ellipsis() // Shows "..." when limit is exceeded .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().src("https://example.com/user4.jpg")) .child(Avatar::new().src("https://example.com/user5.jpg")) ``` ### Group Sizes The [Sizeable] trait can also be applied to AvatarGroup, and it will set the size for all contained avatars. ```rust // Extra small group AvatarGroup::new() .xsmall() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) .child(Avatar::new().name("C")) // Small group AvatarGroup::new() .small() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) // Medium group (default) AvatarGroup::new() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) // Large group AvatarGroup::new() .large() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) ``` ### Adding Multiple Avatars ```rust let avatars = vec![ Avatar::new().src("https://example.com/user1.jpg"), Avatar::new().src("https://example.com/user2.jpg"), Avatar::new().name("John Doe"), ]; AvatarGroup::new() .children(avatars) .limit(5) .ellipsis() ``` ## API Reference - [Avatar] - [AvatarGroup] ## Examples ### Team Display ```rust use gpui_component::{h_flex, v_flex}; v_flex() .gap_4() .child("Development Team") .child( AvatarGroup::new() .limit(4) .ellipsis() .child(Avatar::new().name("Alice Johnson").src("https://example.com/alice.jpg")) .child(Avatar::new().name("Bob Smith").src("https://example.com/bob.jpg")) .child(Avatar::new().name("Charlie Brown")) .child(Avatar::new().name("Diana Prince")) .child(Avatar::new().name("Eve Wilson")) ) ``` ### User Profile Header ```rust h_flex() .items_center() .gap_4() .child( Avatar::new() .src("https://example.com/profile.jpg") .name("John Doe") .large() .border_2() .border_color(cx.theme().primary) ) .child( v_flex() .child("John Doe") .child("Software Engineer") ) ``` ### Anonymous User ```rust use gpui_component::IconName; Avatar::new() .placeholder(IconName::UserCircle) .medium() ``` ### Avatar with Custom Colors ```rust // The avatar automatically generates colors based on the name // Different names will get different colors from the color palette Avatar::new().name("Alice") // Gets one color Avatar::new().name("Bob") // Gets a different color Avatar::new().name("Charlie") // Gets another color ``` [Avatar]: https://docs.rs/gpui-component/latest/gpui_component/avatar/struct.Avatar.html [AvatarGroup]: https://docs.rs/gpui-component/latest/gpui_component/avatar/struct.AvatarGroup.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ================================================ FILE: docs/docs/components/badge.md ================================================ --- title: Badge description: A red dot that indicates the number of unread messages, status, or other notifications. --- # Badge A versatile badge component that can display counts, dots, or icons on elements. Perfect for indicating notifications, status, or other contextual information on avatars, icons, or other UI elements. ## Import ```rust use gpui_component::badge::Badge; ``` ## Usage ### Badge with Count Use `count` to display a numeric badge, if the count is greater than zero (`> 0`) the badge will be shown, otherwise it will be hidden. There is a default maximum count of `99`, any count above this will be displayed as `99+`. You can customize this maximum using the [max](https://docs.rs/gpui-component/latest/gpui_component/badge/struct.Badge.html#method.max) method. ```rust Badge::new() .count(3) .child(Icon::new(IconName::Bell)) ``` ### Variants - Default: Displays a numeric count. - Dot: A small dot indicator, typically used for status. - Icon: Displays an icon instead of a number. ```rust // Number badge (default) Badge::new() .count(5) .child(Avatar::new().src("https://example.com/avatar.jpg")) // Dot badge Badge::new() .dot() .child(Icon::new(IconName::Inbox)) // Icon badge Badge::new() .icon(IconName::Check) .child(Avatar::new().src("https://example.com/avatar.jpg")) ``` ### Badge Sizes The Badge is also implemented with the [Sizable] trait, allowing you to set small, medium (default), or large sizes. ```rust // Small badge Badge::new() .small() .count(1) .child(Avatar::new().small()) // Medium badge (default) Badge::new() .count(5) .child(Avatar::new()) // Large badge Badge::new() .large() .count(10) .child(Avatar::new().large()) ``` ### Badge Colors ```rust use gpui_component::ActiveTheme; // Custom colors Badge::new() .count(3) .color(cx.theme().blue) .child(Avatar::new()) Badge::new() .icon(IconName::Star) .color(cx.theme().yellow) .child(Avatar::new()) Badge::new() .dot() .color(cx.theme().green) .child(Icon::new(IconName::Bell)) ``` ### Badge on Icons ```rust use gpui_component::{Icon, IconName}; // Badge with count on icon Badge::new() .count(3) .child(Icon::new(IconName::Bell).large()) // Badge with high count (shows max) Badge::new() .count(103) .child(Icon::new(IconName::Inbox).large()) // Custom max count Badge::new() .count(150) .max(999) .child(Icon::new(IconName::Mail)) ``` ### Badge on Avatars ```rust use gpui_component::avatar::Avatar; // Basic count badge Badge::new() .count(5) .child(Avatar::new().src("https://example.com/avatar.jpg")) // Status badge with icon Badge::new() .icon(IconName::Check) .color(cx.theme().green) .child(Avatar::new().src("https://example.com/avatar.jpg")) // Online indicator with dot Badge::new() .dot() .color(cx.theme().green) .child(Avatar::new().src("https://example.com/avatar.jpg")) ``` ### Complex Nested Badges ```rust // Badge on badge for complex status Badge::new() .count(212) .large() .child( Badge::new() .icon(IconName::Check) .large() .color(cx.theme().cyan) .child(Avatar::new().large().src("https://example.com/avatar.jpg")) ) // Multiple status indicators Badge::new() .count(2) .color(cx.theme().green) .large() .child( Badge::new() .icon(IconName::Star) .large() .color(cx.theme().yellow) .child(Avatar::new().large().src("https://example.com/avatar.jpg")) ) ``` ## API Reference - [Badge] ## Examples ### Notification Indicators ```rust // Unread messages Badge::new() .count(12) .child(Icon::new(IconName::Mail).large()) // New notifications Badge::new() .count(3) .color(cx.theme().red) .child(Icon::new(IconName::Bell).large()) // High priority with custom max Badge::new() .count(1234) .max(999) .color(cx.theme().orange) .child(Icon::new(IconName::AlertTriangle)) ``` ### Status Indicators ```rust // Online status Badge::new() .dot() .color(cx.theme().green) .child(Avatar::new().src("https://example.com/user.jpg")) // Verified status Badge::new() .icon(IconName::CheckCircle) .color(cx.theme().blue) .child(Avatar::new().src("https://example.com/verified-user.jpg")) // Warning status Badge::new() .icon(IconName::AlertTriangle) .color(cx.theme().yellow) .child(Avatar::new().src("https://example.com/user.jpg")) ``` ### Different Badge Positions ```rust // The badge automatically positions itself based on variant: // - Dot: top-right corner (small dot) // - Number: top-right with dynamic sizing // - Icon: bottom-right corner with border ``` ### Count Formatting ```rust // Numbers 1-99 show as-is Badge::new().count(5) // Shows "5" Badge::new().count(99) // Shows "99" // Numbers above max show with "+" Badge::new().count(100) // Shows "99+" (default max) Badge::new().count(1000).max(999) // Shows "999+" // Zero count hides the badge Badge::new().count(0) // Badge not visible ``` [Badge]: https://docs.rs/gpui_component/latest/gpui_component/badge/struct.Badge.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ================================================ FILE: docs/docs/components/button.md ================================================ --- title: Button description: Displays a button or a component that looks like a button. --- # Button The [Button] element with multiple variants, sizes, and states. Supports icons, loading states, and can be grouped together. ## Import ```rust use gpui_component::button::{Button, ButtonGroup}; ``` ## Usage ### Basic Button ```rust Button::new("my-button") .label("Click me") .on_click(|_, _, _| { println!("Button clicked!"); }) ``` ### Variants ```rust // Primary button Button::new("btn-primary").primary().label("Primary") // Secondary button (default) Button::new("btn-secondary").label("Secondary") // Danger button Button::new("btn-danger").danger().label("Delete") // Warning button Button::new("btn-warning").warning().label("Warning") // Success button Button::new("btn-success").success().label("Success") // Info button Button::new("btn-info").info().label("Info") // Ghost button Button::new("btn-ghost").ghost().label("Ghost") // Link button Button::new("btn-link").link().label("Link") // Text button Button::new("btn-text").text().label("Text") ``` ### Outline Buttons Outline style is not a variant itself, but can be combined with other variants. ```rust Button::new("btn").primary().outline().label("Primary Outline") Button::new("btn").danger().outline().label("Danger Outline") ``` ### Compact Button The `compact` method reduces the padding of the button for a more condensed appearance. ```rust // Compact (reduced padding) Button::new("btn") .label("Compact") .compact() ``` ### Sizeable The Button supports the [Sizable] trait for different sizes. ```rust Button::new("btn").xsmall().label("Extra Small") Button::new("btn").small().label("Small") Button::new("btn").label("Medium") // default Button::new("btn").large().label("Large") ``` ### With Icons The `icon` method supports multiple types, allowing you to use different visual indicators: - **[Icon] / [IconName]** - Static icons for actions and visual cues - **[Spinner]** - Animated loading indicator for async operations - **[ProgressCircle]** - Circular progress indicator showing completion percentage All icon types automatically adapt to the button's size and can be customized with colors and other properties. #### Icon Types ```rust use gpui_component::{Icon, IconName}; // Using IconName (simplest) Button::new("btn") .icon(IconName::Check) .label("Confirm") // Using Icon with custom size Button::new("btn") .icon(Icon::new(IconName::Heart)) .label("Like") // Icon only (no label) Button::new("btn") .icon(IconName::Search) ``` #### Spinner Icon Use a [Spinner] to indicate loading or processing state: ```rust use gpui_component::spinner::Spinner; // Basic spinner Button::new("btn") .icon(Spinner::new()) .label("Loading...") // Spinner with custom color Button::new("btn") .icon(Spinner::new().color(cx.theme().blue)) .label("Processing") // Spinner with icon Button::new("btn") .icon(Spinner::new().icon(IconName::LoaderCircle)) .label("Syncing") ``` #### ProgressCircle Icon Use a [ProgressCircle] to show progress percentage: ```rust use gpui_component::progress::ProgressCircle; // Basic progress circle Button::new("btn") .icon(ProgressCircle::new("install-progress").value(45.0)) .label("Installing...") // Progress circle with custom color Button::new("btn") .primary() .icon( ProgressCircle::new("download-progress") .value(75.0) .color(cx.theme().primary_foreground) ) .label("Downloading") // Different sizes Button::new("btn") .small() .icon(ProgressCircle::new("progress-1").value(60.0)) .label("Installing...") Button::new("btn") .large() .icon(ProgressCircle::new("progress-2").value(80.0)) .label("Installing...") ``` #### Dynamic Icon Updates Icons can be updated dynamically based on component state: ```rust struct InstallButton { progress: f32, is_installing: bool, } impl InstallButton { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let button = Button::new("install-btn") .label(if self.is_installing { "Installing..." } else { "Install" }); if self.is_installing { button.icon( ProgressCircle::new("install-progress") .value(self.progress) ) } else { button.icon(IconName::Download) } } } ``` #### Loading State with Icons When a button is in loading state, it automatically handles icon transitions: ```rust // If icon is already a Spinner or ProgressCircle, it will be shown during loading Button::new("btn") .icon(Spinner::new()) .label("Processing") .loading(true) // Spinner will continue to show // If icon is a regular Icon, it will be replaced with a Spinner during loading Button::new("btn") .icon(IconName::Save) .label("Saving") .loading(true) // Icon will be replaced with Spinner ``` ### With a dropdown caret icon The `.dropdown_caret` method can allows adding a dropdown caret icon to end of the button. ```rust Button::new("btn") .label("Options") .dropdown_caret(true) ``` ### Button States There have `disabled`, `loading`, `selected` state for buttons to indicate different statuses. ```rust // Disabled Button::new("btn") .label("Disabled") .disabled(true) // Loading Button::new("btn") .label("Loading") .loading(true) // Selected Button::new("btn") .label("Selected") .selected(true) ``` ## Button Group ```rust ButtonGroup::new("btn-group") .child(Button::new("btn1").label("One")) .child(Button::new("btn2").label("Two")) .child(Button::new("btn3").label("Three")) ``` ### Toggle Button Group ```rust ButtonGroup::new("toggle-group") .multiple(true) // Allow multiple selections .child(Button::new("btn1").label("Option 1").selected(true)) .child(Button::new("btn2").label("Option 2")) .child(Button::new("btn3").label("Option 3")) .on_click(|selected_indices, _, _| { println!("Selected: {:?}", selected_indices); }) ``` ## Custom Variant ```rust use gpui_component::button::ButtonCustomVariant; let custom = ButtonCustomVariant::new(cx) .color(cx.theme().magenta) .foreground(cx.theme().primary_foreground) .border(cx.theme().magenta) .hover(cx.theme().magenta.opacity(0.1)) .active(cx.theme().magenta); Button::new("custom-btn") .custom(custom) .label("Custom Button") ``` ## API Reference - [Button] - [ButtonGroup] - [ButtonCustomVariant] ## Examples ### With Tooltip ```rust Button::new("btn") .label("Hover me") .tooltip("This is a helpful tooltip") ``` ### Custom Children ```rust Button::new("btn") .child( h_flex() .items_center() .gap_2() .child("Custom Content") .child(IconName::ChevronDown) .child(IconName::Eye) ) ``` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [ButtonGroup]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonGroup.html [ButtonCustomVariant]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonCustomVariant.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html [Spinner]: https://docs.rs/gpui-component/latest/gpui_component/spinner/struct.Spinner.html [ProgressCircle]: https://docs.rs/gpui-component/latest/gpui_component/progress/struct.ProgressCircle.html [Icon]: https://docs.rs/gpui-component/latest/gpui_component/icon/struct.Icon.html [IconName]: https://docs.rs/gpui-component/latest/gpui_component/icon/enum.IconName.html ================================================ FILE: docs/docs/components/calendar.md ================================================ --- title: Calendar description: A flexible calendar component for displaying months, navigating dates, and selecting single dates or date ranges. --- # Calendar A standalone calendar component that provides a rich interface for date selection and navigation. The Calendar component supports single date selection, date range selection, multiple month views, custom disabled dates, and comprehensive keyboard navigation. - [CalendarState]: For managing calendar state and selection. - [Calendar]: For rendering the calendar UI. ## Import ```rust use gpui_component::{ calendar::{Calendar, CalendarState, CalendarEvent, Date, Matcher}, }; ``` ## Usage ### Basic Calendar ```rust let state = cx.new(|cx| CalendarState::new(window, cx)); Calendar::new(&state) ``` ### Calendar with Initial Date ```rust use chrono::Local; let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); state.set_date(Local::now().naive_local().date(), window, cx); state }); Calendar::new(&state) ``` ### Date Range Calendar ```rust use chrono::{Local, Days}; let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); let now = Local::now().naive_local().date(); state.set_date( Date::Range(Some(now), now.checked_add_days(Days::new(7))), window, cx ); state }); Calendar::new(&state) ``` ### Multiple Months Display ```rust // Show 2 months side by side Calendar::new(&state) .number_of_months(2) // Show 3 months Calendar::new(&state) .number_of_months(3) ``` ### Calendar Sizes ```rust Calendar::new(&state).large() Calendar::new(&state) // medium (default) Calendar::new(&state).small() ``` ## Date Restrictions ### Disabled Weekends ```rust let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(vec![0, 6]) // Sunday=0, Saturday=6 }); Calendar::new(&state) ``` ### Disabled Specific Weekdays ```rust // Disable Sundays, Wednesdays, and Saturdays let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(vec![0, 3, 6]) }); Calendar::new(&state) ``` ### Disabled Date Range ```rust use chrono::{Local, Days}; let now = Local::now().naive_local().date(); let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::range( Some(now), now.checked_add_days(Days::new(7)), )) }); Calendar::new(&state) ``` ### Disabled Date Interval ```rust // Disable dates outside the interval (before/after specified dates) let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::interval( Some(now.checked_sub_days(Days::new(30)).unwrap()), now.checked_add_days(Days::new(30)) )) }); Calendar::new(&state) ``` ### Custom Disabled Dates ```rust // Disable first 5 days of each month let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { date.day0() < 5 // day0() returns 0-based day })) }); Calendar::new(&state) // Disable all Mondays let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { date.weekday() == chrono::Weekday::Mon })) }); Calendar::new(&state) // Disable past dates let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { *date < Local::now().naive_local().date() })) }); Calendar::new(&state) ``` ## Month/Year Navigation The Calendar automatically provides navigation controls: - **Previous/Next Month**: Arrow buttons in the header - **Month Selection**: Click on month name to open month picker - **Year Selection**: Click on year to open year picker - **Year Pages**: Navigate through 20-year pages in year view ### Custom Year Range ```rust let state = cx.new(|cx| { CalendarState::new(window, cx) .year_range((2020, 2030)) // Limit to specific year range }); Calendar::new(&state) ``` ## Handle Selection Events ```rust let state = cx.new(|cx| CalendarState::new(window, cx)); cx.subscribe(&state, |view, _, event, _| { match event { CalendarEvent::Selected(date) => { match date { Date::Single(Some(selected_date)) => { println!("Date selected: {}", selected_date); } Date::Range(Some(start), Some(end)) => { println!("Range selected: {} to {}", start, end); } Date::Range(Some(start), None) => { println!("Range start: {}", start); } _ => { println!("Selection cleared"); } } } } }); Calendar::new(&state) ``` ## Advanced Examples ### Business Days Only Calendar ```rust use chrono::Weekday; let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })) }); Calendar::new(&state) ``` ### Holiday Calendar ```rust use chrono::NaiveDate; use std::collections::HashSet; // Define holidays let holidays: HashSet = [ NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), // New Year NaiveDate::from_ymd_opt(2024, 7, 4).unwrap(), // Independence Day NaiveDate::from_ymd_opt(2024, 12, 25).unwrap(), // Christmas ].into_iter().collect(); let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(move |date| { holidays.contains(date) })) }); Calendar::new(&state) ``` ### Multi-Month Range Selector ```rust let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); state.set_date(Date::Range(None, None), window, cx); // Range mode state }); Calendar::new(&state) .number_of_months(3) // Show 3 months for easier range selection ``` ### Quarterly View Calendar ```rust let state = cx.new(|cx| CalendarState::new(window, cx)); // Update to show current quarter's months Calendar::new(&state) .number_of_months(3) ``` ## Custom Styling ```rust use gpui::{px, relative}; Calendar::new(&calendar) .p_4() // Custom padding .bg(cx.theme().secondary) // Custom background .border_2() // Custom border .border_color(cx.theme().primary) // Custom border color .rounded(px(12.)) // Custom border radius .w(px(400.)) // Custom width .h(px(350.)) // Custom height ``` ## API Reference - [Calendar] - [CalendarState] - [RangeMatcher] ## Examples ### Event Planning Calendar ```rust let event_calendar = cx.new(|cx| { let mut state = CalendarState::new(window, cx); // Disable past dates and weekends state = state.disabled_matcher(Matcher::custom(|date| { let now = Local::now().naive_local().date(); *date < now || matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })); state }); Calendar::new(&event_calendar) .large() // Easier to see and interact with ``` ### Vacation Booking Calendar ```rust let vacation_calendar = cx.new(|cx| { let mut state = CalendarState::new(window, cx); state.set_date(Date::Range(None, None), window, cx); // Range mode state }); Calendar::new(&vacation_calendar) .number_of_months(2) // Show 2 months for range selection ``` ### Report Date Range Selector ```rust let report_calendar = cx.new(|cx| { let mut state = CalendarState::new(window, cx) .year_range((2020, 2025)); // Limit to business years state.set_date(Date::Range(None, None), window, cx); state }); Calendar::new(&report_calendar) .number_of_months(3) .small() // Compact for dashboard use ``` ### Availability Calendar ```rust use std::collections::HashSet; let unavailable_dates: HashSet = get_unavailable_dates(); let availability_calendar = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(move |date| { unavailable_dates.contains(date) })) }); Calendar::new(&availability_calendar) .number_of_months(2) ``` The Calendar component provides a foundation for any date-related UI requirements, from simple date pickers to complex scheduling interfaces. [Calendar]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.Calendar.html [CalendarState]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.CalendarState.html [RangeMatcher]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.RangeMatcher.html ================================================ FILE: docs/docs/components/chart.md ================================================ --- title: Chart description: Beautiful charts and graphs for data visualization including line, bar, area, pie, and candlestick charts. --- # Chart A comprehensive charting library providing Line, Bar, Area, Pie, and Candlestick charts for data visualization. The charts feature smooth animations, customizable styling, tooltips, legends, and automatic theming that adapts to your application's theme. ## Import ```rust use gpui_component::chart::{LineChart, BarChart, AreaChart, PieChart, CandlestickChart}; ``` ## Chart Types ### LineChart A line chart displays data points connected by straight line segments, perfect for showing trends over time. #### Basic Line Chart ```rust #[derive(Clone)] struct DataPoint { x: String, y: f64, } let data = vec![ DataPoint { x: "Jan".to_string(), y: 100.0 }, DataPoint { x: "Feb".to_string(), y: 150.0 }, DataPoint { x: "Mar".to_string(), y: 120.0 }, ]; LineChart::new(data) .x(|d| d.x.clone()) .y(|d| d.y) ``` #### Line Chart Variants ```rust // Basic curved line (default) LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) // Linear interpolation LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .linear() // Step after interpolation LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .step_after() // With dots at data points LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .dot() // Custom stroke color LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .stroke(cx.theme().success) ``` #### Tick Control ```rust // Show every tick LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .tick_margin(1) // Show every 2nd tick LineChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .tick_margin(2) ``` ### BarChart A bar chart uses rectangular bars to show comparisons among categories. #### Basic Bar Chart ```rust BarChart::new(data) .x(|d| d.category.clone()) .y(|d| d.value) ``` #### Bar Chart Customization ```rust // Custom fill colors BarChart::new(data) .x(|d| d.category.clone()) .y(|d| d.value) .fill(|d| d.color) // With labels on bars BarChart::new(data) .x(|d| d.category.clone()) .y(|d| d.value) .label(|d| format!("{}", d.value)) // Custom tick spacing BarChart::new(data) .x(|d| d.category.clone()) .y(|d| d.value) .tick_margin(2) ``` ### AreaChart An area chart displays quantitative data visually, similar to a line chart but with the area below the line filled. #### Basic Area Chart ```rust AreaChart::new(data) .x(|d| d.time.clone()) .y(|d| d.value) ``` #### Stacked Area Charts ```rust // Multi-series area chart AreaChart::new(data) .x(|d| d.date.clone()) .y(|d| d.desktop) // First series .stroke(cx.theme().chart_1) .fill(cx.theme().chart_1.opacity(0.4)) .y(|d| d.mobile) // Second series .stroke(cx.theme().chart_2) .fill(cx.theme().chart_2.opacity(0.4)) ``` #### Area Chart Styling ```rust use gpui::{linear_gradient, linear_color_stop}; // With gradient fill AreaChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_1.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) // Different interpolation styles AreaChart::new(data) .x(|d| d.month.clone()) .y(|d| d.value) .linear() // or .step_after() ``` ### PieChart A pie chart displays data as slices of a circular chart, ideal for showing proportions. #### Basic Pie Chart ```rust PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) ``` #### Donut Chart ```rust PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) .inner_radius(60.) // Creates donut effect ``` #### Pie Chart Customization ```rust // Custom colors PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) .color(|d| d.color) // With padding between slices PieChart::new(data) .value(|d| d.amount as f32) .outer_radius(100.) .inner_radius(60.) .pad_angle(4. / 100.) // 4% padding ``` ### CandlestickChart A candlestick chart displays financial data using OHLC (Open, High, Low, Close) values, perfect for visualizing stock prices and market trends. #### Basic Candlestick Chart ```rust #[derive(Clone)] struct StockPrice { pub date: String, pub open: f64, pub high: f64, pub low: f64, pub close: f64, } let data = vec![ StockPrice { date: "Jan".to_string(), open: 100.0, high: 110.0, low: 95.0, close: 105.0 }, StockPrice { date: "Feb".to_string(), open: 105.0, high: 115.0, low: 100.0, close: 112.0 }, StockPrice { date: "Mar".to_string(), open: 112.0, high: 120.0, low: 108.0, close: 115.0 }, ]; CandlestickChart::new(data) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) ``` #### Candlestick Chart Customization ```rust // Adjust body width ratio (default: 0.6) CandlestickChart::new(data) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .body_width_ratio(0.4) // Narrower bodies // Custom tick spacing CandlestickChart::new(data) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .tick_margin(2) // Show every 2nd tick ``` #### Candlestick Chart Colors The candlestick chart automatically uses theme colors: - **Bullish** (close > open): `bullish` color (green) - **Bearish** (close < open): `bearish` color (red) ## Data Structures ### Example Data Types ```rust // Time series data #[derive(Clone)] struct DailyDevice { pub date: String, pub desktop: f64, pub mobile: f64, } // Category data with styling #[derive(Clone)] struct MonthlyDevice { pub month: String, pub desktop: f64, pub color_alpha: f32, } impl MonthlyDevice { pub fn color(&self, base_color: Hsla) -> Hsla { base_color.alpha(self.color_alpha) } } // Financial data #[derive(Clone)] struct StockPrice { pub date: String, pub open: f64, pub high: f64, pub low: f64, pub close: f64, pub volume: u64, } ``` ## Chart Configuration ### Container Setup ```rust fn chart_container( title: &str, chart: impl IntoElement, center: bool, cx: &mut Context, ) -> impl IntoElement { v_flex() .flex_1() .h_full() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius_lg) .p_4() .child( div() .when(center, |this| this.text_center()) .font_semibold() .child(title.to_string()), ) .child( div() .when(center, |this| this.text_center()) .text_color(cx.theme().muted_foreground) .text_sm() .child("Data period label"), ) .child(div().flex_1().py_4().child(chart)) .child( div() .when(center, |this| this.text_center()) .font_semibold() .text_sm() .child("Summary statistic"), ) .child( div() .when(center, |this| this.text_center()) .text_color(cx.theme().muted_foreground) .text_sm() .child("Additional context"), ) } ``` ### Theme Integration ```rust // Charts automatically use theme colors let chart = LineChart::new(data) .x(|d| d.date.clone()) .y(|d| d.value) .stroke(cx.theme().chart_1); // Uses theme chart colors // Available theme chart colors: // cx.theme().chart_1 // cx.theme().chart_2 // cx.theme().chart_3 // ... up to chart_5 ``` ## API Reference - [LineChart] - [BarChart] - [AreaChart] - [PieChart] - [CandlestickChart] ## Examples ### Sales Dashboard ```rust #[derive(Clone)] struct SalesData { month: String, revenue: f64, profit: f64, region: String, } fn sales_dashboard(data: Vec, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( h_flex() .gap_4() .child( chart_container( "Monthly Revenue", LineChart::new(data.clone()) .x(|d| d.month.clone()) .y(|d| d.revenue) .stroke(cx.theme().chart_1) .dot(), false, cx, ) ) .child( chart_container( "Profit Breakdown", PieChart::new(data.clone()) .value(|d| d.profit as f32) .outer_radius(80.) .color(|d| match d.region.as_str() { "North" => cx.theme().chart_1, "South" => cx.theme().chart_2, "East" => cx.theme().chart_3, "West" => cx.theme().chart_4, _ => cx.theme().chart_5, }), true, cx, ) ) ) .child( chart_container( "Regional Performance", BarChart::new(data) .x(|d| d.region.clone()) .y(|d| d.revenue) .fill(|d| match d.region.as_str() { "North" => cx.theme().chart_1, "South" => cx.theme().chart_2, "East" => cx.theme().chart_3, "West" => cx.theme().chart_4, _ => cx.theme().chart_5, }) .label(|d| format!("${:.0}k", d.revenue / 1000.)), false, cx, ) ) } ``` ### Multi-Series Time Chart ```rust #[derive(Clone)] struct DeviceUsage { date: String, desktop: f64, mobile: f64, tablet: f64, } fn device_usage_chart(data: Vec, cx: &mut Context) -> impl IntoElement { chart_container( "Device Usage Over Time", AreaChart::new(data) .x(|d| d.date.clone()) .y(|d| d.desktop) .stroke(cx.theme().chart_1) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_1.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) .y(|d| d.mobile) .stroke(cx.theme().chart_2) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_2.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) .y(|d| d.tablet) .stroke(cx.theme().chart_3) .fill(linear_gradient( 0., linear_color_stop(cx.theme().chart_3.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.3), 0.), )) .tick_margin(3), false, cx, ) } ``` ### Financial Chart ```rust #[derive(Clone)] struct StockData { date: String, price: f64, volume: u64, } #[derive(Clone)] struct StockOHLC { date: String, open: f64, high: f64, low: f64, close: f64, } fn stock_chart(ohlc_data: Vec, price_data: Vec, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( chart_container( "Stock Price - Candlestick", CandlestickChart::new(ohlc_data.clone()) .x(|d| d.date.clone()) .open(|d| d.open) .high(|d| d.high) .low(|d| d.low) .close(|d| d.close) .tick_margin(3), false, cx, ) ) .child( chart_container( "Stock Price - Line", LineChart::new(price_data.clone()) .x(|d| d.date.clone()) .y(|d| d.price) .stroke(cx.theme().chart_1) .linear() .tick_margin(5), false, cx, ) ) .child( chart_container( "Trading Volume", BarChart::new(price_data) .x(|d| d.date.clone()) .y(|d| d.volume as f64) .fill(|d| { if d.volume > 1000000 { cx.theme().chart_1 } else { cx.theme().muted_foreground.opacity(0.6) } }) .tick_margin(5), false, cx, ) ) } ``` ## Customization Options ### Color Schemes ```rust // Theme-based colors (recommended) LineChart::new(data) .x(|d| d.x.clone()) .y(|d| d.y) .stroke(cx.theme().chart_1) // Custom color palette let colors = [ cx.theme().success, cx.theme().warning, cx.theme().destructive, cx.theme().info, cx.theme().chart_1, ]; BarChart::new(data) .x(|d| d.category.clone()) .y(|d| d.value) .fill(|d| colors[d.category_index % colors.len()]) ``` ### Responsive Design ```rust // Container with responsive sizing div() .flex_1() .min_h(px(300.)) .max_h(px(600.)) .w_full() .child( LineChart::new(data) .x(|d| d.x.clone()) .y(|d| d.y) ) ``` ### Grid and Axis Styling Charts automatically include: - Grid lines with dashed appearance - X-axis labels with smart positioning - Y-axis scaling starting from zero - Responsive tick spacing based on `tick_margin` ## Performance Considerations ### Large Datasets ```rust // For large datasets, consider data sampling let sampled_data: Vec<_> = data .iter() .step_by(5) // Show every 5th point .cloned() .collect(); LineChart::new(sampled_data) .x(|d| d.date.clone()) .y(|d| d.value) .tick_margin(3) // Reduce tick density ``` ### Memory Optimization ```rust // Use efficient data accessors LineChart::new(data) .x(|d| d.date.clone()) // Clone only when necessary .y(|d| d.value) // Direct field access ``` ## Integration Examples ### With State Management ```rust struct ChartComponent { data: Vec, chart_type: ChartType, time_range: TimeRange, } impl ChartComponent { fn render_chart(&self, cx: &mut Context) -> impl IntoElement { match self.chart_type { ChartType::Line => LineChart::new(self.filtered_data()) .x(|d| d.date.clone()) .y(|d| d.value) .into_any_element(), ChartType::Bar => BarChart::new(self.filtered_data()) .x(|d| d.date.clone()) .y(|d| d.value) .into_any_element(), ChartType::Area => AreaChart::new(self.filtered_data()) .x(|d| d.date.clone()) .y(|d| d.value) .into_any_element(), } } fn filtered_data(&self) -> Vec { self.data .iter() .filter(|d| self.time_range.contains(&d.date)) .cloned() .collect() } } ``` ### Real-time Updates ```rust struct LiveChart { data: Vec, max_points: usize, } impl LiveChart { fn add_data_point(&mut self, point: DataPoint) { self.data.push(point); if self.data.len() > self.max_points { self.data.remove(0); // Remove oldest point } } fn render(&self, cx: &mut Context) -> impl IntoElement { LineChart::new(self.data.clone()) .x(|d| d.timestamp.clone()) .y(|d| d.value) .linear() .dot() } } ``` [LineChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.LineChart.html [BarChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.BarChart.html [AreaChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.AreaChart.html [PieChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.PieChart.html [CandlestickChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.CandlestickChart.html ================================================ FILE: docs/docs/components/checkbox.md ================================================ --- title: Checkbox description: A control that allows the user to toggle between checked and not checked. --- # Checkbox A checkbox component for binary choices. Supports labels, disabled state, and different sizes. ## Import ```rust use gpui_component::checkbox::Checkbox; ``` ## Usage ### Basic Checkbox ```rust Checkbox::new("my-checkbox") .label("Accept terms and conditions") .checked(false) .on_click(|checked, _, _| { println!("Checkbox is now: {}", checked); }) ``` The `on_click` callback is triggered when the user toggles the checkbox, receiving the **new checked state**. ### Controlled Checkbox ```rust struct MyView { is_checked: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Checkbox::new("checkbox") .label("Option") .checked(self.is_checked) .on_click(cx.listener(|view, checked, _, cx| { view.is_checked = *checked; cx.notify(); })) } } ``` ### Different Sizes ```rust Checkbox::new("cb").text_xs().label("Extra Small") Checkbox::new("cb").text_sm().label("Small") Checkbox::new("cb").label("Medium") // default Checkbox::new("cb").text_lg().label("Large") ``` ### Disabled State ```rust Checkbox::new("checkbox") .label("Disabled checkbox") .disabled(true) .checked(false) ``` ### Without Label ```rust Checkbox::new("checkbox") .checked(true) ``` ### Custom Tab Order ```rust Checkbox::new("checkbox") .label("Custom tab order") .tab_index(2) .tab_stop(true) ``` ## API Reference - [Checkbox] ### Styling Implements `Sizable` and `Disableable` traits: - `text_xs()` - Extra small text - `text_sm()` - Small text - `text_base()` - Base text (default) - `text_lg()` - Large text - `disabled(bool)` - Disabled state ## Examples ### Checkbox List ```rust v_flex() .gap_2() .child(Checkbox::new("cb1").label("Option 1").checked(true)) .child(Checkbox::new("cb2").label("Option 2").checked(false)) .child(Checkbox::new("cb3").label("Option 3").checked(false)) ``` ### Form Integration ```rust struct FormView { agree_terms: bool, subscribe: bool, } v_flex() .gap_3() .child( Checkbox::new("terms") .label("I agree to the terms and conditions") .checked(self.agree_terms) .on_click(cx.listener(|view, checked, _, cx| { view.agree_terms = *checked; cx.notify(); })) ) .child( Checkbox::new("subscribe") .label("Subscribe to newsletter") .checked(self.subscribe) .on_click(cx.listener(|view, checked, _, cx| { view.subscribe = *checked; cx.notify(); })) ) ``` [Checkbox]: https://docs.rs/gpui-component/latest/gpui_component/checkbox/struct.Checkbox.html ================================================ FILE: docs/docs/components/clipboard.md ================================================ --- title: Clipboard description: A button component that helps you copy text or other content to your clipboard. --- # Clipboard The Clipboard component provides an easy way to copy text or other data to the user's clipboard. It renders as a button with a copy icon that changes to a checkmark when content is successfully copied. The component supports both static values and dynamic content through callback functions. ## Import ```rust use gpui_component::clipboard::Clipboard; ``` ## Usage ### Basic Clipboard ```rust Clipboard::new("my-clipboard") .value("Text to copy") .on_copied(|value, window, cx| { window.push_notification(format!("Copied: {}", value), cx) }) ``` ### Using Dynamic Values The `value_fn` method allows you to provide a closure that generates the content to be copied at the time of the copy action. - This is useful when the content to be copied depends on the current state of the application. - And in some cases, it may have a larger overhead to compute, so you only want to do it when the user actually clicks the copy button. ```rust let state = some_state.clone(); Clipboard::new("dynamic-clipboard") .value_fn(move |_, cx| { state.read(cx).get_current_value() }) .on_copied(|value, window, cx| { window.push_notification(format!("Copied: {}", value), cx) }) ``` ### With Custom Content ```rust use gpui_component::label::Label; h_flex() .gap_2() .child(Label::new("Share URL")) .child(Icon::new(IconName::Share)) .child( Clipboard::new("custom-clipboard") .value("https://example.com") ) ``` ### In Input Fields The Clipboard component is commonly used as a suffix in input fields: ```rust use gpui_component::input::{InputState, Input}; let url_state = cx.new(|cx| InputState::new(window, cx).default_value("https://github.com")); Input::new(&url_state) .suffix( Clipboard::new("url-clipboard") .value_fn({ let state = url_state.clone(); move |_, cx| state.read(cx).value() }) .on_copied(|value, window, cx| { window.push_notification(format!("URL copied: {}", value), cx) }) ) ``` ## API Reference - [Clipboard] ## Examples ### Simple Text Copy ```rust Clipboard::new("simple") .value("Hello, World!") ``` ### With User Feedback ```rust h_flex() .gap_2() .child(Label::new("Your API Key:")) .child( Clipboard::new("feedback") .value("sk-1234567890abcdef") .on_copied(|_, window, cx| { window.push_notification("API key copied to clipboard", cx) }) ) ``` ### Form Field Integration ```rust use gpui_component::{ input::{InputState, Input}, h_flex, label::Label }; let api_key = "sk-1234567890abcdef"; h_flex() .gap_2() .items_center() .child(Label::new("API Key:")) .child( Input::new(&input_state) .value(api_key) .readonly(true) .suffix( Clipboard::new("api-key-copy") .value(api_key) .on_copied(|_, window, cx| { window.push_notification("API key copied!", cx) }) ) ) ``` ### Dynamic Content Copy ```rust struct AppState { current_url: String, } let app_state = cx.new(|_| AppState { current_url: "https://example.com".to_string() }); Clipboard::new("current-url") .value_fn({ let state = app_state.clone(); move |_, cx| { SharedString::from(state.read(cx).current_url.clone()) } }) .on_copied(|url, window, cx| { window.push_notification(format!("Shared: {}", url), cx) }) ``` ## Data Types The Clipboard component currently supports copying text strings to the clipboard. It uses GPUI's `ClipboardItem::new_string()` method, which handles: - Plain text strings - UTF-8 encoded content - Cross-platform clipboard integration [Clipboard]: https://docs.rs/gpui-component/latest/gpui_component/clipboard/struct.Clipboard.html ================================================ FILE: docs/docs/components/collapsible.md ================================================ --- title: Collapsible description: An interactive element which expands/collapses. --- # Collapsible An interactive element which expands/collapses. ## Import ```rust use gpui_component::collapsible::Collapsible; ``` ## Usage ### Basic Use ```rust Collapsible::new() .max_w_128() .gap_1() .open(self.open) .child( "This is a collapsible component. \ Click the header to expand or collapse the content.", ) .content( "This is the full content of the Collapsible component. \ It is only visible when the component is expanded. \n\ You can put any content you like here, including text, images, \ or other UI elements.", ) .child( h_flex().justify_center().child( Button::new("toggle1") .icon(IconName::ChevronDown) .label("Show more") .when(open, |this| { this.icon(IconName::ChevronUp).label("Show less") }) .xsmall() .link() .on_click({ cx.listener(move |this, _, _, cx| { this.open = !this.open; cx.notify(); }) }), ), ) ``` We can use `open` method to control the collapsed state. If false, the `content` method added child elements will be hidden. [Collapsible]: https://docs.rs/gpui-component/latest/gpui_component/collapsible/struct.Collapsible.html ================================================ FILE: docs/docs/components/color-picker.md ================================================ --- title: ColorPicker description: A comprehensive color selection interface with support for multiple color formats, presets, and alpha channel. --- # ColorPicker A versatile color picker component that provides an intuitive interface for color selection. Features include color palettes, hex input, featured colors, and support for various color formats including RGB, HSL, and hex values with alpha channel support. ## Import ```rust use gpui_component::color_picker::{ColorPicker, ColorPickerState, ColorPickerEvent}; ``` ## Usage ### Basic Color Picker ```rust use gpui::{Entity, Window, Context}; // Create color picker state let color_picker = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().primary) ); // Create the color picker component ColorPicker::new(&color_picker) ``` ### With Event Handling ```rust use gpui::{Subscription, Entity}; let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); let _subscription = cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { println!("Selected color: {}", color.to_hex()); // Handle color change } } }); ColorPicker::new(&color_picker) ``` ### Setting Default Color ```rust use gpui::Hsla; let color_picker = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().blue) // Set default color ); ``` ### Different Sizes ```rust // Small color picker ColorPicker::new(&color_picker).small() // Medium color picker (default) ColorPicker::new(&color_picker) // Large color picker ColorPicker::new(&color_picker).large() // Extra small color picker ColorPicker::new(&color_picker).xsmall() ``` ### With Custom Featured Colors ```rust use gpui::Hsla; let featured_colors = vec![ cx.theme().red, cx.theme().green, cx.theme().blue, cx.theme().yellow, // Add your custom colors ]; ColorPicker::new(&color_picker) .featured_colors(featured_colors) ``` ### With Icon Instead of Color Square ```rust use gpui_component::IconName; ColorPicker::new(&color_picker) .icon(IconName::Palette) ``` ### With Label ```rust ColorPicker::new(&color_picker) .label("Background Color") ``` ### Custom Anchor Position ```rust use gpui::Corner; ColorPicker::new(&color_picker) .anchor(Corner::TopRight) // Dropdown opens to top-right ``` ## Color Selection Interface ### Color Palettes The color picker includes predefined color palettes organized by color family: - **Stone**: Neutral grays and stone colors - **Red**: Red color variations from light to dark - **Orange**: Orange color variations - **Yellow**: Yellow color variations - **Green**: Green color variations - **Cyan**: Cyan color variations - **Blue**: Blue color variations - **Purple**: Purple color variations - **Pink**: Pink color variations Each palette provides multiple shades and tints of the base color, allowing for precise color selection. ### Featured Colors Section A customizable section at the top of the picker that displays frequently used or brand colors. If not specified, defaults to theme colors: - Primary colors from the current theme - Light variants of theme colors - Essential UI colors (red, blue, green, yellow, cyan, magenta) ### Hex Input Field A text input field that allows direct entry of hex color values: - Supports standard 6-digit hex format (#RRGGBB) - Real-time validation and preview - Updates color picker state automatically - Press Enter to confirm selection ## Color Formats ### RGB (Red, Green, Blue) Colors are internally represented using GPUI's `Hsla` format but can be converted to RGB: ```rust let color = cx.theme().blue; // Access RGB components through Hsla methods ``` ### HSL (Hue, Saturation, Lightness) Native format used by the color picker: ```rust use gpui::Hsla; // Create HSL color let color = Hsla::hsl(240.0, 100.0, 50.0); // Blue color // Access components let hue = color.h; let saturation = color.s; let lightness = color.l; ``` ### Hex Format Standard web hex format with # prefix: ```rust // Convert color to hex let hex_string = color.to_hex(); // Returns "#3366FF" // Parse hex string to color if let Ok(color) = Hsla::parse_hex("#3366FF") { // Use parsed color } ``` ## Alpha Channel Full alpha channel support for transparency: ```rust use gpui::hsla; // Create color with alpha let semi_transparent = hsla(0.5, 0.8, 0.6, 0.7); // 70% opacity // Modify existing color opacity let transparent_blue = cx.theme().blue.opacity(0.5); ``` The color picker preserves alpha values when selecting colors and allows modification through the alpha component of HSLA colors. ## API Reference - [ColorPicker] - [ColorPickerState] - [ColorPickerEvent] ## Examples ### Color Theme Editor ```rust struct ThemeEditor { primary_color: Entity, secondary_color: Entity, accent_color: Entity, } impl ThemeEditor { fn new(window: &mut Window, cx: &mut Context) -> Self { let primary_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().primary) ); let secondary_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().secondary) ); let accent_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().accent) ); Self { primary_color, secondary_color, accent_color, } } fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( h_flex() .gap_2() .items_center() .child("Primary Color:") .child(ColorPicker::new(&self.primary_color)) ) .child( h_flex() .gap_2() .items_center() .child("Secondary Color:") .child(ColorPicker::new(&self.secondary_color)) ) .child( h_flex() .gap_2() .items_center() .child("Accent Color:") .child(ColorPicker::new(&self.accent_color)) ) } } ``` ### Brand Color Selector ```rust use gpui_component::{Sizable as _}; let brand_colors = vec![ Hsla::parse_hex("#FF6B6B").unwrap(), // Brand Red Hsla::parse_hex("#4ECDC4").unwrap(), // Brand Teal Hsla::parse_hex("#45B7D1").unwrap(), // Brand Blue Hsla::parse_hex("#96CEB4").unwrap(), // Brand Green Hsla::parse_hex("#FFEAA7").unwrap(), // Brand Yellow ]; ColorPicker::new(&color_picker) .featured_colors(brand_colors) .label("Brand Color") .large() ``` ### Toolbar Color Picker ```rust use gpui_component::{Sizable as _, IconName); ColorPicker::new(&text_color_picker) .icon(IconName::Type) .small() .anchor(Corner::BottomLeft) ``` ### Color Palette Builder ```rust struct ColorPalette { colors: Vec>, } impl ColorPalette { fn add_color(&mut self, window: &mut Window, cx: &mut Context) { let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); // Subscribe to color changes cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { this.update_palette_preview(); } } }); self.colors.push(color_picker); cx.notify(); } fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .gap_2() .children( self.colors.iter().map(|color_picker| { ColorPicker::new(color_picker).small() }) ) .child( Button::new("add-color") .icon(IconName::Plus) .ghost() .on_click(cx.listener(|this, _, window, cx| { this.add_color(window, cx); })) ) } } ``` ### With Color Validation ```rust let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); let _subscription = cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { // Validate color accessibility if this.validate_contrast(color) { this.apply_color(color); } else { this.show_contrast_warning(); } } } }); ``` [ColorPicker]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/struct.ColorPicker.html [ColorPickerState]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/struct.ColorPickerState.html [ColorPickerEvent]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/enum.ColorPickerEvent.html ================================================ FILE: docs/docs/components/data-table.md ================================================ --- title: DataTable description: High-performance data table with virtual scrolling, sorting, filtering, and column management. --- # Data Table A comprehensive data table component designed for handling large datasets with high performance. Features virtual scrolling, column configuration, sorting, filtering, row/column/cell selection, and custom cell rendering. Perfect for displaying tabular data with thousands of rows while maintaining smooth performance. ## Key Features - **Multiple Selection Modes**: Row, column, and individual cell selection - **Virtual Scrolling**: Handle thousands of rows with smooth performance - **Column Management**: Resizable, movable, and fixed columns - **Sorting**: Built-in column sorting support - **Keyboard Navigation**: Full keyboard support for all selection modes - **Custom Cell Rendering**: Render any content in table cells - **Context Menus**: Right-click support for rows and cells - **Infinite Loading**: Load more data as user scrolls - **Events**: Comprehensive event system for user interactions ## Import ```rust use gpui_component::table::{ DataTable, TableState, TableDelegate, Column, ColumnSort, ColumnFixed, TableEvent }; ``` ## Usage ### Basic Table To create a table, you need to implement the `TableDelegate` trait and provide column definitions, and use `TableState` to manage the table state. ```rust use std::ops::Range; use gpui::{App, Context, Window, IntoElement}; use gpui_component::table::{DataTable, TableDelegate, Column, ColumnSort}; struct MyData { id: usize, name: String, age: u32, email: String, } struct MyTableDelegate { data: Vec, columns: Vec, } impl MyTableDelegate { fn new() -> Self { Self { data: vec![ MyData { id: 1, name: "John".to_string(), age: 30, email: "john@example.com".to_string() }, MyData { id: 2, name: "Jane".to_string(), age: 25, email: "jane@example.com".to_string() }, ], columns: vec![ Column::new("id", "ID").width(60.), Column::new("name", "Name").width(150.).sortable(), Column::new("age", "Age").width(80.).sortable(), Column::new("email", "Email").width(200.), ], } } } impl TableDelegate for MyTableDelegate { fn columns_count(&self, _: &App) -> usize { self.columns.len() } fn rows_count(&self, _: &App) -> usize { self.data.len() } fn column(&self, col_ix: usize, _: &App) -> &Column { &self.columns[col_ix] } fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; match col.key.as_ref() { "id" => row.id.to_string(), "name" => row.name.clone(), "age" => row.age.to_string(), "email" => row.email.clone(), _ => "".to_string(), } } } // Create the table let delegate = MyTableDelegate::new(); let state = cx.new(|cx| TableState::new(delegate, window, cx)); ``` ### Column Configuration Columns provide extensive configuration options: ```rust // Basic column Column::new("id", "ID") // Sortable column Column::new("name", "Name") .sortable() .width(150.) // Right-aligned column Column::new("price", "Price") .text_right() .sortable() // Fixed column (pinned to left) Column::new("actions", "Actions") .fixed(ColumnFixed::Left) .resizable(false) .movable(false) // Column with custom padding Column::new("description", "Description") .width(200.) .paddings(px(8.)) // Non-resizable column Column::new("status", "Status") .width(100.) .resizable(false) // Custom sort orders Column::new("created", "Created") .ascending() // Default ascending // or Column::new("modified", "Modified") .descending() // Default descending ``` ### Virtual Scrolling for Large Datasets The table automatically handles virtual scrolling for optimal performance: ```rust struct LargeDataDelegate { data: Vec, // Could be 10,000+ items columns: Vec, } impl TableDelegate for LargeDataDelegate { fn rows_count(&self, _: &App) -> usize { self.data.len() // No performance impact regardless of size } // Only visible rows are rendered fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>) -> impl IntoElement { // This is only called for visible rows // Efficiently render cell content let row = &self.data[row_ix]; format_cell_data(row, col_ix) } // Track visible range for optimizations fn visible_rows_changed(&mut self, visible_range: Range, _: &mut Window, _: &mut Context>) { // Only update data for visible rows if needed // This is called when user scrolls } } ``` ### Sorting Implementation Implement sorting in your delegate: ```rust impl TableDelegate for MyTableDelegate { fn perform_sort(&mut self, col_ix: usize, sort: ColumnSort, _: &mut Window, _: &mut Context>) { let col = &self.columns[col_ix]; match col.key.as_ref() { "name" => { match sort { ColumnSort::Ascending => self.data.sort_by(|a, b| a.name.cmp(&b.name)), ColumnSort::Descending => self.data.sort_by(|a, b| b.name.cmp(&a.name)), ColumnSort::Default => { // Reset to original order or default sort self.data.sort_by(|a, b| a.id.cmp(&b.id)); } } } "age" => { match sort { ColumnSort::Ascending => self.data.sort_by(|a, b| a.age.cmp(&b.age)), ColumnSort::Descending => self.data.sort_by(|a, b| b.age.cmp(&a.age)), ColumnSort::Default => self.data.sort_by(|a, b| a.id.cmp(&b.id)), } } _ => {} } } } ``` ### ContextMenu ```rust impl TableDelegate for MyTableDelegate { // Context menu for right-click fn context_menu(&mut self, row_ix: usize, menu: PopupMenu, _: &mut Window, _: &mut Context>) -> PopupMenu { let row = &self.data[row_ix]; menu.menu(format!("Edit {}", row.name), Box::new(EditRowAction(row_ix))) .menu("Delete", Box::new(DeleteRowAction(row_ix))) .separator() .menu("Duplicate", Box::new(DuplicateRowAction(row_ix))) } } ``` ### Cell Rendering Create rich cell content with custom rendering: ```rust impl TableDelegate for MyTableDelegate { fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; match col.key.as_ref() { "status" => { // Custom status badge let (color, text) = match row.status { Status::Active => (cx.theme().green, "Active"), Status::Inactive => (cx.theme().red, "Inactive"), Status::Pending => (cx.theme().yellow, "Pending"), }; div() .px_2() .py_1() .rounded(px(4.)) .bg(color.opacity(0.1)) .text_color(color) .child(text) } "progress" => { // Progress bar div() .w_full() .h(px(8.)) .bg(cx.theme().muted) .rounded(px(4.)) .child( div() .h_full() .w(percentage(row.progress)) .bg(cx.theme().primary) .rounded(px(4.)) ) } "actions" => { // Action buttons h_flex() .gap_1() .child(Button::new(format!("edit-{}", row_ix)).text().icon(IconName::Edit)) .child(Button::new(format!("delete-{}", row_ix)).text().icon(IconName::Trash)) } "avatar" => { // User avatar with image h_flex() .items_center() .gap_2() .child( div() .w(px(32.)) .h(px(32.)) .rounded_full() .bg(cx.theme().accent) .flex() .items_center() .justify_center() .child(row.name.chars().next().unwrap_or('?').to_string()) ) .child(row.name.clone()) } _ => row.get_field_value(col.key.as_ref()).into_any_element(), } } } ``` ### Selection Modes The table supports three distinct selection modes: ```rust // Row selection mode (default) let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(true) // Enable row selection .col_selectable(false) .cell_selectable(false) }); // Column selection mode let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(false) .col_selectable(true) // Enable column selection .cell_selectable(false) }); // Cell selection mode let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(true) // Keep row selection for row selector column .col_selectable(false) .cell_selectable(true) // Enable cell selection }); ``` ### Column Resizing and Moving Enable dynamic column management: ```rust // Configure table features let state = cx.new(|cx| { TableState::new(delegate, window, cx) .col_resizable(true) // Allow column resizing .col_movable(true) // Allow column reordering .sortable(true) // Enable sorting .col_selectable(true) // Allow column selection .row_selectable(true) // Allow row selection }); // Listen for column changes cx.subscribe_in(&state, window, |view, table, event, _, cx| { match event { TableEvent::ColumnWidthsChanged(widths) => { // Save column widths to user preferences save_column_widths(widths); } TableEvent::MoveColumn(from_ix, to_ix) => { // Save column order save_column_order(from_ix, to_ix); } _ => {} } }).detach(); ``` ### Infinite Loading / Pagination Implement loading more data as user scrolls: ```rust impl TableDelegate for MyTableDelegate { fn has_more(&self, _: &App) -> bool { self.has_more_data } fn load_more_threshold(&self) -> usize { 50 // Load more when 50 rows from bottom } fn load_more(&mut self, _: &mut Window, cx: &mut Context>) { if self.loading { return; // Prevent multiple loads } self.loading = true; // Spawn async task to load data cx.spawn(async move |view, cx| { let new_data = fetch_more_data().await; cx.update(|cx| { view.update(cx, |view, _| { let delegate = view.table.delegate_mut(); delegate.data.extend(new_data); delegate.loading = false; delegate.has_more_data = !new_data.is_empty(); }); }) }).detach(); } fn loading(&self, _: &App) -> bool { self.loading } } ``` ### Table Styling Customize table appearance: ```rust let state = cx.new(|cx| { TableState::new(delegate, window, cx) }); // In render DataTable::new(&state) .stripe(true) // Alternating row colors .bordered(true) // Border around table .scrollbar_visible(true, true) // Vertical, horizontal scrollbars ``` ## Examples ### Financial Data Table ```rust struct StockData { symbol: String, price: f64, change: f64, change_percent: f64, volume: u64, } impl TableDelegate for StockTableDelegate { fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { let stock = &self.stocks[row_ix]; let col = &self.columns[col_ix]; match col.key.as_ref() { "symbol" => div().font_weight(FontWeight::BOLD).child(stock.symbol.clone()), "price" => div().text_right().child(format!("${:.2}", stock.price)), "change" => { let color = if stock.change >= 0.0 { cx.theme().green } else { cx.theme().red }; div() .text_right() .text_color(color) .child(format!("{:+.2}", stock.change)) } "change_percent" => { let color = if stock.change_percent >= 0.0 { cx.theme().green } else { cx.theme().red }; div() .text_right() .text_color(color) .child(format!("{:+.1}%", stock.change_percent * 100.0)) } "volume" => div().text_right().child(format!("{:,}", stock.volume)), _ => div(), } } } ``` ### User Management Table ```rust struct UserTableDelegate { users: Vec, columns: Vec, } impl UserTableDelegate { fn new() -> Self { Self { users: Vec::new(), columns: vec![ Column::new("avatar", "").width(50.).resizable(false).movable(false), Column::new("name", "Name").width(150.).sortable().fixed_left(), Column::new("email", "Email").width(200.).sortable(), Column::new("role", "Role").width(100.).sortable(), Column::new("status", "Status").width(100.), Column::new("last_login", "Last Login").width(120.).sortable(), Column::new("actions", "Actions").width(100.).resizable(false), ], } } } ``` ### Cell Selection Enable individual cell selection for more granular control: ```rust let state = cx.new(|cx| { TableState::new(delegate, window, cx) .cell_selectable(true) // Enable cell selection .row_selectable(true) // Also allow row selection }); // Listen for cell events cx.subscribe_in(&state, window, |view, table, event, _, cx| { match event { TableEvent::SelectCell(row_ix, col_ix) => { println!("Selected cell: ({}, {})", row_ix, col_ix); } TableEvent::DoubleClickedCell(row_ix, col_ix) => { // Open editor or detail view open_cell_editor(row_ix, col_ix); } TableEvent::RightClickedCell(row_ix, col_ix) => { // Show cell-specific context menu show_cell_context_menu(row_ix, col_ix); } TableEvent::ClearSelection => { println!("Selection cleared"); } _ => {} } }).detach(); ``` #### Cell Selection Features When cell selection is enabled: - **Click to select**: Click on any cell to select it - **Row selector column**: A dedicated column appears on the left for selecting entire rows - **Keyboard navigation**: Arrow keys navigate between cells (not rows/columns) - **Double-click support**: Trigger actions like editing by double-clicking cells - **Right-click support**: Show context menus specific to cell content - **Visual feedback**: Selected cells show highlight with border #### Programmatic Cell Selection ```rust // Get the currently selected cell if let Some((row_ix, col_ix)) = state.read(cx).selected_cell() { println!("Current cell: ({}, {})", row_ix, col_ix); } // Select a specific cell programmatically state.update(cx, |state, cx| { state.set_selected_cell(5, 3, cx); // Select row 5, column 3 }); // Clear all selections state.update(cx, |state, cx| { state.clear_selection(cx); }); ``` #### Non-selectable Columns Prevent specific columns from being selected (useful for action columns): ```rust Column::new("actions", "Actions") .width(100.) .selectable(false) // This column's cells cannot be selected .resizable(false) ``` #### Cell Selection with Custom Rendering ```rust impl TableDelegate for MyTableDelegate { fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; // Render different content based on whether cell is selected let is_selected = cx.entity().read(cx).selected_cell() == Some((row_ix, col_ix)); match col.key.as_ref() { "editable_field" => { if is_selected { // Show input when selected Input::new(format!("cell-{}-{}", row_ix, col_ix)) .value(row.field_value.clone()) .into_any_element() } else { // Show plain text when not selected div().child(row.field_value.clone()).into_any_element() } } _ => div().child(row.get_value(col.key.as_ref())).into_any_element() } } } ``` ## Keyboard Shortcuts ### Row Selection Mode (default) - `↑/↓` - Navigate rows - `←/→` - Navigate columns - `Home` - Jump to first row/column - `End` - Jump to last row/column - `PageUp/PageDown` - Navigate by page - `Escape` - Clear selection ### Cell Selection Mode - `↑/↓` - Navigate up/down within current column - `←/→` - Navigate left/right within current row - `Tab` - Move to next cell (right, then next row) - `Shift+Tab` - Move to previous cell - `Home` - Jump to first cell in current row - `End` - Jump to last cell in current row - `PageUp/PageDown` - Navigate by page within current column - `Escape` - Clear selection ## API Reference ### Core Types - [DataTable] - The data table component - [TableState] - Table state management - [TableDelegate] - Trait for implementing table data source - [Column] - Column configuration - [TableEvent] - Table events (selection, clicks, etc.) ### Column Types - [ColumnSort] - Column sort direction enum - [ColumnFixed] - Column fixed position enum ### Methods #### TableState - `new(delegate, window, cx)` - Create a new table state - `cell_selectable(bool)` - Enable/disable cell selection - `row_selectable(bool)` - Enable/disable row selection - `col_selectable(bool)` - Enable/disable column selection - `selected_cell()` - Get currently selected cell - `set_selected_cell(row_ix, col_ix, cx)` - Select a specific cell - `selected_row()` - Get currently selected row - `selected_col()` - Get currently selected column - `clear_selection(cx)` - Clear all selections - `scroll_to_row(row_ix, cx)` - Scroll to specific row - `scroll_to_col(col_ix, cx)` - Scroll to specific column #### Column - `new(key, name)` - Create a new column - `width(pixels)` - Set column width - `sortable()` - Make column sortable - `ascending()` - Set default sort to ascending - `descending()` - Set default sort to descending - `text_right()` - Right-align column text - `text_center()` - Center-align column text - `fixed(ColumnFixed)` - Pin column to left - `resizable(bool)` - Enable/disable column resizing - `movable(bool)` - Enable/disable column moving - `selectable(bool)` - Enable/disable column/cell selection - `paddings(edges)` - Set custom padding - `min_width(pixels)` - Set minimum width - `max_width(pixels)` - Set maximum width ### Events - `SelectRow(usize)` - Row selected - `DoubleClickedRow(usize)` - Row double-clicked - `SelectColumn(usize)` - Column selected - `SelectCell(usize, usize)` - Cell selected (row_ix, col_ix) - `DoubleClickedCell(usize, usize)` - Cell double-clicked (row_ix, col_ix) - `RightClickedCell(usize, usize)` - Cell right-clicked (row_ix, col_ix) - `RightClickedRow(Option)` - Row right-clicked - `ColumnWidthsChanged(Vec)` - Column widths changed - `MoveColumn(usize, usize)` - Column moved (from_ix, to_ix) [DataTable]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.DataTable.html [TableState]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.TableState.html [TableDelegate]: https://docs.rs/gpui-component/latest/gpui_component/table/trait.TableDelegate.html [Column]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.Column.html [TableEvent]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.TableEvent.html [ColumnSort]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.ColumnSort.html [ColumnFixed]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.ColumnFixed.html ================================================ FILE: docs/docs/components/date-picker.md ================================================ --- title: DatePicker description: A date picker component for selecting single dates or date ranges with calendar interface. --- # DatePicker A flexible date picker component with calendar interface that supports single date selection, date range selection, custom date formatting, disabled dates, and preset ranges. ## Import ```rust use gpui_component::{ date_picker::{DatePicker, DatePickerState, DateRangePreset, DatePickerEvent}, calendar::{Date, Matcher}, }; ``` ## Usage ### Basic Date Picker ```rust let date_picker = cx.new(|cx| DatePickerState::new(window, cx)); DatePicker::new(&date_picker) ``` ### With Initial Date ```rust use chrono::Local; let date_picker = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx); picker.set_date(Local::now().naive_local().date(), window, cx); picker }); DatePicker::new(&date_picker) ``` ### Date Range Picker ```rust use chrono::{Local, Days}; // Range mode picker let range_picker = cx.new(|cx| DatePickerState::range(window, cx)); DatePicker::new(&range_picker) .number_of_months(2) // Show 2 months for easier range selection // With initial range let range_picker = cx.new(|cx| { let now = Local::now().naive_local().date(); let mut picker = DatePickerState::new(window, cx); picker.set_date( (now, now.checked_add_days(Days::new(7)).unwrap()), window, cx, ); picker }); DatePicker::new(&range_picker) .number_of_months(2) ``` ### With Custom Date Format ```rust let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .date_format("%Y-%m-%d") // ISO format }); DatePicker::new(&date_picker) // Other format examples: // "%m/%d/%Y" -> 12/25/2023 // "%B %d, %Y" -> December 25, 2023 // "%d %b %Y" -> 25 Dec 2023 ``` ### With Placeholder ```rust DatePicker::new(&date_picker) .placeholder("Select a date...") ``` ### Cleanable Date Picker ```rust DatePicker::new(&date_picker) .cleanable(true) // Show clear button when date is selected ``` ### Different Sizes ```rust DatePicker::new(&date_picker).large() DatePicker::new(&date_picker) // medium (default) DatePicker::new(&date_picker).small() ``` ### Disabled State ```rust DatePicker::new(&date_picker).disabled(true) ``` ### Custom Appearance ```rust // Without default styling DatePicker::new(&date_picker).appearance(false) // Use in custom container div() .border_b_2() .px_6() .py_3() .border_color(cx.theme().border) .bg(cx.theme().secondary) .child(DatePicker::new(&date_picker).appearance(false)) ``` ## Date Restrictions ### Disabled Weekends ```rust use gpui_component::calendar; let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(vec![0, 6]) // Sunday=0, Saturday=6 }); DatePicker::new(&date_picker) ``` ### Disabled Date Range ```rust use chrono::{Local, Days}; let now = Local::now().naive_local().date(); let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::range( Some(now), now.checked_add_days(Days::new(7)), )) }); DatePicker::new(&date_picker) ``` ### Disabled Date Interval ```rust let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::interval( Some(now), now.checked_add_days(Days::new(5)) )) }); DatePicker::new(&date_picker) ``` ### Custom Disabled Dates ```rust // Disable first 5 days of each month let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { date.day0() < 5 })) }); DatePicker::new(&date_picker) // Disable all Mondays let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { date.weekday() == chrono::Weekday::Mon })) }); ``` ## Preset Ranges ### Single Date Presets ```rust use chrono::{Utc, Duration}; let presets = vec![ DateRangePreset::single( "Yesterday", (Utc::now() - Duration::days(1)).naive_local().date(), ), DateRangePreset::single( "Last Week", (Utc::now() - Duration::weeks(1)).naive_local().date(), ), DateRangePreset::single( "Last Month", (Utc::now() - Duration::days(30)).naive_local().date(), ), ]; DatePicker::new(&date_picker) .presets(presets) ``` ### Date Range Presets ```rust let range_presets = vec![ DateRangePreset::range( "Last 7 Days", (Utc::now() - Duration::days(7)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 30 Days", (Utc::now() - Duration::days(30)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 90 Days", (Utc::now() - Duration::days(90)).naive_local().date(), Utc::now().naive_local().date(), ), ]; DatePicker::new(&date_picker) .number_of_months(2) .presets(range_presets) ``` ## Handle Date Selection Events ```rust let date_picker = cx.new(|cx| DatePickerState::new(window, cx)); cx.subscribe(&date_picker, |view, _, event, _| { match event { DatePickerEvent::Change(date) => { match date { Date::Single(Some(selected_date)) => { println!("Single date selected: {}", selected_date); } Date::Range(Some(start), Some(end)) => { println!("Date range selected: {} to {}", start, end); } Date::Range(Some(start), None) => { println!("Range start selected: {}", start); } _ => { println!("Date cleared"); } } } } }); ``` ## Multiple Months Display ```rust // Show 2 months side by side (useful for date ranges) DatePicker::new(&date_picker) .number_of_months(2) // Show 3 months DatePicker::new(&date_picker) .number_of_months(3) ``` ## Advanced Examples ### Business Days Only ```rust use chrono::Weekday; let business_days_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })) }); DatePicker::new(&business_days_picker) .placeholder("Select business day") ``` ### Date Range with Max Duration ```rust use chrono::Days; let max_30_days_picker = cx.new(|cx| DatePickerState::range(window, cx)); cx.subscribe(&max_30_days_picker, |view, picker, event, _| { match event { DatePickerEvent::Change(Date::Range(Some(start), Some(end))) => { let duration = end.signed_duration_since(*start).num_days(); if duration > 30 { // Reset to start date only if range exceeds 30 days picker.update(cx, |state, cx| { state.set_date(Date::Range(Some(*start), None), window, cx); }); } } _ => {} } }); DatePicker::new(&max_30_days_picker) .number_of_months(2) .placeholder("Select up to 30 days") ``` ### Quarter Presets ```rust use chrono::{NaiveDate, Datelike}; fn quarter_start(year: i32, quarter: u32) -> NaiveDate { let month = (quarter - 1) * 3 + 1; NaiveDate::from_ymd_opt(year, month, 1).unwrap() } fn quarter_end(year: i32, quarter: u32) -> NaiveDate { let month = quarter * 3; let start = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); NaiveDate::from_ymd_opt(year, month, start.days_in_month()).unwrap() } let year = Local::now().year(); let quarterly_presets = vec![ DateRangePreset::range("Q1", quarter_start(year, 1), quarter_end(year, 1)), DateRangePreset::range("Q2", quarter_start(year, 2), quarter_end(year, 2)), DateRangePreset::range("Q3", quarter_start(year, 3), quarter_end(year, 3)), DateRangePreset::range("Q4", quarter_start(year, 4), quarter_end(year, 4)), ]; DatePicker::new(&date_picker) .presets(quarterly_presets) ``` ## Examples ### Event Date Picker ```rust let event_date = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx) .date_format("%B %d, %Y") .disabled_matcher(calendar::Matcher::custom(|date| { // Disable past dates *date < Local::now().naive_local().date() })); picker }); DatePicker::new(&event_date) .placeholder("Choose event date") .cleanable(true) ``` ### Booking System Date Range ```rust let booking_range = cx.new(|cx| DatePickerState::range(window, cx)); let booking_presets = vec![ DateRangePreset::range("This Weekend", /* weekend dates */), DateRangePreset::range("Next Week", /* next week dates */), DateRangePreset::range("This Month", /* this month dates */), ]; DatePicker::new(&booking_range) .number_of_months(2) .presets(booking_presets) .placeholder("Select check-in and check-out dates") ``` ### Financial Period Selector ```rust let financial_period = cx.new(|cx| { DatePickerState::range(window, cx) .date_format("%Y-%m-%d") }); DatePicker::new(&financial_period) .number_of_months(3) .presets(quarterly_presets) .placeholder("Select reporting period") ``` ================================================ FILE: docs/docs/components/description-list.md ================================================ --- title: DescriptionList description: Use to display details with a tidy layout for key-value pairs. --- # DescriptionList A versatile component for displaying key-value pairs in a structured, organized layout. Supports both horizontal and vertical layouts, multiple columns, borders, and different sizes. Perfect for showing detailed information like metadata, specifications, or summary data. ## Import ```rust use gpui_component::description_list::{DescriptionList, DescriptionItem, DescriptionText}; ``` ## Usage ### Basic Description List ```rust DescriptionList::new() .item("Name", "GPUI Component", 1) .item("Version", "0.1.0", 1) .item("License", "Apache-2.0", 1) ``` ### Using DescriptionItem Builder ```rust DescriptionList::new() .children([ DescriptionItem::new("Name").value("GPUI Component"), DescriptionItem::new("Description").value("UI components for building desktop applications"), DescriptionItem::new("Version").value("0.1.0"), ]) ``` ### Different Layouts ```rust // Horizontal layout (default) DescriptionList::horizontal() .item("Platform", "macOS, Windows, Linux", 1) .item("Repository", "https://github.com/longbridge/gpui-component", 1) // Vertical layout DescriptionList::vertical() .item("Name", "GPUI Component", 1) .item("Description", "A comprehensive UI component library", 1) ``` ### Multiple Columns with Spans ```rust DescriptionList::new() .columns(3) .child(DescriptionItem::new("Name").value("GPUI Component").span(1)) .children([ DescriptionItem::new("Version").value("0.1.0").span(1), DescriptionItem::new("License").value("Apache-2.0").span(1), DescriptionItem::new("Description") .value("Full-featured UI components for desktop applications") .span(3), // Spans all 3 columns DescriptionItem::new("Repository") .value("https://github.com/longbridge/gpui-component") .span(2), // Spans 2 columns ]) ``` ### With Dividers ```rust DescriptionList::new() .item("Name", "GPUI Component", 1) .item("Version", "0.1.0", 1) .divider() // Add a visual separator .item("Author", "Longbridge", 1) .item("License", "Apache-2.0", 1) ``` ### Different Sizes ```rust // Large size DescriptionList::new() .large() .item("Title", "Large Description List", 1) // Medium size (default) DescriptionList::new() .item("Title", "Medium Description List", 1) // Small size DescriptionList::new() .small() .item("Title", "Small Description List", 1) ``` ### Without Borders ```rust DescriptionList::new() .bordered(false) // Remove borders for a cleaner look .item("Name", "GPUI Component", 1) .item("Type", "UI Library", 1) ``` ### Custom Label Width (Horizontal Layout) ```rust use gpui::px; DescriptionList::horizontal() .label_width(px(200.0)) // Set custom label width .item("Very Long Label Name", "Short Value", 1) .item("Short", "Very long value that needs more space", 1) ``` ### Rich Content with Custom Elements ```rust use gpui_component::text::markdown; DescriptionList::new() .columns(2) .children([ DescriptionItem::new("Name").value("GPUI Component"), DescriptionItem::new("Description").value( markdown( "UI components for building **fantastic** desktop applications.", ).into_any_element() ), ]) ``` ### Complex Example with Mixed Content ```rust DescriptionList::new() .columns(3) .label_width(px(150.0)) .children([ DescriptionItem::new("Project Name").value("GPUI Component").span(1), DescriptionItem::new("Version").value("0.1.0").span(1), DescriptionItem::new("Status").value("Active").span(1), DescriptionItem::Divider, // Full-width divider DescriptionItem::new("Description").value( "A comprehensive UI component library for building desktop applications with GPUI" ).span(3), DescriptionItem::new("Repository").value( "https://github.com/longbridge/gpui-component" ).span(2), DescriptionItem::new("License").value("Apache-2.0").span(1), DescriptionItem::new("Platforms").value("macOS, Windows, Linux").span(2), DescriptionItem::new("Language").value("Rust").span(1), ]) ``` ## Examples ### User Profile Information ```rust DescriptionList::new() .columns(2) .bordered(true) .children([ DescriptionItem::new("Full Name").value("John Doe"), DescriptionItem::new("Email").value("john@example.com"), DescriptionItem::new("Phone").value("+1 (555) 123-4567"), DescriptionItem::new("Department").value("Engineering"), DescriptionItem::Divider, DescriptionItem::new("Bio").value( "Senior software engineer with 10+ years of experience in Rust and system programming." ).span(2), ]) ``` ### System Information ```rust DescriptionList::vertical() .small() .bordered(false) .children([ DescriptionItem::new("Operating System").value("macOS 14.0"), DescriptionItem::new("Architecture").value("Apple Silicon (M2)"), DescriptionItem::new("Memory").value("16 GB"), DescriptionItem::new("Storage").value("512 GB SSD"), DescriptionItem::new("GPU").value("Apple M2 10-core GPU"), ]) ``` ### Product Specifications ```rust DescriptionList::new() .columns(3) .large() .children([ DescriptionItem::new("Model").value("MacBook Pro").span(1), DescriptionItem::new("Year").value("2023").span(1), DescriptionItem::new("Screen Size").value("14-inch").span(1), DescriptionItem::new("Processor").value("Apple M2 Pro").span(2), DescriptionItem::new("Base Price").value("$1,999").span(1), DescriptionItem::Divider, DescriptionItem::new("Key Features").value( "Liquid Retina XDR display, ProMotion technology, P3 wide color gamut" ).span(3), ]) ``` ### Configuration Settings ```rust DescriptionList::horizontal() .label_width(px(180.0)) .bordered(false) .children([ DescriptionItem::new("Theme").value("Dark Mode"), DescriptionItem::new("Font Size").value("14px"), DescriptionItem::new("Auto Save").value("Enabled"), DescriptionItem::new("Backup Frequency").value("Every 30 minutes"), DescriptionItem::new("Language").value("English (US)"), ]) ``` ## Design Guidelines - Use horizontal layout for simple key-value pairs - Use vertical layout when values are lengthy or complex - Limit columns to 3-4 for optimal readability - Use dividers to group related information - Keep labels concise and descriptive - Use consistent spacing with the size prop - Consider removing borders for embedded contexts ================================================ FILE: docs/docs/components/dialog.md ================================================ --- title: Dialog description: A dialog dialog for displaying content in a layer above the app. --- # Dialog Dialog component for creating dialogs, confirmations, and alerts. Supports overlay, keyboard shortcuts, and various customizations. ## Import ```rust use gpui_component::dialog::DialogButtonProps; use gpui_component::WindowExt; ``` ## Usage ### Setup application root view for display of dialogs You need to set up your application's root view to render the dialog layer. This is typically done in your main application struct's render method. The [Root::render_dialog_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_dialog_layer) function handles rendering any active dialogs on top of your app content. ```rust use gpui_component::TitleBar; struct MyApp { view: AnyView, } impl Render for MyApp { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let dialog_layer = Root::render_dialog_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(TitleBar::new()) .child(div().flex_1().overflow_hidden().child(self.view.clone())), ) // Render the dialog layer on top of the app content .children(dialog_layer) } } ``` ### Basic Dialog ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Welcome") .child("This is a dialog dialog.") }) ``` ### Form Dialog ```rust let input = cx.new(|cx| InputState::new(window, cx)); window.open_dialog(cx, |dialog, _, _| { dialog .title("User Information") .child( v_flex() .gap_3() .child("Please enter your details:") .child(Input::new(&input)) ) .footer(|_, _, _, _| { vec![ Button::new("ok") .primary() .label("Submit") .on_click(|_, window, cx| { window.close_dialog(cx); }), Button::new("cancel") .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }), ] }) }) ``` ### Dialog with Icon ```rust window.open_dialog(cx, |dialog, _, cx| { dialog .child( h_flex() .gap_3() .child(Icon::new(IconName::TriangleAlert) .size_6() .text_color(cx.theme().warning)) .child("This action cannot be undone.") ) }) ``` ### Scrollable Dialog ```rust use gpui_component::text::markdown; window.open_dialog(cx, |dialog, window, cx| { dialog .h(px(450.)) .title("Long Content") .child(markdown(long_markdown_text)) }) ``` ### Dialog Options ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Custom Dialog") .overlay(true) // Show overlay (default: true) .overlay_closable(true) // Click overlay to close (default: true) .keyboard(true) // ESC to close (default: true) .close_button(false) // Show close button (default: true) .child("Dialog content") }) ``` ### Nested Dialogs ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("First Dialog") .child("This is the first dialog") .footer(|_, _, _, _| { vec![ Button::new("open-another") .label("Open Another Dialog") .on_click(|_, window, cx| { window.open_dialog(cx, |dialog, _, _| { dialog .title("Second Dialog") .child("This is nested") }); }), ] }) }) ``` ### Custom Styling ```rust window.open_dialog(cx, |dialog, _, cx| { dialog .rounded(cx.theme().radius_lg) .bg(cx.theme().cyan) .text_color(cx.theme().info_foreground) .title("Custom Style") .child("Styled dialog content") }) ``` ### Custom Padding ```rust window.open_dialog(cx, |dialog, _, _| { dialog .p_3() // Custom padding .title("Custom Padding") .child("Dialog with custom spacing") }) ``` ### Close Dialog Programmatically The `close_dialog` method can be used to close the active dialog from anywhere within the window context. ```rust // Close top level active dialog. window.close_dialog(cx); // Close and perform action Button::new("submit") .primary() .label("Submit") .on_click(|_, window, cx| { // Do something window.close_dialog(cx); }) ``` ## Declarative API The Dialog component now supports a declarative API that provides a more React-like component composition pattern using dedicated header, title, description, and footer components. ### Import ```rust use gpui_component::dialog::{ Dialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter, }; ``` ### Trigger-based Dialog The trigger-based approach allows you to create a dialog that opens when a trigger element is clicked. The dialog is defined inline with the trigger. ```rust Dialog::new(cx) .trigger( Button::new("open-dialog") .outline() .label("Open Dialog") ) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Account Created")) .child(DialogDescription::new().child( "Your account has been created successfully!", )) ) .child( DialogFooter::new() .border_t_1() .border_color(cx.theme().border) .bg(cx.theme().muted) .child( Button::new("cancel") .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("ok") .primary() .label("Save Changes") ) ) }) ``` ### Content Builder Pattern Use the content builder pattern with `window.open_dialog` for more control over dialog creation: ```rust window.open_dialog(cx, |dialog, _, _| { dialog .w(px(400.)) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Custom Width")) .child(DialogDescription::new().child( "This dialog has a custom width of 400px.", )) ) .child(div().child( "Content area with custom width configuration." )) .child( DialogFooter::new() .justify_center() .child( Button::new("cancel") .flex_1() .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("done") .flex_1() .primary() .label("Done") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) ) }) }) ``` ### Declarative Components #### DialogHeader Container for the dialog's title and description section. ```rust DialogHeader::new() .child(DialogTitle::new().child("Title")) .child(DialogDescription::new().child("Description")) ``` #### DialogTitle Displays the main title of the dialog with semantic styling. ```rust DialogTitle::new() .child("Account Settings") ``` #### DialogDescription Displays descriptive text below the title with muted styling. ```rust DialogDescription::new() .child("Update your account settings and preferences here.") ``` #### DialogFooter Container for action buttons and footer content. Automatically applies proper spacing and alignment. ```rust DialogFooter::new() .bg(cx.theme().muted) .border_t_1() .border_color(cx.theme().border) .child(Button::new("cancel").outline().label("Cancel")) .child(Button::new("save").primary().label("Save")) ``` ### Form Dialog with Declarative API ```rust let name_input = cx.new(|cx| InputState::new(window, cx)); let email_input = cx.new(|cx| InputState::new(window, cx)); Dialog::new(cx) .trigger(Button::new("edit-profile").label("Edit Profile")) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Edit Profile")) .child(DialogDescription::new().child( "Make changes to your profile here. Click save when done." )) ) .child( v_flex() .gap_4() .py_4() .child( v_flex() .gap_2() .child("Name") .child(Input::new(&name_input).placeholder("Enter your name")) ) .child( v_flex() .gap_2() .child("Email") .child(Input::new(&email_input).placeholder("Enter your email")) ) ) .child( DialogFooter::new() .child(Button::new("cancel").outline().label("Cancel")) .child(Button::new("save").primary().label("Save Changes")) ) }) ``` ### Styled Footer Customize the footer appearance with background colors, borders, and alignment: ```rust DialogFooter::new() .justify_center() // Center align buttons .bg(cx.theme().muted) // Background color .border_t_1() // Top border .border_color(cx.theme().border) .child(Button::new("btn1").flex_1().label("Cancel")) .child(Button::new("btn2").flex_1().primary().label("Confirm")) ``` ### DialogContent Container The `DialogContent` component provides a flexible container for dialog body content: ```rust use gpui_component::dialog::DialogContent; window.open_dialog(cx, |dialog, _, _| { dialog.content(|content, _, cx| { content .child(DialogHeader::new() .child(DialogTitle::new().child("Settings")) .child(DialogDescription::new().child("Configure your preferences")) ) .child( div() .py_4() .child("Main content area") ) .child(DialogFooter::new() .child(Button::new("close").label("Close")) ) }) }) ``` ## API Reference - Declarative Components ### Dialog | Method | Description | | ------------------------ | ----------------------------------------------------- | | `new(cx)` | Create a new Dialog (no longer requires window param) | | `trigger(element)` | Set trigger element that opens the dialog | | `content(builder)` | Set content using a builder function | | `w(px)` / `width(px)` | Set dialog width | | `max_w(px)` | Set maximum width | | `margin_top(px)` | Set top margin | | `overlay(bool)` | Show/hide overlay (default: true) | | `overlay_closable(bool)` | Allow closing by clicking overlay (default: true) | | `keyboard(bool)` | Allow closing with ESC key (default: true) | | `close_button(bool)` | Show/hide close button (default: true) | ### DialogContent Container for dialog body content. Automatically applies padding and flex layout. ```rust DialogContent::new() .child(DialogHeader::new()...) .child(/* your content */) .child(DialogFooter::new()...) ``` ### DialogHeader Container for title and description. Automatically applies vertical flex layout with proper gap. ```rust DialogHeader::new() .child(DialogTitle::new().child("Title")) .child(DialogDescription::new().child("Description")) ``` ### DialogTitle Displays the dialog title with semantic styling (font-semibold, proper line-height). ```rust DialogTitle::new() .child("Dialog Title") ``` ### DialogDescription Displays descriptive text with muted foreground color and proper text sizing. ```rust DialogDescription::new() .child("This is a description text that provides more context.") ``` ### DialogFooter Container for footer buttons with automatic spacing and alignment. ```rust DialogFooter::new() .justify_end() // Right align (default) .child(Button::new("btn1").label("Cancel")) .child(Button::new("btn2").primary().label("OK")) ``` ## Breaking Changes ### Dialog::new() Signature Change The `Dialog::new()` constructor no longer requires a `window` parameter: ```rust // Old API (deprecated) Dialog::new(window, cx) // New API Dialog::new(cx) ``` ### Content Builder Function The `.content()` method now accepts a builder function instead of a pre-built `DialogContent`: ```rust // Old approach (still works) dialog.child(DialogHeader::new()...) // New declarative API dialog.content(|content, window, cx| { content .child(DialogHeader::new()...) .child(DialogFooter::new()...) }) ``` ## Best Practices 1. **Use Declarative Components**: Prefer `DialogHeader`, `DialogTitle`, `DialogDescription`, and `DialogFooter` for consistent styling 2. **Trigger-based for Simple Cases**: Use the trigger pattern for straightforward dialogs that open from a button 3. **Builder Pattern for Complex Dialogs**: Use `window.open_dialog` with content builder for dialogs requiring complex logic or state 4. **Semantic Structure**: Always include `DialogHeader` with title and description for accessibility 5. **Consistent Footer**: Use `DialogFooter` for all action buttons to maintain visual consistency 6. **Proper Sizing**: Explicitly set dialog width when content requires specific dimensions ================================================ FILE: docs/docs/components/dropdown_button.md ================================================ --- title: DropdownButton description: A DropdownButton is a combination of a button and a trigger button. It allows us to display a dropdown menu when the trigger is clicked, but the left Button can still respond to independent events. --- # DropdownButton A [DropdownButton] is a combination of a button and a trigger button. It allows us to display a dropdown menu when the trigger is clicked, but the left Button can still respond to independent events. And more option methods of [Button] are also available for the DropdownButton, such as setting different variants using [ButtonCustomVariant], sizes using [Sizable], adding icons, loading states. ## Import ```rust use gpui_component::button::{Button, DropdownButton}; ``` ## Usage ```rust use gpui::Corner; DropdownButton::new("dropdown") .button(Button::new("btn").label("Click Me")) .dropdown_menu(|menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) .menu("Option 2", Box::new(MyAction)) .separator() .menu("Option 3", Box::new(MyAction)) }) ``` ### Variants Same as [Button], DropdownButton supports different variants. ````rust DropdownButton::new("dropdown") .primary() .button(Button::new("btn").label("Primary")) .dropdown_menu(|menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) }) ``` ### With custom anchor ```rust // With custom anchor DropdownButton::new("dropdown") .button(Button::new("btn").label("Click Me")) .dropdown_menu_with_anchor(Corner::BottomRight, |menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) }) ```` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [DropdownButton]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.DropdownButton.html [ButtonCustomVariant]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonCustomVariant.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ================================================ FILE: docs/docs/components/editor.md ================================================ --- title: Editor description: Multi-line text input component with auto-resize, validation, and advanced editing features. --- # Editor A powerful multi-line text input component that extends the basic input functionality with support for multiple lines, auto-resizing, syntax highlighting, line numbers, and code editing features. Perfect for forms, code editors, and content editing. ## Import ```rust use gpui_component::input::{InputState, Input}; ``` ## Usage ### Textarea ```rust let state = cx.new(|cx| InputState::new(window, cx) .multi_line(true) .placeholder("Enter your message...") ); Input::new(&state) ``` With fixed height Textarea: ```rust let state = cx.new(|cx| InputState::new(window, cx) .multi_line(true) .rows(10) // Set number of rows .placeholder("Enter text here...") ); Input::new(&state) .h(px(320.)) // Set explicit height ``` ### AutoGrow ```rust let state = cx.new(|cx| InputState::new(window, cx) .auto_grow(1, 5) // min_rows: 1, max_rows: 5 .placeholder("Type here and watch it grow...") ); Input::new(&state) ``` ### CodeEditor GPUI Component's `InputState` supports a code editor mode with syntax highlighting, line numbers, and search functionality. It design for high performance and can handle large files efficiently. We used [tree-sitter](https://tree-sitter.github.io/tree-sitter/) for syntax highlighting, and [ropey](https://github.com/cessen/ropey) for text storage and manipulation. ```rust let state = cx.new(|cx| InputState::new(window, cx) .code_editor("rust") // Language for syntax highlighting .line_number(true) // Show line numbers .searchable(true) // Enable search functionality .show_whitespaces(true) // Show whitespace characters .default_value("fn main() {\n println!(\"Hello, world!\");\n}") ); Input::new(&state) .h_full() // Full height ``` #### Single Line Mode Sometimes you may want to use the code editor features but restrict input to a single line, for example for code snippets or commands. ```rust let state = cx.new(|cx| InputState::new(window, cx) .code_editor("rust") .multi_line(false) // Single line .default_value("println!(\"Hello, world!\");") ); Input::new(&state) ``` ### TabSize ```rust use gpui_component::input::TabSize; let state = cx.new(|cx| InputState::new(window, cx) .multi_line(true) .tab_size(TabSize { tab_size: 4, hard_tabs: false, // Use spaces instead of tabs }) ); Input::new(&state) ``` ### Searchable The search feature allows for all multi-line inputs to support searching through the content using `Ctrl+F` (or `Cmd+F` on Mac). It provides a search bar with options to navigate between matches and highlight them. Use `searchable` method to enable: ```rust let state = cx.new(|cx| InputState::new(window, cx) .multi_line(true) .searchable(true) // Enable Ctrl+F search .rows(15) .default_value("Search through this content...") ); Input::new(&state) ``` ### SoftWrap By default multi-line inputs have soft wrapping enabled, meaning long lines will wrap to fit the width of the textarea. You can disable soft wrapping to allow horizontal scrolling instead: ```rust // With soft wrap (default) let state = cx.new(|cx| InputState::new(window, cx) .multi_line(true) .soft_wrap(true) .rows(6) ); // Without soft wrap (horizontal scrolling) let state = cx.new(|cx| InputState::new(window, cx) .multi_line(true) .soft_wrap(false) .rows(6) .default_value("This is a very long line that will not wrap automatically but will show horizontal scrollbar instead.") ); ``` ### Text Manipulation ```rust // Insert text at cursor position state.update(cx, |state, cx| { state.insert("inserted text", window, cx); }); // Replace all content state.update(cx, |state, cx| { state.replace("new content", window, cx); }); // Set cursor position state.update(cx, |state, cx| { state.set_cursor_position(Position { line: 2, character: 5 }, window, cx); }); // Get cursor position let position = state.read(cx).cursor_position(); println!("Line: {}, Column: {}", position.line, position.character); ``` ### Validation ```rust let state = cx.new(|cx| InputState::new(window, cx) .multi_line(true) .validate(|text, _| { // Validate that content is not empty and under 1000 chars !text.trim().is_empty() && text.len() <= 1000 }) ); Input::new(&state) ``` ### Handle Events ```rust cx.subscribe_in(&state, window, |view, state, event, window, cx| { match event { InputEvent::Change => { let content = state.read(cx).value(); println!("Content changed: {} characters", content.len()); } InputEvent::PressEnter { secondary } => { if secondary { println!("Shift+Enter pressed - insert line break"); } else { println!("Enter pressed - could submit form"); } } InputEvent::Focus => println!("Textarea focused"), InputEvent::Blur => println!("Textarea blurred"), } }); ``` ### Disabled State ```rust Input::new(&state) .disabled(true) .h(px(200.)) ``` ### Custom Styling ```rust // Without default appearance Input::new(&state) .appearance(false) .h(px(200.)) // Custom container styling div() .bg(cx.theme().background) .border_2() .border_color(cx.theme().input) .rounded(cx.theme().radius_lg) .p_4() .child( Input::new(&state) .appearance(false) .h(px(150.)) ) ``` ## Examples ### Comment Box ```rust struct CommentBox { state: Entity, char_limit: usize, } impl CommentBox { fn new(window: &mut Window, cx: &mut Context) -> Self { let state = cx.new(|cx| InputState::new(window, cx) .auto_grow(3, 8) .placeholder("Write your comment...") .validate(|text, _| text.len() <= 500) ); Self { state, char_limit: 500, } } } impl Render for CommentBox { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let content = self.state.read(cx).value(); let char_count = content.len(); let remaining = self.char_limit.saturating_sub(char_count); v_flex() .gap_2() .child(Input::new(&self.state)) .child( h_flex() .justify_between() .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child(format!("{} characters remaining", remaining)) ) .child( Button::new("submit") .primary() .disabled(char_count == 0 || char_count > self.char_limit) .label("Post Comment") ) ) } } ``` ### Code Editor with Language Selection ```rust struct CodeEditor { editor: Entity, language: String, } impl CodeEditor { fn set_language(&mut self, language: String, window: &mut Window, cx: &mut Context) { self.language = language.clone(); self.editor.update(cx, |editor, cx| { editor.set_highlighter(language, cx); }); } } impl Render for CodeEditor { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( h_flex() .gap_2() .child("Language:") .child( // Language selector dropdown would go here div().child(self.language.clone()) ) ) .child( Input::new(&self.editor) .h(px(400.)) .bordered(true) ) } } ``` ### Text Editor with Toolbar ```rust struct TextEditor { editor: Entity, } impl TextEditor { fn format_bold(&mut self, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, cx| { if !editor.selected_range.is_empty() { let selected = editor.selected_text().to_string(); editor.replace(&format!("**{}**", selected), window, cx); } }); } } impl Render for TextEditor { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child( h_flex() .gap_1() .p_2() .border_b_1() .border_color(cx.theme().border) .child( Button::new("bold") .ghost() .icon(IconName::Bold) .on_click(cx.listener(Self::format_bold)) ) .child( Button::new("italic") .ghost() .icon(IconName::Italic) ) ) .child( Input::new(&self.editor) .h(px(300.)) ) } } ``` ================================================ FILE: docs/docs/components/focus-trap.md ================================================ --- title: Focus Trap description: A utility element that traps keyboard focus within a container, preventing Tab navigation from escaping. --- # Focus Trap Focus trap utility for constraining keyboard focus within a specific container. Essential for modal dialogs, sheets, and overlay components to provide proper keyboard navigation accessibility. **Note:** [Dialog](/docs/components/dialog) and [Sheet](/docs/components/sheet) components have focus trap built-in. You only need to manually use `focus_trap()` for custom modal-like components. ## Import ```rust use gpui_component::FocusTrapElement; ``` ## Usage ### Basic Focus Trap ```rust let container_handle = cx.focus_handle(); v_flex() .child(Button::new("btn1").label("Button 1")) .child(Button::new("btn2").label("Button 2")) .child(Button::new("btn3").label("Button 3")) .focus_trap("trap1", &container_handle) // Pressing Tab will cycle: btn1 -> btn2 -> btn3 -> btn1 // Focus will not escape to elements outside this container ``` ### Multiple Focus Traps You can have multiple independent focus trap areas in your application. Each trap operates independently: ```rust let trap1_handle = cx.focus_handle(); let trap2_handle = cx.focus_handle(); v_flex() .gap_4() // First focus trap area .child( h_flex() .gap_2() .child(Button::new("trap1-1").label("Area 1 - Button 1")) .child(Button::new("trap1-2").label("Area 1 - Button 2")) .child(Button::new("trap1-3").label("Area 1 - Button 3")) .focus_trap("trap1", &trap1_handle) ) // Second focus trap area .child( h_flex() .gap_2() .child(Button::new("trap2-1").label("Area 2 - Button 1")) .child(Button::new("trap2-2").label("Area 2 - Button 2")) .focus_trap("trap2", &trap2_handle) ) ``` ### Focus Trap with Dialog [Dialog] components have focus trap built-in automatically. You don't need to manually add `focus_trap()`: ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Settings") .child( v_flex() .gap_3() .child(Button::new("save").label("Save")) .child(Button::new("cancel").label("Cancel")) .child(Button::new("reset").label("Reset")) ) // Dialog internally uses focus_trap() // Tab navigation automatically cycles: save -> cancel -> reset -> save }) ``` ### Focus Trap with Sheet [Sheet] components also have focus trap built-in automatically: ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Filter Options") .child( v_flex() .gap_2() .child(Checkbox::new("option1").label("Option 1")) .child(Checkbox::new("option2").label("Option 2")) .child(Button::new("apply").label("Apply Filters")) ) // Sheet internally uses focus_trap() // Focus automatically cycles within the sheet panel }) ``` ## How It Works The focus trap system consists of three key components: 1. **FocusTrapContainer**: Wraps any container element and registers it as a focus trap area 2. **FocusTrapManager**: Global state manager that tracks all active focus traps 3. **Root Integration**: The [Root] view intercepts Tab/Shift-Tab events and enforces focus cycling When Tab or Shift-Tab is pressed: 1. [Root] detects if the currently focused element is inside a focus trap 2. If yes, it calculates the next focusable element within the same trap 3. If focus would escape the trap, it cycles back to the beginning (Tab) or end (Shift-Tab) 4. This prevents focus from leaving the trapped container ### Built-in Focus Trap Components The following components have focus trap functionality built-in and don't require manual `focus_trap()` calls: - **[Dialog]** - Modal dialogs automatically trap focus (see `dialog.rs:437`) - **[Sheet]** - Side panels automatically trap focus (see `sheet.rs:197`) ## API Reference - [FocusTrapElement](https://docs.rs/gpui-component/latest/gpui_component/trait.FocusTrapElement.html) - [FocusTrapContainer](https://docs.rs/gpui-component/latest/gpui_component/struct.FocusTrapContainer.html) ## Examples ### Custom Modal with Focus Trap ```rust struct CustomModal { container_handle: FocusHandle, } impl CustomModal { fn new(cx: &mut App) -> Self { Self { container_handle: cx.focus_handle(), } } } impl Render for CustomModal { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .absolute() .inset_0() .flex() .items_center() .justify_center() .child( v_flex() .gap_4() .p_6() .bg(cx.theme().background) .rounded(cx.theme().radius_lg) .shadow_lg() .border_1() .border_color(cx.theme().border) .child("This is a modal dialog") .child( h_flex() .gap_2() .child(Button::new("ok").primary().label("OK")) .child(Button::new("cancel").label("Cancel")) ) .focus_trap("modal", &self.container_handle) ) } } ``` ### Nested Focus Traps Focus traps support nesting. When multiple traps are active, the innermost trap takes precedence: ```rust let outer_handle = cx.focus_handle(); let inner_handle = cx.focus_handle(); div() .child( v_flex() .gap_4() .p_4() .border_1() .border_color(cx.theme().border) .child(Button::new("outer-1").label("Outer Button 1")) .child( // Inner trap takes precedence when focused h_flex() .gap_2() .p_4() .bg(cx.theme().accent.opacity(0.1)) .child(Button::new("inner-1").label("Inner Button 1")) .child(Button::new("inner-2").label("Inner Button 2")) .focus_trap("inner", &inner_handle) ) .child(Button::new("outer-2").label("Outer Button 2")) .focus_trap("outer", &outer_handle) ) ``` ### Conditional Focus Trap You can conditionally apply focus trapping based on application state: ```rust struct ModalView { is_modal: bool, container_handle: FocusHandle, } impl Render for ModalView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let content = v_flex() .gap_2() .child(Button::new("btn1").label("Button 1")) .child(Button::new("btn2").label("Button 2")) .child(Button::new("btn3").label("Button 3")); if self.is_modal { // Apply focus trap when in modal mode content.focus_trap("conditional", &self.container_handle) .into_any_element() } else { // Normal behavior without focus trap content.into_any_element() } } } ``` ## Accessibility Notes - Focus trapping is essential for modal dialogs and overlays to meet WCAG accessibility guidelines - Always provide a way to close or dismiss trapped focus areas (ESC key, close button) - The first focusable element in the trap should receive focus when the trap is activated - Use focus traps sparingly - only for truly modal interactions - Ensure keyboard navigation order is logical within the trapped area ## See Also - [Root View System](/docs/root) - Manages focus trap behavior at the window level - [Dialog](/docs/components/dialog) - Uses focus trap automatically - [Sheet](/docs/components/sheet) - Uses focus trap automatically - [focus-trap-react](https://github.com/focus-trap/focus-trap-react) - Similar concept for React applications [Root]: https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html [FocusTrapElement]: https://docs.rs/gpui-component/latest/gpui_component/trait.FocusTrapElement.html [Dialog]: /docs/components/dialog [Sheet]: /docs/components/sheet ================================================ FILE: docs/docs/components/form.md ================================================ --- title: Form description: Flexible form container with support for field layout, validation, and multi-column layouts. --- # Form A comprehensive form component that provides structured layout for form fields with support for vertical/horizontal layouts, validation, field groups, and responsive multi-column layouts. ## Import ```rust use gpui_component::form::{field, v_form, h_form, Form, Field}; ``` ## Usage ### Basic Form ```rust v_form() .child( field() .label("Name") .child(Input::new(&name_input)) ) .child( field() .label("Email") .child(Input::new(&email_input)) .required(true) ) ``` ### Horizontal Form Layout ```rust h_form() .label_width(px(120.)) .child( field() .label("First Name") .child(Input::new(&first_name)) ) .child( field() .label("Last Name") .child(Input::new(&last_name)) ) ``` ### Multi-Column Form ```rust v_form() .columns(2) // Two-column layout .child( field() .label("First Name") .child(Input::new(&first_name)) ) .child( field() .label("Last Name") .child(Input::new(&last_name)) ) .child( field() .label("Bio") .col_span(2) // Span across both columns .child(Input::new(&bio_input)) ) ``` ## Form Container and Layout ### Vertical Layout (Default) ```rust v_form() .gap(px(12.)) .child(field().label("Name").child(input)) .child(field().label("Email").child(email_input)) ``` ### Horizontal Layout ```rust h_form() .label_width(px(100.)) .child(field().label("Name").child(input)) .child(field().label("Email").child(email_input)) ``` ### Custom Sizing ```rust v_form() .large() // Large form size .label_text_size(rems(1.2)) .child(field().label("Title").child(input)) v_form() .small() // Small form size .child(field().label("Code").child(input)) ``` ## Form Validation ### Required Fields ```rust field() .label("Email") .required(true) // Shows asterisk (*) next to label .child(Input::new(&email_input)) ``` ### Field Descriptions ```rust field() .label("Password") .description("Must be at least 8 characters long") .child(Input::new(&password_input)) ``` ### Dynamic Descriptions ```rust field() .label("Bio") .description_fn(|_, _| { div().child("Use at most 100 words to describe yourself.") }) .child(Input::new(&bio_input)) ``` ### Field Visibility ```rust field() .label("Admin Settings") .visible(user.is_admin()) // Conditionally show field .child(Switch::new("admin-mode")) ``` ## Submit Handling ### Basic Submit Pattern ```rust struct FormView { name_input: Entity, email_input: Entity, } impl FormView { fn submit(&mut self, cx: &mut Context) { let name = self.name_input.read(cx).value(); let email = self.email_input.read(cx).value(); // Validate inputs if name.is_empty() || email.is_empty() { // Show validation error return; } // Submit form data self.handle_submit(name, email, cx); } } // Form with submit button v_form() .child(field().label("Name").child(Input::new(&self.name_input))) .child(field().label("Email").child(Input::new(&self.email_input))) .child( field() .label_indent(false) .child( Button::new("submit") .primary() .child("Submit") .on_click(cx.listener(|this, _, _, cx| this.submit(cx))) ) ) ``` ### Form with Action Buttons ```rust v_form() .child(field().label("Title").child(Input::new(&title))) .child(field().label("Content").child(Input::new(&content))) .child( field() .label_indent(false) .child( h_flex() .gap_2() .child(Button::new("save").primary().child("Save")) .child(Button::new("cancel").child("Cancel")) .child(Button::new("preview").outline().child("Preview")) ) ) ``` ## Field Groups ### Related Fields ```rust v_form() .child( field() .label("Name") .child( h_flex() .gap_2() .child(div().flex_1().child(Input::new(&first_name))) .child(div().flex_1().child(Input::new(&last_name))) ) ) .child( field() .label("Address") .items_start() // Align to start for multi-line content .child( v_flex() .gap_2() .child(Input::new(&street)) .child( h_flex() .gap_2() .child(div().flex_1().child(Input::new(&city))) .child(div().w(px(100.)).child(Input::new(&zip))) ) ) ) ``` ### Custom Field Components ```rust field() .label("Theme Color") .child(ColorPicker::new(&color_state).small()) field() .label("Birth Date") .description("We'll send you a birthday gift!") .child(DatePicker::new(&date_state)) field() .label("Notifications") .child( v_flex() .gap_2() .child(Switch::new("email").label("Email notifications")) .child(Switch::new("push").label("Push notifications")) .child(Switch::new("sms").label("SMS notifications")) ) ``` ### Conditional Fields ```rust v_form() .child( field() .label("Account Type") .child(Select::new(&account_type)) ) .child( field() .label("Company Name") .visible(is_business_account) // Show only for business accounts .child(Input::new(&company_name)) ) .child( field() .label("Tax ID") .visible(is_business_account) .required(is_business_account) .child(Input::new(&tax_id)) ) ``` ## Grid Layout and Positioning ### Column Spanning ```rust v_form() .columns(3) // Three-column grid .child(field().label("First").child(input1)) .child(field().label("Second").child(input2)) .child(field().label("Third").child(input3)) .child( field() .label("Full Width") .col_span(3) // Spans all three columns .child(Input::new(&full_width)) ) ``` ### Column Positioning ```rust v_form() .columns(4) .child(field().label("A").child(input_a)) .child(field().label("B").child(input_b)) .child( field() .label("Positioned") .col_start(1) // Start at column 1 .col_span(2) // Span 2 columns .child(input_positioned) ) ``` ### Responsive Layout ```rust v_form() .columns(if is_mobile { 1 } else { 2 }) .child(field().label("Name").child(name_input)) .child(field().label("Email").child(email_input)) .child( field() .label("Bio") .when(!is_mobile, |field| field.col_span(2)) .child(bio_input) ) ``` ## Examples ### User Registration Form ```rust struct RegistrationForm { first_name: Entity, last_name: Entity, email: Entity, password: Entity, confirm_password: Entity, terms_accepted: bool, } impl Render for RegistrationForm { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_form() .large() .child( field() .label("Personal Information") .label_indent(false) .child( h_flex() .gap_3() .child( div().flex_1().child( Input::new(&self.first_name) .placeholder("First name") ) ) .child( div().flex_1().child( Input::new(&self.last_name) .placeholder("Last name") ) ) ) ) .child( field() .label("Email") .required(true) .child(Input::new(&self.email)) ) .child( field() .label("Password") .required(true) .description("Must be at least 8 characters") .child(Input::new(&self.password)) ) .child( field() .label("Confirm Password") .required(true) .child(Input::new(&self.confirm_password)) ) .child( field() .label_indent(false) .child( Checkbox::new("terms") .label("I agree to the Terms of Service") .checked(self.terms_accepted) .on_click(cx.listener(|this, checked, _, cx| { this.terms_accepted = *checked; cx.notify(); })) ) ) .child( field() .label_indent(false) .child( Button::new("register") .primary() .large() .w_full() .child("Create Account") ) ) } } ``` ### Settings Form with Sections ```rust v_form() .column(2) .child( field() .label("Profile") .label_indent(false) .col_span(2) .child(Divider::horizontal()) ) .child( field() .label("Display Name") .child(Input::new(&display_name)) ) .child( field() .label("Email") .child(Input::new(&email)) ) .child( field() .label("Bio") .col_span(2) .items_start() .child(Input::new(&bio)) ) .child( field() .label("Preferences") .label_indent(false) .col_span(2) .child(Divider::horizontal()) ) .child( field() .label("Theme") .child(Select::new(&theme_state)) ) .child( field() .label("Language") .child(Select::new(&language_state)) ) .child( field() .label_indent(false) .child(Switch::new("notifications").label("Enable notifications")) ) .child( field() .label_indent(false) .child(Switch::new("marketing").label("Marketing emails")) ) ``` ### Contact Form ```rust v_form() .child( field() .label("Contact Information") .child( h_flex() .gap_2() .child( Select::new(&prefix_state) .w(px(80.)) ) .child( div().flex_1().child( Input::new(&name_input) .placeholder("Your name") ) ) ) ) .child( field() .label("Email") .required(true) .child(Input::new(&email_input)) ) .child( field() .label("Subject") .child(Select::new(&subject_state)) ) .child( field() .label("Message") .required(true) .items_start() .description("Please describe your inquiry in detail") .child(Input::new(&message_input)) ) .child( field() .label_indent(false) .child( h_flex() .gap_2() .justify_between() .child( Checkbox::new("copy") .label("Send me a copy") ) .child( h_flex() .gap_2() .child(Button::new("cancel").child("Cancel")) .child(Button::new("send").primary().child("Send Message")) ) ) ) ``` ================================================ FILE: docs/docs/components/group-box.md ================================================ --- title: GroupBox description: A styled container element with an optional title to group related content together. --- # GroupBox The GroupBox component is a versatile container that groups related content together with optional borders, backgrounds, and titles. It provides visual organization and semantic grouping for form controls, settings panels, and other related UI elements. ## Import ```rust use gpui_component::group_box::{GroupBox, GroupBoxVariant, GroupBoxVariants as _}; ``` ## Usage ### Basic GroupBox ```rust GroupBox::new() .child("Subscriptions") .child(Checkbox::new("all").label("All")) .child(Checkbox::new("newsletter").label("Newsletter")) .child(Button::new("save").primary().label("Save")) ``` ### GroupBox Variants ```rust // Normal variant (default) - no background or border GroupBox::new() .child("Content without visual container") // Fill variant - with background color GroupBox::new() .fill() .title("Settings") .child("Content with background") // Outline variant - with border, no background GroupBox::new() .outline() .title("Preferences") .child("Content with border") ``` ### With Title ```rust GroupBox::new() .fill() .title("Account Settings") .child( h_flex() .justify_between() .child("Make profile private") .child(Switch::new("privacy").checked(false)) ) .child(Button::new("save").primary().label("Save Changes")) ``` ### Custom ID ```rust GroupBox::new() .id("user-preferences") .outline() .title("User Preferences") .child("Preference controls...") ``` ### Custom Title Styling ```rust use gpui::{StyleRefinement, relative}; GroupBox::new() .outline() .title("Custom Title") .title_style( StyleRefinement::default() .font_semibold() .line_height(relative(1.0)) .px_3() .text_color(cx.theme().accent) ) .child("Content with custom title styling") ``` ### Custom Content Styling ```rust GroupBox::new() .fill() .title("Custom Content Area") .content_style( StyleRefinement::default() .rounded_xl() .py_3() .px_4() .border_2() .border_color(cx.theme().accent) ) .child("Content with custom styling") ``` ### Complex Example ```rust GroupBox::new() .id("notification-settings") .outline() .bg(cx.theme().group_box) .rounded_xl() .p_5() .title("Notification Preferences") .title_style( StyleRefinement::default() .font_semibold() .line_height(relative(1.0)) .px_3() ) .content_style( StyleRefinement::default() .rounded_xl() .py_3() .px_4() .border_2() ) .child( v_flex() .gap_3() .child( h_flex() .justify_between() .child("Email notifications") .child(Switch::new("email").checked(true)) ) .child( h_flex() .justify_between() .child("Push notifications") .child(Switch::new("push").checked(false)) ) .child( h_flex() .justify_between() .child("SMS notifications") .child(Switch::new("sms").checked(false)) ) ) .child( h_flex() .justify_end() .gap_2() .child(Button::new("cancel").label("Cancel")) .child(Button::new("save").primary().label("Save Settings")) ) ``` ## Examples ### Form Section ```rust GroupBox::new() .fill() .title("Personal Information") .child( v_flex() .gap_4() .child( h_flex() .gap_2() .child(Input::new("first-name").placeholder("First Name")) .child(Input::new("last-name").placeholder("Last Name")) ) .child(Input::new("email").placeholder("Email Address")) .child( h_flex() .justify_end() .child(Button::new("update").primary().label("Update Profile")) ) ) ``` ### Settings Panel ```rust GroupBox::new() .outline() .title("Display Settings") .child( v_flex() .gap_3() .child( h_flex() .justify_between() .child(Label::new("Theme")) .child( RadioGroup::horizontal("theme") .child(Radio::new("light").label("Light")) .child(Radio::new("dark").label("Dark")) .child(Radio::new("auto").label("Auto")) ) ) .child( h_flex() .justify_between() .child(Label::new("Font Size")) .child( Select::new("font-size") .option("small", "Small") .option("medium", "Medium") .option("large", "Large") ) ) ) ``` ### Subscription Management ```rust GroupBox::new() .title("Email Subscriptions") .child( v_flex() .gap_2() .child(Checkbox::new("newsletter").label("Weekly Newsletter")) .child(Checkbox::new("updates").label("Product Updates")) .child(Checkbox::new("security").label("Security Alerts")) .child(Checkbox::new("marketing").label("Marketing Communications")) ) .child( h_flex() .justify_between() .mt_4() .child(Button::new("unsubscribe-all").link().label("Unsubscribe All")) .child(Button::new("save").primary().label("Update Preferences")) ) ``` ### Without Title ```rust GroupBox::new() .outline() .child( h_flex() .justify_between() .items_center() .child("Enable two-factor authentication") .child(Switch::new("2fa").checked(false)) ) ``` ## Styling The GroupBox component supports extensive customization through both built-in variants and custom styling: ### Theme Integration ```rust // Using theme colors GroupBox::new() .fill() .bg(cx.theme().group_box) .title("Themed Group Box") ``` ### Custom Appearance ```rust GroupBox::new() .outline() .border_2() .border_color(cx.theme().accent) .rounded(cx.theme().radius_lg) .title("Custom Styled Group Box") .title_style( StyleRefinement::default() .text_color(cx.theme().accent) .font_bold() ) ``` ## Best Practices 1. **Use titles for clarity** - Always include a descriptive title when grouping form controls 2. **Choose appropriate variants** - Use `fill()` for primary content groups, `outline()` for secondary groupings 3. **Maintain visual hierarchy** - Use GroupBox to create clear sections without overwhelming the interface 4. **Group related content** - Only group logically related controls and information 5. **Consider spacing** - The component automatically handles internal spacing, but consider external margins 6. **Responsive design** - GroupBox adapts well to different screen sizes and container widths ## Related Components - **Form**: Use GroupBox within forms to organize sections - **Dialog**: GroupBox works well within dialogs for organizing content - **Accordion**: For collapsible grouped content, consider using Accordion instead - **Card**: For elevated content containers with more visual weight ================================================ FILE: docs/docs/components/hover-card.md ================================================ --- title: HoverCard description: A floating overlay that displays rich content when hovering over a trigger element. --- # HoverCard HoverCard component for displaying rich content that appears when the mouse hovers over a trigger element. Ideal for previewing user profiles, link previews, and other contextual information without requiring a click. Features configurable delays for both opening and closing to prevent flickering during quick mouse movements. This is most like the [Popover] component, but triggered by hover instead of click, and with timing controls for a smoother user experience. ## Import ```rust use gpui_component::hover_card::HoverCard; ``` ## Usage ### Basic HoverCard ```rust use gpui::{ParentElement as _, Styled as _}; use gpui_component::{hover_card::HoverCard, v_flex}; HoverCard::new("basic") .trigger( div() .child("Hover over me") .text_color(cx.theme().primary) .cursor_pointer() .text_sm() ) .child( v_flex() .gap_2() .child( div() .child("This is a hover card") .font_semibold() .text_sm() ) .child( div() .child("You can display rich content when hovering over a trigger element.") .text_color(cx.theme().muted_foreground) .text_sm() ) ) ``` ### User Profile Preview A common use case is showing user profiles when hovering over a username, similar to GitHub or Twitter: ```rust use gpui::{px, relative, Styled as _}; use gpui_component::{ avatar::Avatar, hover_card::HoverCard, h_flex, v_flex, }; h_flex() .child("Hover over ") .text_sm() .child( HoverCard::new("user-profile") .trigger( div() .child("@huacnlee") .cursor_pointer() .text_color(cx.theme().link) ) .child( h_flex() .w(px(320.)) .gap_4() .items_start() .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/5518?s=64") ) .child( v_flex() .gap_1() .line_height(relative(1.)) .child(div().child("Jason Lee").font_semibold()) .child( div() .child("@huacnlee") .text_color(cx.theme().muted_foreground) .text_sm() ) .child("The author of GPUI Component.") ) ) ) .child(" to see their profile") ``` ### Custom Timing Adjust the opening and closing delays to suit your needs: ```rust use std::time::Duration; use gpui::Styled as _; use gpui_component::{ button::{Button, ButtonVariants as _}, h_flex, }; h_flex() .gap_4() .child( HoverCard::new("fast-open") .open_delay(Duration::from_millis(200)) .close_delay(Duration::from_millis(100)) .trigger(Button::new("fast").label("Fast Open (200ms)").outline()) .child(div().child("This hover card opens after 200ms").text_sm()) ) .child( HoverCard::new("slow-open") .open_delay(Duration::from_secs(1)) .close_delay(Duration::from_secs_f32(0.5)) .trigger(Button::new("slow").label("Slow Open (1000ms)").outline()) .child(div().child("This hover card opens after 1000ms").text_sm()) ) ``` ### Positioning HoverCard supports 6 positioning options using the [Anchor] type: - TopLeft - TopCenter - TopRight - BottomLeft - BottomCenter - BottomRight ### Custom Content Builder For performance optimization, you can provide a content builder function for more complex case, which only calls when the HoverCard is opened: ```rust HoverCard::new("complex") .trigger(Button::new("btn").label("Hover me")) .content(|state, window, cx| { v_flex() .child("Dynamic content") .child(format!("Open: {}", state.is_open())) }) ``` ### Styling HoverCard inherits all `Styled` trait methods: ```rust HoverCard::new("styled") .trigger(Button::new("btn").label("Styled")) .w(px(400.)) .max_h(px(500.)) .text_sm() .gap_2() .child("Styled content") ``` Disable default appearance and apply custom styles: ```rust HoverCard::new("custom-styled") .appearance(false) // Disable default popover styling .trigger(Button::new("btn").label("Custom")) .bg(cx.theme().background) .border_2() .border_color(cx.theme().primary) .rounded(px(12.)) .p_4() .child("Custom styled content") ``` ## API Reference ### HoverCard Methods - `new(id: impl Into)` - Create a new HoverCard with a unique ID - `trigger(trigger: T)` - Set the element that triggers the hover - `content(content: F)` - Set a content builder function that receives `(&mut HoverCardState, &mut Window, &mut Context)` - `open_delay(duration: Duration)` - Set delay before showing (default: 600ms) - `close_delay(duration: Duration)` - Set delay before hiding (default: 300ms) - `anchor(anchor: impl Into)` - Set positioning (default: TopCenter) - `on_open_change(callback: F)` - Callback when open state changes, receives `(&bool, &mut Window, &mut App)` - `appearance(appearance: bool)` - Enable/disable default styling (default: true) ### HoverCardState Methods - `is_open() -> bool` - Check if the hover card is currently open ## Behavior Details ### Hover Timing The HoverCard uses a sophisticated timing system to provide a smooth user experience: 1. **Open Delay (600ms default)**: Prevents the card from flickering when the mouse quickly passes over the trigger 2. **Close Delay (300ms default)**: Gives users time to move their mouse from the trigger to the content area without the card closing 3. **Interactive Content**: Users can move their mouse into the content area, and the card will remain open as long as the mouse is either on the trigger or in the content ### Edge Cases Handled - **Quick Mouse Sweep**: If the mouse quickly moves across the trigger, the card won't open (cancelled by the open delay) - **Trigger to Content Movement**: The card stays open when moving the mouse from the trigger to the content area - **Rapid Hovering**: Multiple rapid hover events are debounced using an epoch-based timer system - **Multiple HoverCards**: Each HoverCard has independent state, so multiple cards can coexist without interfering ## Best Practices 1. **Use appropriate delays**: - Standard content: 600ms open, 300ms close - Quick previews: 500ms open, 200ms close - Tooltips: 300ms open, 100ms close 2. **Keep content concise**: HoverCards should provide preview information, not full content 3. **Make triggers visually distinct**: Use colors, underlines, or cursor changes to indicate hoverable elements 4. **Consider accessibility**: HoverCards are visual-only and don't support keyboard navigation. For keyboard-accessible content, consider using a Popover instead 5. **Avoid nested HoverCards**: They can create confusing user experiences ## Differences from [Popover] | Feature | HoverCard | Popover | | ------------------------ | ---------------- | ------------------ | | Trigger | Mouse hover | Click/right-click | | Keyboard navigation | No | Yes (with focus) | | Dismiss on outside click | No | Yes (configurable) | | Timing delays | Yes (open/close) | No | | Primary use case | Previews | Actions/forms | [Popover]: ./popover.md [Anchor]: https://docs.rs/gpui-component/latest/gpui_component/enum.Anchor.html [Avatar]: ./avatar.md ================================================ FILE: docs/docs/components/icon.md ================================================ --- title: Icon description: Display SVG icons with various sizes, colors, and transformations. --- # Icon A flexible icon component that renders SVG icons from the built-in icon library. Icons are based on Lucide.dev and support customization of size, color, and rotation. The component requires SVG files to be provided by the user in the assets bundle. Before you start, please make sure you have read: [Icons & Assets](../assets.md) to understand how use SVG in GPUI & GPUI Component application. ## Import ```rust use gpui_component::{Icon, IconName}; ``` ## Usage ### Basic Icon ```rust // Using IconName enum directly IconName::Heart // Or creating an Icon explicitly Icon::new(IconName::Heart) ``` ### Icon with Custom Size ```rust // Predefined sizes Icon::new(IconName::Search).xsmall() // size_3() Icon::new(IconName::Search).small() // size_3p5() Icon::new(IconName::Search).medium() // size_4() (default) Icon::new(IconName::Search).large() // size_6() // Custom pixel size Icon::new(IconName::Search).with_size(px(20.)) ``` ### Icon with Custom Color ```rust // Using theme colors Icon::new(IconName::Heart) .text_color(cx.theme().red) // Using custom colors Icon::new(IconName::Star) .text_color(gpui::red()) ``` ### Rotated Icons ```rust use gpui::Radians; // Rotate by radians Icon::new(IconName::ArrowUp) .rotate(Radians::from_degrees(90.)) // Transform with custom transformation Icon::new(IconName::ChevronRight) .transform(Transformation::rotate(Radians::PI)) ``` ### Custom SVG Path ```rust // Using a custom SVG file from assets Icon::new(Icon::empty()) .path("icons/my-custom-icon.svg") ``` ## Available Icons The `IconName` enum provides access to a curated set of icons. Here are some commonly used ones: ### Navigation - `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight` - `ChevronUp`, `ChevronDown`, `ChevronLeft`, `ChevronRight` - `ChevronsUpDown` ### Actions - `Check`, `Close`, `Plus`, `Minus` - `Copy`, `Delete`, `Search`, `Replace` - `Maximize`, `Minimize`, `WindowRestore` ### Files & Folders - `File`, `Folder`, `FolderOpen`, `FolderClosed` - `BookOpen`, `Inbox` ### UI Elements - `Menu`, `Settings`, `Settings2`, `Ellipsis`, `EllipsisVertical` - `Eye`, `EyeOff`, `Bell`, `Info` ### Social & External - `GitHub`, `Globe`, `ExternalLink` - `Heart`, `HeartOff`, `Star`, `StarOff` - `ThumbsUp`, `ThumbsDown` ### Status & Alerts - `CircleCheck`, `CircleX`, `TriangleAlert` - `Loader`, `LoaderCircle` ### Panels & Layout - `PanelLeft`, `PanelRight`, `PanelBottom` - `PanelLeftOpen`, `PanelRightOpen`, `PanelBottomOpen` - `LayoutDashboard`, `Frame` ### Users & Profile - `User`, `CircleUser`, `Bot` ### Other - `Calendar`, `Map`, `Palette`, `Inspector` - `Sun`, `Moon`, `Building2` ## Icon Sizes The Icon component supports several predefined sizes: | Size | Method | CSS Class | Pixels | | ----------- | --------------------- | ------------ | ------ | | Extra Small | `.xsmall()` | `size_3()` | 12px | | Small | `.small()` | `size_3p5()` | 14px | | Medium | `.medium()` (default) | `size_4()` | 16px | | Large | `.large()` | `size_6()` | 24px | | Custom | `.with_size(px(n))` | - | n px | ## Build you own `IconName`. You can define your own `IconName` to have more specific icons for your application. We have `IconNamed` trait for you to implement for your. ```rust use gpui_component::IconNamed; pub enum IconName { Encounters, Monsters, Spells, } impl IconNamed for IconName { fn path(self) -> gpui::SharedString { match self { IconName::Encounters => "icons/encounters.svg", IconName::Monsters => "icons/monsters.svg", IconName::Spells => "icons/spells.svg", } .into() } } // This allows for the following interactions (works with anything that has the `.icon(icon)` method. Button::new("my-button").icon(IconName::Spells); Icon::new(IconName::Monsters); ``` If you want to directly `render` a custom `IconName` you must implement the `RenderOnce` trait and derive `IntoElement` on the `IconName`. ```rust impl RenderOnce for IconName { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { Icon::empty().path(self.path()) } } // Now you can use it directly in your element tree: div() .child(IconName::Monsters) ``` ## Examples ### Icon in Button ```rust use gpui_component::button::Button; Button::new("like-btn") .icon( Icon::new(IconName::Heart) .text_color(cx.theme().red) .large() ) .label("Like") ``` ### Animated Loading Icon ```rust Icon::new(IconName::LoaderCircle) .text_color(cx.theme().muted_foreground) .medium() // Add rotation animation in your render logic ``` ### Status Icons ```rust // Success Icon::new(IconName::CircleCheck) .text_color(cx.theme().green) // Error Icon::new(IconName::CircleX) .text_color(cx.theme().red) // Warning Icon::new(IconName::TriangleAlert) .text_color(cx.theme().yellow) ``` ### Navigation Icons ```rust // Back button Icon::new(IconName::ArrowLeft) .medium() .text_color(cx.theme().foreground) // Dropdown indicator Icon::new(IconName::ChevronDown) .small() .text_color(cx.theme().muted_foreground) ``` ### Custom Icon from Assets ```rust // Using a custom SVG file Icon::empty() .path("icons/my-brand-logo.svg") .large() .text_color(cx.theme().primary) ``` ## Notes - Icons are rendered as SVG elements and support full CSS styling - The default size matches the current text size if no explicit size is set - Icons are flex-shrink-0 by default to prevent unwanted shrinking in flex layouts - All icon paths are relative to the assets bundle root - Icons from Lucide.dev are designed to work well at 16px and scale nicely to other sizes ================================================ FILE: docs/docs/components/image.md ================================================ --- title: Image description: A flexible image display component with loading states, fallbacks, and responsive sizing options. --- # Image The Image component provides a robust way to display images with comprehensive fallback handling, loading states, and responsive sizing. Built on GPUI's native image support, it handles various image sources including URLs, local files, and SVG graphics with proper error handling and accessibility features. ## Import ```rust use gpui::{img, ImageSource, ObjectFit}; use gpui_component::{v_flex, h_flex, div, Icon, IconName}; ``` ## Usage ### Basic Image ```rust // Simple image from URL img("https://example.com/image.jpg") // Local image file img("assets/logo.png") // SVG image img("icons/star.svg") ``` ### Image with Sizing ```rust // Fixed dimensions img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) // Responsive width with aspect ratio img("https://example.com/banner.jpg") .w(relative(1.)) // Full width .max_w(px(800.)) .h(px(400.)) // Square image img("https://example.com/avatar.jpg") .size(px(100.)) // 100x100px ``` ### Object Fit Options Control how images are scaled and positioned within their containers: ```rust // Cover - scales to fill container, may crop img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::Cover) // Contain - scales to fit within container, preserves aspect ratio img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::Contain) // Fill - stretches to fill container, may distort img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::Fill) // Scale Down - acts like contain, but never scales up img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::ScaleDown) // None - original size, may overflow or be smaller than container img("https://example.com/photo.jpg") .w(px(300.)) .h(px(200.)) .object_fit(ObjectFit::None) ``` ### Image with Fallback Handling ```rust // Basic fallback with placeholder fn image_with_fallback(src: &str, alt_text: &str) -> impl IntoElement { div() .w(px(300.)) .h(px(200.)) .bg(cx.theme().surface) .border_1() .border_color(cx.theme().border) .rounded(px(8.)) .overflow_hidden() .child( img(src) .w_full() .h_full() .object_fit(ObjectFit::Cover) // Add error handling in practice ) } // Fallback with icon placeholder fn image_with_icon_fallback(src: &str) -> impl IntoElement { div() .size(px(200.)) .bg(cx.theme().surface) .border_1() .border_color(cx.theme().border) .rounded(px(8.)) .flex() .items_center() .justify_center() .child( img(src) .size_full() .object_fit(ObjectFit::Cover) // On error, show icon: // Icon::new(IconName::Image) // .size(px(48.)) // .text_color(cx.theme().muted_foreground) ) } ``` ### Loading States ```rust // Image with loading skeleton fn image_with_loading(src: &str, is_loading: bool) -> impl IntoElement { div() .w(px(400.)) .h(px(300.)) .rounded(px(8.)) .overflow_hidden() .map(|this| { if is_loading { this.bg(cx.theme().muted) .flex() .items_center() .justify_center() .child("Loading...") } else { this.child( img(src) .w_full() .h_full() .object_fit(ObjectFit::Cover) ) } }) } // Progressive loading with placeholder fn progressive_image(src: &str, placeholder_src: &str) -> impl IntoElement { div() .relative() .w(px(400.)) .h(px(300.)) .rounded(px(8.)) .overflow_hidden() .child( // Low-quality placeholder img(placeholder_src) .absolute() .inset_0() .w_full() .h_full() .object_fit(ObjectFit::Cover) .opacity(0.5) ) .child( // High-quality image img(src) .absolute() .inset_0() .w_full() .h_full() .object_fit(ObjectFit::Cover) ) } ``` ### Responsive Images ```rust // Responsive grid images fn responsive_image_grid() -> impl IntoElement { div() .grid() .grid_cols(3) .gap_4() .child( img("https://example.com/photo1.jpg") .w_full() .aspect_ratio(1.0) // Square aspect ratio .object_fit(ObjectFit::Cover) .rounded(px(8.)) ) .child( img("https://example.com/photo2.jpg") .w_full() .aspect_ratio(1.0) .object_fit(ObjectFit::Cover) .rounded(px(8.)) ) .child( img("https://example.com/photo3.jpg") .w_full() .aspect_ratio(1.0) .object_fit(ObjectFit::Cover) .rounded(px(8.)) ) } // Hero image with text overlay fn hero_image() -> impl IntoElement { div() .relative() .w_full() .h(px(500.)) .rounded(px(12.)) .overflow_hidden() .child( img("https://example.com/hero-image.jpg") .absolute() .inset_0() .w_full() .h_full() .object_fit(ObjectFit::Cover) ) .child( div() .absolute() .inset_0() .bg(rgba(0, 0, 0, 0.4)) // Dark overlay .flex() .items_center() .justify_center() .child( v_flex() .items_center() .gap_4() .child("Hero Title") .child("Subtitle text here") ) ) } ``` ### Image Gallery ```rust // Simple image gallery fn image_gallery(images: Vec<&str>) -> impl IntoElement { v_flex() .gap_6() .child( // Main image div() .w_full() .h(px(400.)) .rounded(px(12.)) .overflow_hidden() .child( img(images[0]) .w_full() .h_full() .object_fit(ObjectFit::Cover) ) ) .child( // Thumbnail row h_flex() .gap_3() .children( images.iter().map(|src| { div() .size(px(80.)) .rounded(px(6.)) .overflow_hidden() .border_2() .border_color(cx.theme().border) .cursor_pointer() .hover(|this| this.border_color(cx.theme().primary)) .child( img(*src) .size_full() .object_fit(ObjectFit::Cover) ) }) ) ) } ``` ### SVG Images ```rust // SVG icon with custom styling img("assets/icons/logo.svg") .size(px(64.)) .text_color(cx.theme().primary) // SVG color // Inline SVG handling img("data:image/svg+xml;base64,...") .w(px(32.)) .h(px(32.)) // SVG with animation-friendly setup img("assets/spinner.svg") .size(px(24.)) .text_color(cx.theme().primary) // Add rotation animation in practice ``` ## API Reference ### Core Image Function | Function | Description | | ------------- | ------------------------------------- | | `img(source)` | Create image element from ImageSource | ### Image Sources (ImageSource) | Type | Description | Example | | ----------- | ---------------------- | --------------------------------- | | String/&str | URL or file path | `"https://example.com/image.jpg"` | | SharedUri | Shared URI reference | `SharedUri::from("file://path")` | | Local Path | Local file system path | `"assets/logo.png"` | | Data URI | Base64 encoded image | `"data:image/png;base64,..."` | ### Sizing Methods | Method | Description | | --------------- | ------------------------- | | `w(length)` | Set width | | `h(length)` | Set height | | `size(length)` | Set both width and height | | `w_full()` | Full width of container | | `h_full()` | Full height of container | | `size_full()` | Full size of container | | `max_w(length)` | Maximum width | | `max_h(length)` | Maximum height | | `min_w(length)` | Minimum width | | `min_h(length)` | Minimum height | ### Object Fit Options | Value | Description | | ---------------------- | --------------------------------- | | `ObjectFit::Cover` | Scale to fill container, may crop | | `ObjectFit::Contain` | Scale to fit within container | | `ObjectFit::Fill` | Stretch to fill container | | `ObjectFit::ScaleDown` | Like contain, but never scale up | | `ObjectFit::None` | Original size | ### Styling Methods | Method | Description | | --------------------- | ----------------------- | | `rounded(radius)` | Border radius | | `border_1()` | 1px border | | `border_color(color)` | Border color | | `opacity(value)` | Image opacity (0.0-1.0) | | `shadow_sm()` | Small shadow | | `shadow_lg()` | Large shadow | ## Examples ### Product Image Card ```rust use gpui_component::{v_flex, div, Icon, IconName}; fn product_card(image_src: &str, title: &str, price: &str) -> impl IntoElement { v_flex() .gap_3() .p_4() .bg(cx.theme().card) .rounded(px(12.)) .shadow_sm() .child( div() .relative() .w_full() .h(px(200.)) .rounded(px(8.)) .overflow_hidden() .bg(cx.theme().muted) .child( img(image_src) .w_full() .h_full() .object_fit(ObjectFit::Cover) ) .child( // Wishlist button div() .absolute() .top_2() .right_2() .size(px(32.)) .bg(rgba(255, 255, 255, 0.9)) .rounded_full() .flex() .items_center() .justify_center() .cursor_pointer() .child(Icon::new(IconName::Heart).size(px(16.))) ) ) .child(title) .child(price) } ``` ### Avatar with Image ```rust fn custom_avatar(src: &str, name: &str, size: f32) -> impl IntoElement { div() .size(px(size)) .rounded_full() .overflow_hidden() .border_2() .border_color(cx.theme().background) .shadow_sm() .child( img(src) .size_full() .object_fit(ObjectFit::Cover) ) } ``` ### Image Comparison Slider ```rust fn image_comparison(before_src: &str, after_src: &str) -> impl IntoElement { div() .relative() .w_full() .h(px(400.)) .rounded(px(12.)) .overflow_hidden() .child( // Before image img(before_src) .absolute() .inset_0() .w_full() .h_full() .object_fit(ObjectFit::Cover) ) .child( // After image with clip div() .absolute() .top_0() .left_0() .w(relative(0.5)) // Show 50% initially .h_full() .overflow_hidden() .child( img(after_src) .w(px(800.)) // Full width of container .h_full() .object_fit(ObjectFit::Cover) ) ) .child( // Divider line div() .absolute() .top_0() .left(relative(0.5)) .w(px(2.)) .h_full() .bg(cx.theme().primary) ) } ``` ### Error Handling Pattern ```rust enum ImageState { Loading, Loaded, Error, } fn robust_image(src: &str, state: ImageState) -> impl IntoElement { div() .w(px(300.)) .h(px(200.)) .bg(cx.theme().muted) .rounded(px(8.)) .border_1() .border_color(cx.theme().border) .flex() .items_center() .justify_center() .map(|this| { match state { ImageState::Loading => { this.child( v_flex() .items_center() .gap_2() .child(Icon::new(IconName::Loader2).size(px(24.))) .child("Loading...") ) } ImageState::Loaded => { this.p_0() .overflow_hidden() .child( img(src) .w_full() .h_full() .object_fit(ObjectFit::Cover) ) } ImageState::Error => { this.child( v_flex() .items_center() .gap_2() .child( Icon::new(IconName::ImageOff) .size(px(32.)) .text_color(cx.theme().muted_foreground) ) .child("Failed to load image") ) } } }) } ``` ## Best Practices ### Image Optimization - Use appropriate image dimensions for display size - Compress images without sacrificing quality - Consider using modern image formats (WebP, AVIF) - Implement responsive images for different screen sizes ### Error Handling - Always provide meaningful fallbacks for failed image loads - Use skeleton loading states to maintain layout stability - Implement retry mechanisms for temporary network failures - Provide user feedback for permanent load failures ### Performance - Use lazy loading for images not immediately visible - Implement proper caching strategies - Consider using placeholder images during loading - Optimize image sizes for their display context ### User Experience - Maintain consistent aspect ratios in image grids - Provide smooth loading transitions - Use appropriate object-fit values for content type - Consider providing zoom functionality for detailed images ## Implementation Notes ### GPUI Integration - Built on GPUI's native image rendering capabilities - Supports all GPUI ImageSource types automatically - Inherits GPUI's styling and layout system - Compatible with GPUI's animation and interaction systems ### SVG Support - Full support for SVG graphics with proper scaling - SVG images can be styled with text colors for theming - Vector graphics maintain sharpness at all sizes - Supports both external SVG files and inline data URIs ### Memory Management - GPUI handles image caching and memory management automatically - Large images are efficiently managed by the graphics backend - No manual memory cleanup required for image components ### Cross-Platform Compatibility - Consistent behavior across Windows, macOS, and Linux - Native image format support varies by platform - Uses platform-optimized rendering where available ================================================ FILE: docs/docs/components/index.md ================================================ --- title: Components order: 2 collapsed: false --- # Components ### Basic Components - [Accordion](accordion) - Collapsible content panels - [Alert](alert) - Alert messages with different variants - [Avatar](avatar) - User avatars with fallback text - [Badge](badge) - Count badges and indicators - [Button](button) - Interactive buttons with multiple variants - [Checkbox](checkbox) - Binary selection control - [Collapsible](collapsible) - Expandable/collapsible content - [DropdownButton](dropdown_button) - Button with dropdown menu - [Icon](icon) - Icon display component - [Image](image) - Image display with fallbacks - [Kbd](kbd) - Keyboard shortcut display - [Label](label) - Text labels for form elements - [Pagination](pagination) - Page navigation controls - [Progress](progress) - Progress bars - [Radio](radio) - Single selection from multiple options - [Rating](rating) - Interactive star rating component - [Skeleton](skeleton) - Loading placeholders - [Slider](slider) - Value selection from a range - [Spinner](spinner) - Loading and status spinners - [Stepper](stepper) - Step-by-step progress indicator - [Switch](switch) - Toggle on/off control - [Tag](tag) - Labels and categories - [Toggle](toggle) - Toggle button states - [Tooltip](tooltip) - Helpful hints on hover ### Form Components - [Input](input) - An input field or a component that looks like an input field. - [Select](select) - A list of options for the user to pick. - [NumberInput](number-input) - Numeric input with increment/decrement - [DatePicker](date-picker) - Date selection with calendar - [OtpInput](otp-input) - One-time password input - [ColorPicker](color-picker) - Color selection interface - [Editor](editor) - Multi-line text editor and code editor - [Form](form) - Form container and layout ### Layout Components - [DescriptionList](description-list) - Key-value pair display - [GroupBox](group-box) - Grouped content with borders - [Dialog](dialog) - Dialog and modal windows - [Notification](notification) - Toast notifications - [Popover](popover) - Floating content display - [Resizable](resizable) - Resizable panels and containers - [Scrollable](scrollable) - Scrollable containers - [Sheet](sheet) - Slide-in panel from edges - [Sidebar](sidebar) - Navigation sidebar ### Advanced Components - [Calendar](calendar) - Calendar display and navigation - [Chart](chart) - Data visualization charts (Line, Bar, Area, Pie, Candlestick) - [List](list) - List display with items - [Menu](menu) - Menu and context menu and dropdown menu. - [Settings](settings) - Settings UI - [DataTable](data-table) - High-performance data tables - [Tabs](tabs) - Tabbed interface - [Tree](tree) - Hierarchical tree data display - [VirtualList](virtual-list) - Virtualized list for large datasets ================================================ FILE: docs/docs/components/input.md ================================================ --- title: Input description: Text input component with validation, masking, and various features. --- # Input A flexible text input component with support for validation, masking, prefix/suffix elements, and different states. ## Import ```rust use gpui_component::input::{InputState, Input}; ``` ## Usage ### Basic Input ```rust let input = cx.new(|cx| InputState::new(window, cx)); Input::new(&input) ``` ### With Placeholder ```rust let input = cx.new(|cx| InputState::new(window, cx) .placeholder("Enter your name...") ); Input::new(&input) ``` ### With Default Value ```rust let input = cx.new(|cx| InputState::new(window, cx) .default_value("John Doe") ); Input::new(&input) ``` ### Cleanable Input ```rust Input::new(&input) .cleanable(true) // Show clear button when input has value ``` ### With Prefix and Suffix ```rust use gpui_component::{Icon, IconName}; // With prefix icon Input::new(&input) .prefix(Icon::new(IconName::Search).small()) // With suffix button Input::new(&input) .suffix( Button::new("info") .ghost() .icon(IconName::Info) .xsmall() ) // With both Input::new(&input) .prefix(Icon::new(IconName::Search).small()) .suffix(Button::new("btn").ghost().icon(IconName::Info).xsmall()) ``` ### Password Input (Masked) ```rust let input = cx.new(|cx| InputState::new(window, cx) .masked(true) .default_value("password123") ); Input::new(&input) .mask_toggle() // Shows toggle button to reveal password ``` ### Input Sizes ```rust Input::new(&input).large() Input::new(&input) // medium (default) Input::new(&input).small() ``` ### Disabled Input ```rust Input::new(&input).disabled(true) ``` ### Clean on ESC ```rust let input = cx.new(|cx| InputState::new(window, cx) .clean_on_escape() // Clear input when ESC is pressed ); Input::new(&input) ``` ### Input Validation ```rust // Validate float numbers let input = cx.new(|cx| InputState::new(window, cx) .validate(|s, _| s.parse::().is_ok()) ); // Regex pattern validation let input = cx.new(|cx| InputState::new(window, cx) .pattern(regex::Regex::new(r"^[a-zA-Z0-9]*$").unwrap()) ); ``` ### Input Masking ```rust // Phone number mask let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern("(999)-999-9999") ); // Custom pattern: AAA-###-AAA (A=letter, #=digit, 9=digit optional) let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern("AAA-###-AAA") ); // Number with thousands separator use gpui_component::input::MaskPattern; let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(3), }) ); ``` ### Handle Input Events ```rust let input = cx.new(|cx| InputState::new(window, cx)); cx.subscribe_in(&input, window, |view, state, event, window, cx| { match event { InputEvent::Change => { let text = state.read(cx).value(); println!("Input changed: {}", text); } InputEvent::PressEnter { secondary } => { println!("Enter pressed, secondary: {}", secondary); } InputEvent::Focus => println!("Input focused"), InputEvent::Blur => println!("Input blurred"), } }); ``` ### Custom Appearance ```rust // Without default styling Input::new(&input).appearance(false) // Use in custom container div() .border_b_2() .px_6() .py_3() .border_color(cx.theme().border) .bg(cx.theme().secondary) .child(Input::new(&input).appearance(false)) ``` ## Examples ### Search Input ```rust let search = cx.new(|cx| InputState::new(window, cx) .placeholder("Search...") ); Input::new(&search) .prefix(Icon::new(IconName::Search).small()) ``` ### Currency Input ```rust let amount = cx.new(|cx| InputState::new(window, cx) .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), }) ); div() .child(Input::new(&amount)) .child(format!("Value: {}", amount.read(cx).value())) ``` ### Form with Multiple Inputs ```rust struct FormView { name_input: Entity, email_input: Entity, } v_flex() .gap_3() .child(Input::new(&self.name_input)) .child(Input::new(&self.email_input)) ``` ================================================ FILE: docs/docs/components/kbd.md ================================================ --- title: Kbd description: Displays keyboard shortcuts with platform-specific formatting. --- # Kbd A component for displaying keyboard shortcuts and key combinations with proper platform-specific formatting. Automatically adapts the display to match the conventions of macOS (using symbols) or Windows/Linux (using text labels). ## Import ```rust use gpui_component::kbd::Kbd; use gpui::Keystroke; ``` ## Usage ### Basic Keyboard Shortcut ```rust // Create from a keystroke let kbd = Kbd::new(Keystroke::parse("cmd-shift-p").unwrap()); // Or convert directly from keystroke let kbd: Kbd = Keystroke::parse("escape").unwrap().into(); ``` ### Common Shortcuts ```rust // Command palette Kbd::new(Keystroke::parse("cmd-shift-p").unwrap()) // New tab Kbd::new(Keystroke::parse("cmd-t").unwrap()) // Zoom controls Kbd::new(Keystroke::parse("cmd--").unwrap()) // Zoom out Kbd::new(Keystroke::parse("cmd-+").unwrap()) // Zoom in // Navigation Kbd::new(Keystroke::parse("escape").unwrap()) Kbd::new(Keystroke::parse("enter").unwrap()) Kbd::new(Keystroke::parse("backspace").unwrap()) ``` ### Multiple Modifiers ```rust // Complex combinations Kbd::new(Keystroke::parse("cmd-ctrl-shift-a").unwrap()) Kbd::new(Keystroke::parse("cmd-alt-backspace").unwrap()) Kbd::new(Keystroke::parse("ctrl-alt-shift-a").unwrap()) ``` ### Arrow Keys and Function Keys ```rust // Arrow keys Kbd::new(Keystroke::parse("left").unwrap()) Kbd::new(Keystroke::parse("right").unwrap()) Kbd::new(Keystroke::parse("up").unwrap()) Kbd::new(Keystroke::parse("down").unwrap()) // Function keys Kbd::new(Keystroke::parse("f12").unwrap()) Kbd::new(Keystroke::parse("secondary-f12").unwrap()) // Page navigation Kbd::new(Keystroke::parse("pageup").unwrap()) Kbd::new(Keystroke::parse("pagedown").unwrap()) ``` ### Without Visual Styling ```rust // Display only the key text without the styled background Kbd::new(Keystroke::parse("cmd-s").unwrap()) .appearance(false) ``` ### From Action Bindings ```rust use gpui::{Action, Window, FocusHandle}; // Get first keybinding for an action if let Some(kbd) = Kbd::binding_for_action(&MyAction {}, None, window) { // Display the bound shortcut } // Get keybinding for action within a specific context if let Some(kbd) = Kbd::binding_for_action(&MyAction {}, Some("Editor"), window) { // Display context-specific shortcut } // Get keybinding for action within a focus handle if let Some(kbd) = Kbd::binding_for_action_in(&MyAction {}, &focus_handle, window) { // Display shortcut for focused element } ``` ## Platform Differences The Kbd component automatically formats shortcuts according to platform conventions: ### macOS - Uses symbols: ⌃ ⌥ ⇧ ⌘ - No separators between modifiers - Order: Control, Option, Shift, Command - Special keys: ⌫ (backspace), ⎋ (escape), ⏎ (enter), ← → ↑ ↓ (arrows) ### Windows/Linux - Uses text labels: Ctrl, Alt, Shift, Win - Plus sign (+) separators - Order: Ctrl, Alt, Shift, Win - Special keys: Backspace, Esc, Enter, Left, Right, Up, Down ### Examples by Platform | Input | macOS | Windows/Linux | | ------------------- | ----- | ----------------- | | `cmd-a` | ⌘A | Win+A | | `ctrl-shift-a` | ⌃⇧A | Ctrl+Shift+A | | `cmd-alt-backspace` | ⌥⌘⌫ | Win+Alt+Backspace | | `escape` | ⎋ | Esc | | `enter` | ⏎ | Enter | | `left` | ← | Left | ## Examples ### Keyboard Shortcut Help ```rust use gpui::{div, h_flex, v_flex}; // Display common shortcuts v_flex() .gap_2() .child( h_flex() .gap_2() .items_center() .child("Open command palette:") .child(Kbd::new(Keystroke::parse("cmd-shift-p").unwrap())) ) .child( h_flex() .gap_2() .items_center() .child("Save file:") .child(Kbd::new(Keystroke::parse("cmd-s").unwrap())) ) .child( h_flex() .gap_2() .items_center() .child("Find in files:") .child(Kbd::new(Keystroke::parse("cmd-shift-f").unwrap())) ) ``` ### Menu Item with Shortcut ```rust h_flex() .justify_between() .items_center() .child("New File") .child(Kbd::new(Keystroke::parse("cmd-n").unwrap())) ``` ### Inline Documentation ```rust div() .child("Press ") .child(Kbd::new(Keystroke::parse("escape").unwrap())) .child(" to cancel or ") .child(Kbd::new(Keystroke::parse("enter").unwrap())) .child(" to confirm.") ``` ### Custom Styling ```rust Kbd::new(Keystroke::parse("cmd-k").unwrap()) .text_color(cx.theme().accent) .border_color(cx.theme().accent) .bg(cx.theme().accent.opacity(0.1)) ``` ### Text-Only Format ```rust // Get formatted text without styling let shortcut_text = Kbd::format(&Keystroke::parse("cmd-shift-p").unwrap()); div().child(format!("Shortcut: {}", shortcut_text)) ``` ## Styling The Kbd component uses the following default styles: - Border with theme border color - Muted foreground text color - Background with theme background color - Small rounded corners - Centered text alignment - Extra small font size - Minimal padding (0.5px vertical, 1px horizontal) - Minimum width of 5 units - Flex shrink disabled to maintain size All styles can be customized using the `Styled` trait methods. ================================================ FILE: docs/docs/components/label.md ================================================ --- title: Label description: Text labels for form elements with highlighting and styling options. --- # Label A versatile label component for displaying text with support for secondary text, highlighting, masking, and customizable styling. Perfect for form labels, captions, and general text display with optional/required indicators. ## Import ```rust use gpui_component::label::{Label, HighlightsMatch}; ``` ## Usage ### Basic Label ```rust Label::new("This is a label") ``` ### Label with Secondary Text ```rust // Label with optional indicator Label::new("Company Address") .secondary("(optional)") // Label with required indicator Label::new("Email Address") .secondary("(required)") ``` ### Text Alignment ```rust // Left aligned (default) Label::new("Text align left") // Center aligned Label::new("Text align center") .text_center() // Right aligned Label::new("Text align right") .text_right() ``` ### Text Highlighting ```rust // Full text highlighting (finds all matches) Label::new("Hello World Hello") .highlights("Hello") // Prefix highlighting (only matches at start) Label::new("Hello World") .highlights(HighlightsMatch::Prefix("Hello".into())) // Highlight with secondary text Label::new("Company Name") .secondary("(optional)") .highlights("Company") ``` ### Color and Styling ```rust use gpui_component::green_500; // Custom text color Label::new("Color Label") .text_color(green_500()) // Font styling Label::new("Font Size Label") .text_size(px(20.)) .font_semibold() .line_height(rems(1.8)) ``` ### Masked Labels ```rust // For sensitive information Label::new("9,182,1 USD") .text_2xl() .masked(true) // Shows as "•••••••••••" // Toggle masking programmatically Label::new("500 USD") .text_xl() .masked(self.masked) ``` ### Multi-line Text ```rust // Text wrapping with line height div().w(px(200.)).child( Label::new( "Label should support text wrap in default, \ if the text is too long, it should wrap to the next line." ) .line_height(rems(1.8)) ) ``` ### Different Sizes ```rust // Using text size utilities Label::new("Extra Large").text_2xl() Label::new("Large").text_xl() Label::new("Medium").text_base() // default Label::new("Small").text_sm() Label::new("Extra Small").text_xs() ``` ## API Reference ### Label | Method | Description | | ------------------- | ------------------------------------------------------------- | | `new(text)` | Create a new label with text | | `secondary(text)` | Add secondary text (usually for optional/required indicators) | | `masked(bool)` | Show/hide text with bullet characters | | `highlights(match)` | Highlight matching text | ### HighlightsMatch | Variant | Description | | -------------- | ------------------------------------------------ | | `Full(text)` | Highlights all occurrences of the text | | `Prefix(text)` | Highlights only if text appears at the beginning | | Method | Description | | ------------- | ------------------------------- | | `as_str()` | Get the search text as string | | `is_prefix()` | Check if this is a prefix match | ### Styling Methods (via Styled trait) | Method | Description | | --------------------- | --------------------------- | | `text_color(color)` | Set text color | | `text_size(size)` | Set font size | | `text_center()` | Center align text | | `text_right()` | Right align text | | `font_semibold()` | Set font weight to semibold | | `font_bold()` | Set font weight to bold | | `line_height(height)` | Set line height | | `text_xs()` | Extra small text size | | `text_sm()` | Small text size | | `text_base()` | Base text size (default) | | `text_lg()` | Large text size | | `text_xl()` | Extra large text size | | `text_2xl()` | 2x large text size | ## Examples ### Form Labels ```rust // Required field Label::new("Email Address") .secondary("*") .text_color(cx.theme().destructive) // Optional field Label::new("Phone Number") .secondary("(optional)") // Field with description Label::new("Password") .secondary("(minimum 8 characters)") ``` ### Search Highlighting ```rust // Interactive search highlighting let search_term = "Hello"; Label::new("Hello World Hello Universe") .highlights(search_term) // Highlights all "Hello" occurrences ``` ### Sensitive Information ```rust // Financial data with toggle h_flex() .child( Label::new("$9,182.50 USD") .text_2xl() .masked(self.is_masked) ) .child( Button::new("toggle-mask") .ghost() .icon(if self.is_masked { IconName::EyeOff } else { IconName::Eye }) .on_click(|this, _, _, _| { this.is_masked = !this.is_masked; }) ) ``` ### Multi-language Support ```rust // Supports Unicode text Label::new("这是一个标签") // Chinese text Label::new("こんにちは世界") // Japanese text Label::new("🌍 Hello World 🚀") // Emojis ``` ### Status Indicators ```rust // Success status Label::new("✓ Verified") .text_color(cx.theme().success) // Warning status Label::new("⚠ Pending Review") .text_color(cx.theme().warning) // Error status Label::new("✗ Failed") .text_color(cx.theme().destructive) ``` ### Custom Layouts ```rust // Flex layout with labels h_flex() .justify_between() .child(Label::new("Total Amount")) .child(Label::new("$1,234.56").font_semibold()) // Grid layout v_flex() .gap_2() .child(Label::new("Name:").font_semibold()) .child(Label::new("John Doe")) .child(Label::new("Email:").font_semibold()) .child(Label::new("john@example.com")) ``` ================================================ FILE: docs/docs/components/list.md ================================================ --- title: List description: A flexible list component that displays a series of items with support for sections, search, selection, and infinite scrolling. --- # List A powerful List component that provides a virtualized, searchable list interface with support for sections, headers, footers, selection states, and infinite scrolling. The component is built on a delegate pattern that allows for flexible data management and custom item rendering. ## Import ```rust use gpui_component::list::{List, ListState, ListDelegate, ListItem, ListEvent, ListSeparatorItem}; use gpui_component::IndexPath; ``` ## Usage ### Basic List To create a list, you need to implement the `ListDelegate` trait for your data: ```rust struct MyListDelegate { items: Vec, selected_index: Option, } impl ListDelegate for MyListDelegate { type Item = ListItem; fn items_count(&self, _section: usize, _cx: &App) -> usize { self.items.len() } fn render_item( &mut self, ix: IndexPath, _window: &mut Window, _cx: &mut Context>, ) -> Option { self.items.get(ix.row).map(|item| { ListItem::new(ix) .child(Label::new(item.clone())) .selected(Some(ix) == self.selected_index) }) } fn set_selected_index( &mut self, ix: Option, _window: &mut Window, cx: &mut Context>, ) { self.selected_index = ix; cx.notify(); } } // Create the list let delegate = MyListDelegate { items: vec!["Item 1".into(), "Item 2".into(), "Item 3".into()], selected_index: None, }; /// Create a list state. let state = cx.new(|cx| ListState::new(delegate, window, cx)); ``` Now use [List] to render list: ```rs div().child(List::new(&state)) ``` ### List with Sections **Note:** Sections with `items_count` of 0 will be automatically hidden (no header or footer will be rendered for empty sections). ```rust impl ListDelegate for MyListDelegate { type Item = ListItem; fn sections_count(&self, _cx: &App) -> usize { 3 // Number of sections } fn items_count(&self, section: usize, _cx: &App) -> usize { match section { 0 => 5, 1 => 3, 2 => 7, _ => 0, } } fn render_section_header( &mut self, section: usize, _window: &mut Window, cx: &mut Context>, ) -> Option { let title = match section { 0 => "Section 1", 1 => "Section 2", 2 => "Section 3", _ => return None, }; Some( h_flex() .px_2() .py_1() .gap_2() .text_sm() .text_color(cx.theme().muted_foreground) .child(Icon::new(IconName::Folder)) .child(title) ) } fn render_section_footer( &mut self, section: usize, _window: &mut Window, cx: &mut Context>, ) -> Option { Some( div() .px_2() .py_1() .text_xs() .text_color(cx.theme().muted_foreground) .child(format!("End of section {}", section + 1)) ) } } ``` ### List Items with Icons and Actions ```rust fn render_item( &mut self, ix: IndexPath, _window: &mut Window, cx: &mut Context>, ) -> Option { self.items.get(ix.row).map(|item| { ListItem::new(ix) .child( h_flex() .items_center() .gap_2() .child(Icon::new(IconName::File)) .child(Label::new(item.title.clone())) ) .suffix(|_, _| { Button::new("action") .ghost() .small() .icon(IconName::MoreHorizontal) }) .selected(Some(ix) == self.selected_index) .on_click(cx.listener(move |this, _, window, cx| { this.delegate_mut().select_item(ix, window, cx); })) }) } ``` ### List with Search The list automatically includes a search input by default. Implement `perform_search` to handle queries: And you should use `searchable(true)` when creating the `ListState` to show search input. ```rust impl ListDelegate for MyListDelegate { fn perform_search( &mut self, query: &str, _window: &mut Window, _cx: &mut Context>, ) -> Task<()> { // Filter items based on query self.filtered_items = self.all_items .iter() .filter(|item| item.to_lowercase().contains(&query.to_lowercase())) .cloned() .collect(); Task::ready(()) } } let state = cx.new(|cx| ListState::new(delegate, window, cx).searchable(true)); List::new(&state) ``` ### List with Loading State ```rust impl ListDelegate for MyListDelegate { fn loading(&self, _cx: &App) -> bool { self.is_loading } fn render_loading( &mut self, _window: &mut Window, _cx: &mut Context>, ) -> impl IntoElement { // Custom loading view v_flex() .justify_center() .items_center() .py_4() .child(Skeleton::new().h_4().w_full()) .child(Skeleton::new().h_4().w_3_4()) } } ``` ### Infinite Scrolling ```rust impl ListDelegate for MyListDelegate { fn has_more(&self, _cx: &App) -> bool { self.has_more_data } fn load_more_threshold(&self) -> usize { 20 // Trigger when 20 items from bottom } fn load_more(&mut self, window: &mut Window, cx: &mut Context>) { if self.is_loading { return; } self.is_loading = true; cx.spawn_in(window, async move |view, window| { // Simulate API call Timer::after(Duration::from_secs(1)).await; view.update_in(window, |view, _, cx| { // Add more items view.delegate_mut().load_more_items(); view.delegate_mut().is_loading = false; cx.notify(); }); }).detach(); } } ``` ### List Events ```rust // Subscribe to list events let _subscription = cx.subscribe(&state, |_, _, event: &ListEvent, _| { match event { ListEvent::Select(ix) => { println!("Item selected at: {:?}", ix); } ListEvent::Confirm(ix) => { println!("Item confirmed at: {:?}", ix); } ListEvent::Cancel => { println!("Selection cancelled"); } } }); ``` ### Different Item Styles ```rust // Basic item with hover effect ListItem::new(ix) .child(Label::new("Basic Item")) .selected(is_selected) // Item with check icon ListItem::new(ix) .child(Label::new("Checkable Item")) .check_icon(IconName::Check) .confirmed(is_confirmed) // Disabled item ListItem::new(ix) .child(Label::new("Disabled Item")) .disabled(true) // Separator item ListSeparatorItem::new() .child( div() .h_px() .w_full() .bg(cx.theme().border) ) ``` ### Custom Empty State ```rust impl ListDelegate for MyListDelegate { fn render_empty(&mut self, _window: &mut Window, cx: &mut Context>) -> impl IntoElement { v_flex() .size_full() .justify_center() .items_center() .gap_2() .child(Icon::new(IconName::Search).size_16().text_color(cx.theme().muted_foreground)) .child( Label::new("No items found") .text_color(cx.theme().muted_foreground) ) .child( Label::new("Try adjusting your search terms") .text_sm() .text_color(cx.theme().muted_foreground.opacity(0.7)) ) } } ``` ## Configuration Options ### List Configuration ```rust List::new(&state) .max_h(px(400.)) // Set maximum height .scrollbar_visible(false) // Hide scrollbar .paddings(Edges::all(px(8.))) // Set internal padding ``` ### Scrolling Control ```rust // Scroll to specific item state.update(cx, |state, cx| { state.scroll_to_item( IndexPath::new(0).section(1), // Row 0 of section 1 ScrollStrategy::Center, window, cx, ); }); // Scroll to selected item state.update(cx, |state, cx| { state.scroll_to_selected_item(window, cx); }); // Set selected index without scrolling state.update(cx, |state, cx| { state.set_selected_index(Some(IndexPath::new(5)), window, cx); }); ``` ## Examples ### File Browser List ```rust struct FileBrowserDelegate { files: Vec, selected: Option, } #[derive(Clone)] struct FileInfo { name: String, is_directory: bool, size: Option, } impl ListDelegate for FileBrowserDelegate { type Item = ListItem; fn render_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context>) -> Option { self.files.get(ix.row).map(|file| { let icon = if file.is_directory { IconName::Folder } else { IconName::File }; ListItem::new(ix) .child( h_flex() .items_center() .justify_between() .w_full() .child( h_flex() .items_center() .gap_2() .child(Icon::new(icon)) .child(Label::new(file.name.clone())) ) .when_some(file.size, |this, size| { this.child( Label::new(format_size(size)) .text_sm() .text_color(cx.theme().muted_foreground) ) }) ) .selected(Some(ix) == self.selected) }) } } ``` ### Contact List with Sections ```rust struct ContactListDelegate { contacts_by_letter: BTreeMap>, selected: Option, } impl ListDelegate for ContactListDelegate { type Item = ListItem; fn sections_count(&self, _cx: &App) -> usize { self.contacts_by_letter.len() } fn render_section_header(&mut self, section: usize, _window: &mut Window, cx: &mut Context>) -> Option { let letter = self.contacts_by_letter.keys().nth(section)?; Some( div() .px_3() .py_2() .bg(cx.theme().background) .border_b_1() .border_color(cx.theme().border) .child( Label::new(letter.to_string()) .text_lg() .text_color(cx.theme().accent_foreground) .font_weight(FontWeight::BOLD) ) ) } } ``` ================================================ FILE: docs/docs/components/menu.md ================================================ --- title: Menu description: Context menus and popup menus with support for icons, shortcuts, submenus, and various menu item types. --- # PopupMenu The Menu component provides both context menus (right-click menus) and popup menus with comprehensive features including icons, keyboard shortcuts, submenus, separators, checkable items, and custom elements. Built with accessibility and keyboard navigation in mind. ## Import ```rust use gpui_component::{ menu::{PopupMenu, PopupMenuItem, ContextMenuExt, DropdownMenu}, Button }; use gpui::{actions, Action}; ``` ## Usage ### ContextMenu Context menus appear when right-clicking on an element: ```rust use gpui_component::menu::ContextMenuExt; div() .id("my-element") .child("Right click me") .context_menu(|menu, window, cx| { menu.menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() .menu("Delete", Box::new(Delete)) }) ``` ### DropdownMenu Dropdown menus are triggered by buttons or other interactive elements: ```rust use gpui_component::popup_menu::{PopupMenuExt as _, PopupMenuItem}; let view = cx.entity(); Button::new("menu-btn") .label("Open Menu") .dropdown_menu(|menu, window, cx| { menu.menu("New File", Box::new(NewFile)) .menu("Open File", Box::new(OpenFile)) .link("Documentation", "https://longbridge.github.io/gpui-component/") .separator() .item(PopupMenuItem::new("Custom Action") .on_click(window.listener_for(&view, |this, _, window, cx| { // Custom action logic here this. }) ) .separator() .menu("Exit", Box::new(Exit)) }) ``` :::tip As you see, the each menu item is associated with an [Action], we choice this design to better integrate with GPUI's action and key binding system, allowing menu items to automatically display keyboard shortcuts when applicable. So, the [Action] is the recommended way to define menu item behaviors. However, if you prefer not to use [Action]s, you can create custom menu items using the `item` method along with [PopupMenuItem]. There have a `on_click` callback to handle the click event directly. ::: ### Anchor Position Control where the dropdown menu appears relative to the trigger: ```rust use gpui::Corner; Button::new("menu-btn") .label("Options") .dropdown_menu_with_anchor(Corner::TopRight, |menu, window, cx| { menu.menu("Option 1", Box::new(Action1)) .menu("Option 2", Box::new(Action2)) }) ``` ### Icons Add icons to menu items for better visual clarity: ```rust use gpui_component::IconName; menu.menu_with_icon("Search", IconName::Search, Box::new(Search)) .menu_with_icon("Settings", IconName::Settings, Box::new(OpenSettings)) .separator() .menu_with_icon("Help", IconName::Help, Box::new(ShowHelp)) ``` ### Disabled State Create disabled menu items that cannot be activated: ```rust menu.menu("Available Action", Box::new(Action1)) .menu_with_disabled("Disabled Action", Box::new(Action2), true) .menu_with_icon_and_disabled( "Unavailable", IconName::Lock, Box::new(Action3), true ) ``` ### Check state Create menu items that show a check state: ```rust let is_enabled = true; menu.menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature)) .menu_with_check("Show Sidebar", sidebar_visible, Box::new(ToggleSidebar)) ``` By default, the check icon will be shown on the left side of the menu item, if this menu item has an icon, the check icon will replace the icon on the left side. There also have a `check_side` option for you to config the check icon to be shown on the right side: ```rust menu.check_size(Side::Right) .menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature)) ``` ### Separators Use separators to group related menu items: ```rust menu.menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() // Groups file operations .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() // Groups edit operations .menu("Exit", Box::new(Exit)) ``` ### Labels Add non-interactive labels to organize menu sections: ```rust menu.label("File Operations") .menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() .label("Edit Operations") .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) ``` ### Link MenuItem Create menu items that open external links: ```rust menu.link("Documentation", "https://docs.example.com") .link_with_icon( "GitHub", IconName::GitHub, "https://github.com/example/repo" ) .separator() .external_link_icon(false) // Hide external link icons .link("Support", "https://support.example.com") ``` ### Custom Element Create custom menu items with complex content: ```rust use gpui_component::{h_flex, v_flex}; menu.menu_element(Box::new(CustomAction), |window, cx| { v_flex() .child("Custom Element") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("This is a subtitle") ) }) .menu_element_with_icon( IconName::Info, Box::new(InfoAction), |window, cx| { h_flex() .gap_1() .child("Status") .child( div() .text_sm() .text_color(cx.theme().success) .child("✓ Connected") ) } ) ``` ### Keyboard Shortcuts Menu items automatically display keyboard shortcuts if they're bound to actions: ```rust // First define your actions and key bindings actions!(my_app, [Copy, Paste, Cut]); // In your app initialization cx.bind_keys([ KeyBinding::new("ctrl-c", Copy, Some("editor")), KeyBinding::new("ctrl-v", Paste, Some("editor")), KeyBinding::new("ctrl-x", Cut, Some("editor")), ]); // The menu will automatically show shortcuts menu.action_context(focus_handle) // Set context for shortcuts .menu("Copy", Box::new(Copy)) // Will show "Ctrl+C" .menu("Paste", Box::new(Paste)) // Will show "Ctrl+V" .menu("Cut", Box::new(Cut)) // Will show "Ctrl+X" ``` ### Submenus Create nested menus with submenu support: ```rust menu.submenu("File", window, cx, |submenu, window, cx| { submenu.menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() .menu("Recent", Box::new(ShowRecent)) }) .submenu("Edit", window, cx, |submenu, window, cx| { submenu.menu("Undo", Box::new(Undo)) .menu("Redo", Box::new(Redo)) }) ``` ### Submenus with Icons Add icons to submenu headers: ```rust menu.submenu_with_icon( Some(IconName::Folder.into()), "Project", window, cx, |submenu, window, cx| { submenu.menu("Open Project", Box::new(OpenProject)) .menu("Close Project", Box::new(CloseProject)) } ) ``` ### Scrollable Menus :::warning When you have enabled `scrollable()` on a menu, avoid using submenus within it, as this can lead to usability issues. ::: For menus with many items, enable scrolling: ```rust Button::new("large-menu") .label("Many Options") .dropdown_menu(|menu, window, cx| { let mut menu = menu .scrollable(true) .max_h(px(300.)) .label("Select an option"); for i in 0..100 { menu = menu.menu( format!("Option {}", i), Box::new(SelectOption(i)) ); } menu }) ``` ### Menu Sizing Control menu dimensions: ```rust menu.min_w(px(200.)) // Minimum width .max_w(px(400.)) // Maximum width .max_h(px(300.)) // Maximum height .scrollable(true) // Enable scrolling when content exceeds max height ``` ### Action Context Set the focus context for handling menu actions: ```rust let focus_handle = cx.focus_handle(); menu.action_context(focus_handle) .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) ``` ## API Reference - [PopupMenu] - [context_menu] - [PopupMenuItem] ## Examples ### File Manager Context Menu ```rust div() .id("file-manager") .child("Right-click for options") .context_menu(|menu, window, cx| { menu.menu_with_icon("Open", IconName::FolderOpen, Box::new(Open)) .separator() .menu_with_icon("Copy", IconName::Copy, Box::new(Copy)) .menu_with_icon("Cut", IconName::Scissors, Box::new(Cut)) .menu_with_icon("Paste", IconName::Clipboard, Box::new(Paste)) .separator() .submenu("New", window, cx, |submenu, window, cx| { submenu.menu_with_icon("File", IconName::File, Box::new(NewFile)) .menu_with_icon("Folder", IconName::Folder, Box::new(NewFolder)) }) .separator() .menu_with_icon("Delete", IconName::Trash, Box::new(Delete)) .separator() .menu("Properties", Box::new(ShowProperties)) }) ``` ### Add MenuItem without action Sometimes you may not like to define an action for a menu item, you just want add a `on_click` handler, in this case, the `item` and [PopupMenuItem] can help you: ```rust use gpui_component::{menu::PopupMenuItem, Button}; Button::new("custom-item-menu") .label("Options") .dropdown_menu(|menu, window, cx| { menu.item( PopupMenuItem::new("Custom Action") .disabled(false) .icon(IconName::Star) .on_click(|window, cx| { // Custom click handler logic println!("Custom Action Clicked!"); }) ) .separator() .menu("Standard Action", Box::new(StandardAction)) }) ``` ### Editor Menu with Shortcuts ```rust // Define actions with keyboard shortcuts actions!(editor, [Save, SaveAs, Find, Replace, ToggleWordWrap]); // Set up key bindings cx.bind_keys([ KeyBinding::new("ctrl-s", Save, Some("editor")), KeyBinding::new("ctrl-shift-s", SaveAs, Some("editor")), KeyBinding::new("ctrl-f", Find, Some("editor")), KeyBinding::new("ctrl-h", Replace, Some("editor")), ]); // Create menu with automatic shortcuts let editor_focus = cx.focus_handle(); Button::new("editor-menu") .label("Edit") .dropdown_menu(|menu, window, cx| { menu.action_context(editor_focus) .menu("Save", Box::new(Save)) // Shows "Ctrl+S" .menu("Save As...", Box::new(SaveAs)) // Shows "Ctrl+Shift+S" .separator() .menu("Find", Box::new(Find)) // Shows "Ctrl+F" .menu("Replace", Box::new(Replace)) // Shows "Ctrl+H" .separator() .menu_with_check("Word Wrap", true, Box::new(ToggleWordWrap)) }) ``` ### Settings Menu with Custom Elements ```rust Button::new("settings") .label("Settings") .dropdown_menu(|menu, window, cx| { menu.label("Display") .menu_element_with_check(dark_mode, Box::new(ToggleDarkMode), |window, cx| { h_flex() .gap_2() .child("Dark Mode") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child(if dark_mode { "On" } else { "Off" }) ) }) .separator() .label("Account") .menu_element_with_icon( IconName::User, Box::new(ShowProfile), |window, cx| { v_flex() .child("John Doe") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("john@example.com") ) } ) .separator() .link_with_icon("Help Center", IconName::Help, "https://help.example.com") .menu("Sign Out", Box::new(SignOut)) }) ``` ## Keyboard Shortcuts | Key | Action | | ----------------- | --------------------------------- | | `↑` / `↓` | Navigate menu items | | `←` / `→` | Navigate submenus | | `Enter` / `Space` | Activate menu item | | `Escape` | Close menu | | `Tab` | Close menu and focus next element | ## Best Practices 1. **Group Related Items**: Use separators to group related functionality 2. **Consistent Icons**: Use consistent iconography across your application 3. **Logical Order**: Place most common actions at the top 4. **Keyboard Shortcuts**: Provide shortcuts for frequently used actions 5. **Context Awareness**: Show only relevant items for the current context 6. **Progressive Disclosure**: Use submenus for complex hierarchies 7. **Clear Labels**: Use descriptive, action-oriented labels 8. **Reasonable Limits**: Use scrollable menus for more than 10-15 items [PopupMenu]: https://docs.rs/gpui-component/latest/gpui_component/menu/struct.PopupMenu.html [PopupMenuItem]: https://docs.rs/gpui-component/latest/gpui_component/menu/struct.PopupMenuItem.html [context_menu]: https://docs.rs/gpui-component/latest/gpui_component/menu/trait.ContextMenuExt.html#method.context_menu [Action]: https://docs.rs/gpui/latest/gpui/trait.Action.html ================================================ FILE: docs/docs/components/notification.md ================================================ --- title: Notification description: Display toast notifications that appear at the top right of the window with auto-dismiss functionality. --- # Notification A toast notification system for displaying temporary messages to users. Notifications appear at the top right of the window and can auto-dismiss after a timeout. Supports multiple variants (info, success, warning, error), custom content, titles, and action buttons. Perfect for status updates, confirmations, and user feedback. ## Import ```rust use gpui_component::{ notification::{Notification, NotificationType}, WindowExt }; ``` ## Usage ### Setup application root view for display of notifications You need to set up your application's root view to render the notification layer. This is typically done in your main application struct's render method. The [Root::render_notification_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_notification_layer) function handles rendering any active modals on top of your app content. ```rust use gpui_component::{TitleBar, Root}; struct Example {} impl Render for Example { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let notification_layer = Root::render_notification_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(TitleBar::new()) .child(div().flex_1().child("Hello world!")), ) // Render the notification layer on top of the app content .children(notification_layer) } } ``` ### Basic Notification ```rust // Simple string notification window.push_notification("This is a notification.", cx); // Using Notification builder Notification::new() .message("Your changes have been saved.") ``` ### Notification Types ```rust // Info notification (blue) window.push_notification( (NotificationType::Info, "File saved successfully."), cx, ); // Success notification (green) window.push_notification( (NotificationType::Success, "Payment processed successfully."), cx, ); // Warning notification (yellow/orange) window.push_notification( (NotificationType::Warning, "Network connection is unstable."), cx, ); // Error notification (red) window.push_notification( (NotificationType::Error, "Failed to save file. Please try again."), cx, ); ``` ### Notification with Title ```rust Notification::new() .title("Update Available") .message("A new version of the application is ready to install.") .with_type(NotificationType::Info) ``` ### Auto-hide Control ```rust // Disable auto-hide (manual dismiss only) Notification::new() .message("This notification stays until manually closed.") .autohide(false) // Default auto-hide after 5 seconds Notification::new() .message("This will disappear automatically.") .autohide(true) // default ``` ### With Action Button ```rust Notification::new() .title("Connection Lost") .message("Unable to connect to server.") .with_type(NotificationType::Error) .autohide(false) .action(|_, cx| { Button::new("retry") .primary() .label("Retry") .on_click(cx.listener(|this, _, window, cx| { // Perform retry action println!("Retrying connection..."); this.dismiss(window, cx); })) }) ``` ### Clickable Notifications ```rust Notification::new() .message("Click to view details") .on_click(cx.listener(|_, _, _, cx| { println!("Notification clicked"); // Handle notification click cx.notify(); })) ``` ### Custom Content ```rust use gpui_component::text::markdown; let markdown_content = r#" ## Custom Notification - **Feature**: New dashboard available - **Status**: Ready to use - [Learn more](https://example.com) "#; Notification::new() .content(|_, window, cx| { markdown(markdown_content).into_any_element() }) ``` ### Unique Notifications When you need to manage notifications manually, such as for long-running processes or persistent alerts, you can use unique IDs to push and remove notifications as needed. In this case, you can create a special `struct` in local scope, and use `id` methods with this struct to identify the notification. Then you can push the notification when needed, and later remove it using the same ID. Like this: ```rust // Using type-based ID for uniqueness struct UpdateNotification; Notification::new() .id::() .message("System update available") .autohide(false) // Using type + element ID for multiple unique notifications struct TaskNotification; Notification::warning("Task failed to complete") .id1::("task-123") .title("Task Failed") ``` Then remove the notification with `window.remove_notification::`, like this: ```rust // Later, dismiss the notification window.remove_notification::(cx); ``` ## Examples ### Form Validation Error ```rust Notification::error("Please correct the following errors before submitting.") .title("Validation Failed") .autohide(false) .action(|_, _, cx| { Button::new("review") .outline() .label("Review Form") .on_click(cx.listener(|this, _, window, cx| { // Navigate to form this.dismiss(window, cx); })) }) ``` ### File Upload Progress ```rust struct UploadNotification; // Start upload notification window.push_notification( Notification::info("Uploading file...") .id::() .title("File Upload") .autohide(false), cx, ); // Update to success when complete window.push_notification( Notification::success("File uploaded successfully!") .id::() .title("Upload Complete"), cx, ); ``` ### System Status Updates ```rust // Warning about maintenance Notification::warning("System maintenance will begin in 30 minutes.") .title("Scheduled Maintenance") .autohide(false) .action(|_, cx| { Button::new("details") .link() .label("View Details") .on_click(cx.listener(|this, _, window, cx| { // Show maintenance details this.dismiss(window, cx); })) }) ``` ### Batch Operation Results ```rust use gpui_component::text::markdown; let results_content = r#" ## Batch Operation Complete **Processed**: 150 items **Success**: 147 items **Failed**: 3 items [View failed items](/) "#; Notification::success("Batch operation completed with some failures.") .title("Operation Results") .content(|window, cx| { markdown(results_content).into_any_element() }) .autohide(false) ``` ### Interactive Confirmation ```rust struct SaveConfirmation; Notification::new() .id::() .title("Unsaved Changes") .message("You have unsaved changes. Save before leaving?") .autohide(false) .action(|_, cx| { Button::new("save") .primary() .label("Save") .on_click(cx.listener(|this, _, window, cx| { // Perform save println!("Saving changes..."); this.dismiss(window, cx); })) }) .on_click(cx.listener(|_, _, _, cx| { println!("Save reminder clicked"); cx.notify(); })) ``` ================================================ FILE: docs/docs/components/number-input.md ================================================ --- title: NumberInput description: Number input component with increment/decrement controls and numeric formatting. --- # NumberInput A specialized input component for numeric values with built-in increment/decrement buttons and support for min/max values, step values, and number formatting with thousands separators. ## Import ```rust use gpui_component::input::{InputState, NumberInput, NumberInputEvent, StepAction}; ``` ## Usage ### Basic Number Input ```rust let number_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Enter number") .default_value("1") ); NumberInput::new(&number_input) ``` ### With Min/Max Validation ```rust // Integer input with validation let integer_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Integer value") .pattern(Regex::new(r"^\d+$").unwrap()) // Only positive integers ); NumberInput::new(&integer_input) ``` ### With Number Formatting ```rust use gpui_component::input::MaskPattern; // Currency input with thousands separator let currency_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Amount") .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), // 2 decimal places }) ); NumberInput::new(¤cy_input) ``` ### Different Sizes ```rust // Large size NumberInput::new(&input).large() // Medium size (default) NumberInput::new(&input) // Small size NumberInput::new(&input).small() ``` ### With Prefix and Suffix ```rust use gpui_component::{button::{Button, ButtonVariants}, IconName}; // With currency prefix NumberInput::new(&input) .prefix(div().child("$")) // With info button suffix NumberInput::new(&input) .suffix( Button::new("info") .ghost() .icon(IconName::Info) .xsmall() ) ``` ### Disabled State ```rust NumberInput::new(&input).disabled(true) ``` ### Without Default Styling ```rust // For custom container styling div() .w_full() .bg(cx.theme().secondary) .rounded(cx.theme().radius) .child(NumberInput::new(&input).appearance(false)) ``` ### Handle Number Input Events ```rust let number_input = cx.new(|cx| InputState::new(window, cx)); let mut value: i64 = 0; // Subscribe to input changes cx.subscribe_in(&number_input, window, |view, state, event, window, cx| { match event { InputEvent::Change => { let text = state.read(cx).value(); if let Ok(new_value) = text.parse::() { view.value = new_value; } } _ => {} } }); // Subscribe to increment/decrement actions cx.subscribe_in(&number_input, window, |view, state, event, window, cx| { match event { NumberInputEvent::Step(step_action) => { match step_action { StepAction::Increment => { view.value += 1; state.update(cx, |input, cx| { input.set_value(view.value.to_string(), window, cx); }); } StepAction::Decrement => { view.value -= 1; state.update(cx, |input, cx| { input.set_value(view.value.to_string(), window, cx); }); } } } } }); ``` ### Programmatic Control ```rust // Increment programmatically NumberInput::increment(&number_input, window, cx); // Decrement programmatically NumberInput::decrement(&number_input, window, cx); ``` ## API Reference ### NumberInput | Method | Description | | ------------------------------ | ------------------------------------------ | | `new(state)` | Create number input with InputState entity | | `placeholder(str)` | Set placeholder text | | `size(size)` | Set input size (small, medium, large) | | `prefix(el)` | Add prefix element | | `suffix(el)` | Add suffix element | | `appearance(bool)` | Enable/disable default styling | | `disabled(bool)` | Set disabled state | | `increment(state, window, cx)` | Increment value programmatically | | `decrement(state, window, cx)` | Decrement value programmatically | ### NumberInputEvent | Event | Description | | ------------------ | ---------------------------------- | | `Step(StepAction)` | Increment/decrement button pressed | ### StepAction | Action | Description | | ----------- | ------------------------- | | `Increment` | Value should be increased | | `Decrement` | Value should be decreased | ### InputState (Number-specific methods) | Method | Description | | ----------------------------------- | ------------------------------------------------------- | | `pattern(regex)` | Set regex pattern for validation (e.g., digits only) | | `mask_pattern(MaskPattern::Number)` | Set number formatting with separator and decimal places | | `value()` | Get current display value (formatted) | | `unmask_value()` | Get actual numeric value (unformatted) | ### MaskPattern::Number | Field | Type | Description | | ----------- | --------------- | -------------------------------------- | | `separator` | `Option` | Thousands separator (e.g., ',' or ' ') | | `fraction` | `Option` | Number of decimal places | ## Keyboard Navigation | Key | Action | | ----------- | -------------------------- | | `↑` | Increment value | | `↓` | Decrement value | | `Tab` | Navigate to next field | | `Shift+Tab` | Navigate to previous field | | `Enter` | Submit/confirm value | | `Escape` | Clear input (if enabled) | ## Examples ### Integer Counter ```rust struct CounterView { counter_input: Entity, counter_value: i32, } impl CounterView { fn new(window: &mut Window, cx: &mut Context) -> Self { let counter_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Count") .default_value("0") .pattern(Regex::new(r"^-?\d+$").unwrap()) // Allow negative integers ); let _subscription = cx.subscribe_in(&counter_input, window, Self::on_number_event); Self { counter_input, counter_value: 0, } } fn on_number_event( &mut self, state: &Entity, event: &NumberInputEvent, window: &mut Window, cx: &mut Context, ) { match event { NumberInputEvent::Step(StepAction::Increment) => { self.counter_value += 1; state.update(cx, |input, cx| { input.set_value(self.counter_value.to_string(), window, cx); }); } NumberInputEvent::Step(StepAction::Decrement) => { self.counter_value -= 1; state.update(cx, |input, cx| { input.set_value(self.counter_value.to_string(), window, cx); }); } } } } // Usage NumberInput::new(&self.counter_input) ``` ### Currency Input ```rust struct PriceInput { price_input: Entity, price_value: f64, } impl PriceInput { fn new(window: &mut Window, cx: &mut Context) -> Self { let price_input = cx.new(|cx| InputState::new(window, cx) .placeholder("0.00") .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), }) ); Self { price_input, price_value: 0.0, } } } // Usage with currency prefix h_flex() .gap_2() .child(div().child("$")) .child(NumberInput::new(&self.price_input)) ``` ### Quantity Selector with Limits ```rust struct QuantitySelector { quantity_input: Entity, quantity: u32, min_quantity: u32, max_quantity: u32, } impl QuantitySelector { fn new(window: &mut Window, cx: &mut Context) -> Self { let min_quantity = 1; let max_quantity = 99; let quantity_input = cx.new(|cx| InputState::new(window, cx) .default_value(min_quantity.to_string()) .pattern(Regex::new(&format!(r"^[{}-{}]\d*$", min_quantity, max_quantity)).unwrap()) ); let _subscription = cx.subscribe_in(&quantity_input, window, Self::on_quantity_event); Self { quantity_input, quantity: min_quantity, min_quantity, max_quantity, } } fn on_quantity_event( &mut self, state: &Entity, event: &NumberInputEvent, window: &mut Window, cx: &mut Context, ) { match event { NumberInputEvent::Step(StepAction::Increment) => { if self.quantity < self.max_quantity { self.quantity += 1; state.update(cx, |input, cx| { input.set_value(self.quantity.to_string(), window, cx); }); } } NumberInputEvent::Step(StepAction::Decrement) => { if self.quantity > self.min_quantity { self.quantity -= 1; state.update(cx, |input, cx| { input.set_value(self.quantity.to_string(), window, cx); }); } } } } } // Usage NumberInput::new(&self.quantity_input).small() ``` ### Floating Point Input ```rust struct FloatInput { float_input: Entity, float_value: f64, step: f64, } impl FloatInput { fn new(window: &mut Window, cx: &mut Context) -> Self { let float_input = cx.new(|cx| InputState::new(window, cx) .placeholder("0.0") .pattern(Regex::new(r"^-?\d*\.?\d*$").unwrap()) // Allow decimal numbers ); Self { float_input, float_value: 0.0, step: 0.1, } } fn on_float_event( &mut self, state: &Entity, event: &NumberInputEvent, window: &mut Window, cx: &mut Context, ) { match event { NumberInputEvent::Step(StepAction::Increment) => { self.float_value += self.step; state.update(cx, |input, cx| { input.set_value(format!("{:.1}", self.float_value), window, cx); }); } NumberInputEvent::Step(StepAction::Decrement) => { self.float_value -= self.step; state.update(cx, |input, cx| { input.set_value(format!("{:.1}", self.float_value), window, cx); }); } } } } ``` ## Best Practices 1. **Validation**: Always validate numeric input on both client and server side 2. **Range Limits**: Implement min/max validation for user safety 3. **Step Size**: Choose appropriate step values for your use case 4. **Error Handling**: Provide clear feedback for invalid input 5. **Formatting**: Use consistent number formatting across your application 6. **Performance**: Debounce rapid increment/decrement actions if needed 7. **Accessibility**: Always provide proper labels and descriptions ================================================ FILE: docs/docs/components/otp-input.md ================================================ --- title: OtpInput description: One-time password input component with multiple fields, auto-focus, and paste handling. --- # OtpInput A specialized input component for one-time passwords (OTP) that displays multiple input fields in a grid layout. Perfect for SMS verification codes, authenticator app codes, and other numeric verification scenarios. ## Import ```rust use gpui_component::input::{OtpInput, OtpState}; ``` ## Usage ### Basic OTP Input ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); OtpInput::new(&otp_state) ``` ### With Default Value ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .default_value("123456") ); OtpInput::new(&otp_state) ``` ### Masked OTP Input ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .masked(true) .default_value("123456") ); OtpInput::new(&otp_state) ``` ### Different Sizes ```rust // Small size OtpInput::new(&otp_state).small() // Medium size (default) OtpInput::new(&otp_state) // Large size OtpInput::new(&otp_state).large() // Custom size OtpInput::new(&otp_state).with_size(px(55.)) ``` ### Grouped Layout ```rust // Single group (all fields together) OtpInput::new(&otp_state).groups(1) // Two groups (default) - splits fields in half OtpInput::new(&otp_state).groups(2) // Three groups - splits fields into thirds OtpInput::new(&otp_state).groups(3) ``` ### Disabled State ```rust OtpInput::new(&otp_state).disabled(true) ``` ### Different Length Codes ```rust // 4-digit PIN let pin_state = cx.new(|cx| OtpState::new(4, window, cx)); OtpInput::new(&pin_state).groups(1) // 6-digit SMS code (most common) let sms_state = cx.new(|cx| OtpState::new(6, window, cx)); OtpInput::new(&sms_state) // 8-digit authenticator code let auth_state = cx.new(|cx| OtpState::new(8, window, cx)); OtpInput::new(&auth_state).groups(2) ``` ### Handle OTP Events ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { match event { InputEvent::Change => { let code = state.read(cx).value(); if code.len() == 6 { println!("Complete OTP: {}", code); // Automatically submit when complete this.verify_otp(&code, cx); } } InputEvent::Focus => println!("OTP input focused"), InputEvent::Blur => println!("OTP input lost focus"), _ => {} } }); ``` ### Programmatic Control ```rust // Set value programmatically otp_state.update(cx, |state, cx| { state.set_value("123456", window, cx); }); // Toggle masking otp_state.update(cx, |state, cx| { state.set_masked(true, window, cx); }); // Focus the input otp_state.update(cx, |state, cx| { state.focus(window, cx); }); // Get current value let current_value = otp_state.read(cx).value(); ``` ## API Reference ### OtpState | Method | Description | | ------------------------------ | -------------------------------------------- | | `new(length, window, cx)` | Create a new OTP state with specified length | | `default_value(str)` | Set initial value | | `masked(bool)` | Enable masked display (shows asterisks) | | `set_value(str, window, cx)` | Set OTP value programmatically | | `value()` | Get current OTP value | | `set_masked(bool, window, cx)` | Toggle masked display | | `focus(window, cx)` | Focus the OTP input | | `focus_handle(cx)` | Get focus handle | ### OtpInput | Method | Description | | ---------------- | ---------------------------------------- | | `new(state)` | Create OTP input with state entity | | `groups(n)` | Set number of visual groups (default: 2) | | `disabled(bool)` | Set disabled state | | `small()` | Small size (6x6 px fields) | | `large()` | Large size (11x11 px fields) | | `with_size(px)` | Custom field size | ### InputEvent | Event | Description | | -------- | ------------------------------------------------- | | `Change` | Emitted when OTP is complete (all digits entered) | | `Focus` | Input received focus | | `Blur` | Input lost focus | ## Examples ### SMS Verification ```rust struct SmsVerification { otp_state: Entity, phone_number: String, is_verifying: bool, } impl SmsVerification { fn new(window: &mut Window, cx: &mut Context) -> Self { let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let code = state.read(cx).value(); this.verify_sms_code(&code, cx); } }); Self { otp_state, phone_number: "+1234567890".to_string(), is_verifying: false, } } fn verify_sms_code(&mut self, code: &str, cx: &mut Context) { self.is_verifying = true; // API call to verify SMS code println!("Verifying SMS code: {}", code); cx.notify(); } } impl Render for SmsVerification { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child(format!("Enter the 6-digit code sent to {}", self.phone_number)) .child(OtpInput::new(&self.otp_state)) .when(self.is_verifying, |this| { this.child("Verifying...") }) } } ``` ### Two-Factor Authentication ```rust struct TwoFactorAuth { otp_state: Entity, is_masked: bool, } impl TwoFactorAuth { fn new(window: &mut Window, cx: &mut Context) -> Self { let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .masked(true) ); Self { otp_state, is_masked: true, } } fn toggle_visibility(&mut self, window: &mut Window, cx: &mut Context) { self.is_masked = !self.is_masked; self.otp_state.update(cx, |state, cx| { state.set_masked(self.is_masked, window, cx); }); } } impl Render for TwoFactorAuth { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child("Enter your authenticator code") .child(OtpInput::new(&self.otp_state)) .child( Button::new("toggle-visibility") .label(if self.is_masked { "Show" } else { "Hide" }) .on_click(cx.listener(Self::toggle_visibility)) ) } } ``` ### PIN Entry ```rust struct PinEntry { pin_state: Entity, attempts: usize, max_attempts: usize, } impl PinEntry { fn new(window: &mut Window, cx: &mut Context) -> Self { let pin_state = cx.new(|cx| OtpState::new(4, window, cx) .masked(true) ); cx.subscribe(&pin_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let pin = state.read(cx).value(); this.verify_pin(&pin, cx); } }); Self { pin_state, attempts: 0, max_attempts: 3, } } fn verify_pin(&mut self, pin: &str, cx: &mut Context) { self.attempts += 1; // Simulate PIN verification if pin == "1234" { println!("PIN verified successfully!"); } else { println!("Incorrect PIN. Attempts: {}/{}", self.attempts, self.max_attempts); // Clear PIN on incorrect attempt self.pin_state.update(cx, |state, cx| { state.set_value("", window, cx); }); } cx.notify(); } } impl Render for PinEntry { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let is_locked = self.attempts >= self.max_attempts; v_flex() .gap_4() .child("Enter your 4-digit PIN") .child( OtpInput::new(&self.pin_state) .groups(1) .disabled(is_locked) ) .when(is_locked, |this| { this.child("Too many attempts. Please try again later.") }) .when(self.attempts > 0 && !is_locked, |this| { this.child(format!( "Incorrect PIN. {} attempts remaining.", self.max_attempts - self.attempts )) }) } } ``` ## Behavior ### Input Handling - **Numeric Only**: Accepts only digits (0-9) - **Auto-Focus**: Automatically moves to next field when digit is entered - **Backspace**: Removes current digit and moves to previous field - **Length Limit**: Prevents input beyond specified length - **Auto-Complete**: Emits `Change` event when all fields are filled ### Visual Feedback - **Focus Indicator**: Blue border and blinking cursor on active field - **Masking**: Shows asterisk icons instead of numbers when enabled - **Grouping**: Visual separation of fields into groups for better readability - **Disabled State**: Grayed out appearance when disabled ### Keyboard Navigation - **Arrow Keys**: Navigate between fields - **Tab**: Move to next focusable element - **Shift+Tab**: Move to previous focusable element - **Backspace**: Delete current digit and move backward - **Delete**: Clear current field ## Common Patterns ### Auto-Submit on Complete ```rust cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let code = state.read(cx).value(); if code.len() == 6 { // Auto-submit when complete this.submit_verification_code(&code, cx); } } }); ``` ### Clear on Focus ```rust cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Focus = event { // Clear previous value when user starts entering new code state.update(cx, |state, cx| { state.set_value("", window, cx); }); } }); ``` ### Resend Code Timer ```rust struct OtpWithResend { otp_state: Entity, resend_timer: Option, can_resend: bool, } // Implementation would include timer logic for resend functionality ``` ================================================ FILE: docs/docs/components/pagination.md ================================================ --- title: Pagination description: Pagination with page navigation, next and previous links. --- # Pagination The [Pagination] component provides page navigation with next and previous links. It displays page numbers and allows users to navigate through multiple pages of content. ## Import ```rust use gpui_component::pagination::Pagination; ``` ## Usage ### Basic Pagination ```rust Pagination::new("my-pagination") .current_page(5) .total_pages(10) .on_click(|page, _, cx| { println!("Navigated to page: {}", page); }) ``` ### With Visible Pages By default, the pagination shows up to 5 visible page buttons. You can customize this with `visible_pages()`: ```rust Pagination::new("my-pagination") .current_page(1) .total_pages(50) .visible_pages(10) .on_click(|page, _, cx| { // Handle page change }) ``` ### Compact Style The compact style only shows the previous and next buttons with icons, without displaying page numbers. Use `compact` method to enable compact style: ```rust Pagination::new("my-pagination") .compact() .current_page(3) .total_pages(10) .on_click(|page, _, cx| { // Handle page change }) ``` ### Different Sizes The Pagination supports the [Sizable] trait for different sizes: ```rust use gpui_component::{Sizable as _, Size}; Pagination::new("my-pagination") .xsmall() .current_page(1) .total_pages(10) Pagination::new("my-pagination") .small() .current_page(1) .total_pages(10) Pagination::new("my-pagination") .current_page(1) .total_pages(10) // Medium (default) Pagination::new("my-pagination") .large() .current_page(1) .total_pages(10) ``` ### Disabled State ```rust Pagination::new("my-pagination") .current_page(4) .total_pages(10) .disabled(true) .on_click(|_, _, _| {}) ``` ### Handle Page Change Events The `on_click` callback receives the new page number when users click on page numbers, previous, or next buttons: ```rust Pagination::new("my-pagination") .current_page(current_page) .total_pages(total_pages) .on_click(|page, _, cx| { // Update your state with the new page // The page number is 1-based }) ``` ## API Reference - [Pagination] ### Sizing Implements [Sizable] trait: - `xsmall()` - Extra small size - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size - `with_size(size)` - Set custom size ### Methods - `current_page(page: usize)` - Set the current page number (1-based). The value will be clamped between 1 and total_pages. - `total_pages(pages: usize)` - Set the total number of pages. - `visible_pages(max: usize)` - Set the maximum number of visible page buttons (default: 5). - `compact()` - Enable compact style (only shows prev/next buttons with icons). - `disabled(bool)` - Set the disabled state. - `on_click(handler)` - Set the handler for page change events. ## Examples ### With State Management ```rust let mut current_page = 1; let total_pages = 20; Pagination::new("pagination") .current_page(current_page) .total_pages(total_pages) .on_click({ let entity = entity.clone(); move |page, _, cx| { entity.update(cx, |this, cx| { this.current_page = *page; cx.notify(); }); } }) ``` ### Large Dataset Pagination For large datasets, use `visible_pages()` to show more page options: ```rust Pagination::new("large-pagination") .current_page(25) .total_pages(100) .visible_pages(10) .on_click(|page, _, cx| { // Load data for the new page }) ``` [Pagination]: https://docs.rs/gpui-component/latest/gpui_component/pagination/struct.Pagination.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ================================================ FILE: docs/docs/components/plot.md ================================================ --- title: Plot description: A low-level plotting library for creating custom charts and data visualizations. --- # Plot The `plot` module provides low-level building blocks for creating custom charts. It includes scales, shapes, and utilities that power the high-level `Chart` components. ## Import ```rust use gpui_component::plot::{ scale::{Scale, ScaleLinear, ScaleBand, ScalePoint, ScaleOrdinal}, shape::{Bar, Stack, Line, Area, Pie, Arc}, PlotAxis, AxisText }; ``` ## Scales Scales map a dimension of abstract data to a visual representation. ### ScaleLinear Maps a continuous quantitative domain to a continuous range. ```rust let scale = ScaleLinear::new( vec![0., 100.], // Domain (data values) vec![0., 500.] // Range (pixel coordinates) ); scale.tick(&50.); // Returns pixel position ``` ### ScaleBand Maps a discrete domain to a continuous range, useful for bar charts. ```rust let scale = ScaleBand::new( vec!["A", "B", "C"], // Domain vec![0., 300.] // Range ) .padding_inner(0.1) .padding_outer(0.1); scale.band_width(); // Returns width of each band scale.tick(&"A"); // Returns start position of band "A" ``` ### ScalePoint Maps a discrete domain to a set of points in a continuous range, useful for scatter plots or line charts with categorical axes. ```rust let scale = ScalePoint::new( vec!["A", "B", "C"], // Domain vec![0., 300.] // Range ); scale.tick(&"A"); // Returns position of point "A" ``` ### ScaleOrdinal Maps a discrete domain to a discrete range. ```rust let scale = ScaleOrdinal::new( vec!["A", "B", "C"], // Domain vec![color1, color2, color3] // Range ); scale.map(&"A"); // Returns color1 ``` ## Shapes ### Bar Renders a bar shape, commonly used in bar charts. ```rust Bar::new() .data(data) .band_width(30.) .x(|d| x_scale.tick(&d.category)) .y0(|d| y_scale.tick(&0.).unwrap()) .y1(|d| y_scale.tick(&d.value)) .fill(|d| color_scale.map(&d.category)) .paint(&bounds, window, cx); ``` ### Line Renders a line shape, commonly used in line charts. ```rust Line::new() .data(data) .x(|d| x_scale.tick(&d.date)) .y(|d| y_scale.tick(&d.value)) .stroke(cx.theme().chart_1) .stroke_width(px(2.)) .paint(&bounds, window); ``` #### Line with Dots Supports rendering dots at data points. ```rust Line::new() .data(data) .x(|d| x_scale.tick(&d.date)) .y(|d| y_scale.tick(&d.value)) .dot() .dot_size(px(4.)) .paint(&bounds, window); ``` ### Area Renders an area shape, commonly used in area charts. ```rust Area::new() .data(data) .x(|d| x_scale.tick(&d.date)) .y0(height) // Baseline .y1(|d| y_scale.tick(&d.value)) .fill(cx.theme().chart_1.opacity(0.5)) .stroke(cx.theme().chart_1) .paint(&bounds, window); ``` ### Pie & Arc Renders pie charts and donut charts using `Pie` layout and `Arc` shape. ```rust // 1. Compute pie layout let pie = Pie::new() .value(|d| Some(d.value)) .pad_angle(0.05); let arcs = pie.arcs(&data); // 2. Render arcs let arc_shape = Arc::new() .inner_radius(0.) .outer_radius(100.); for arc_data in arcs { arc_shape.paint( &arc_data, color_scale.map(&arc_data.data.category), // Color None, // Override inner radius None, // Override outer radius &bounds, window ); } ``` ### Stack Computes stacked layout for data series. ```rust let stack = Stack::new() .data(data) .keys(vec!["series1", "series2"]) .value(|d, key| match key { "series1" => Some(d.val1), "series2" => Some(d.val2), _ => None }); let series = stack.series(); // Returns Vec> ``` ## Components ### PlotAxis Renders chart axes with labels and ticks. ```rust PlotAxis::new() .x(height) // Y position for X axis .x_label(labels) // Iterator of AxisText .stroke(cx.theme().border) .paint(&bounds, window, cx); ``` ## Examples ### Custom Bar Chart Implementation Here's how to implement a custom stacked bar chart using low-level plot primitives: ```rust struct StackedBarChart { data: Vec, series: Vec>, } impl StackedBarChart { pub fn new(data: Vec) -> Self { let series = Stack::new() .data(data.clone()) .keys(vec!["desktop", "mobile"]) .value(|d, key| match key { "desktop" => Some(d.desktop), "mobile" => Some(d.mobile), _ => None, }) .series(); Self { data, series } } } impl Plot for StackedBarChart { fn paint(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { // 1. Setup Scales let x = ScaleBand::new( self.data.iter().map(|v| v.date.clone()).collect(), vec![0., width], ); let y = ScaleLinear::new(vec![0., max_value], vec![height, 0.]); // 2. Draw Axis // ... (axis rendering logic) // 3. Draw Stacked Bars let bar = Bar::new() .stack_data(&self.series) .band_width(x.band_width()) .x(move |d| x.tick(&d.data.date)) .fill(move |_| cx.theme().chart_1); bar.paint(&bounds, window, cx); } } ``` ================================================ FILE: docs/docs/components/popover.md ================================================ --- title: Popover description: A floating overlay that displays rich content relative to a trigger element. --- # Popover Popover component for displaying floating content that appears when interacting with a trigger element. Supports multiple positioning options, custom content, different trigger methods, and automatic dismissal behaviors. Perfect for tooltips, menus, forms, and other contextual information. ## Import ```rust use gpui_component::popover::{Popover}; ``` ## Usage ### Basic Popover :::info Any element that implements [Selectable] can be used as a trigger, for example, a [Button]. Any element that implements [RenderOnce] or [Render] can be used as popover content, use `.child(...)` to add children directly. ::: ```rust use gpui::ParentElement as _; use gpui_component::{button::Button, popover::Popover}; Popover::new("basic-popover") .trigger(Button::new("trigger").label("Click me").outline()) .child("Hello, this is a popover!") .child("It appears when you click the button.") ``` ### Popover with Custom Positioning The `anchor` method allows you to specify where the popover appears relative to the trigger element. It accepts both `Corner` and `Anchor` types. **Using `Corner` type** (4 corner positions): ```rust use gpui::Corner; Popover::new("positioned-popover") .anchor(Corner::TopRight) .trigger(Button::new("top-right").label("Top Right").outline()) .child("This popover appears at the top right") ``` **Using `Anchor` type** (6 positions including center): The `Anchor` type provides more positioning options, including center positions: ```rust use gpui_component::Anchor; // Top positions Popover::new("top-left") .anchor(Anchor::TopLeft) .trigger(Button::new("btn").label("Top Left").outline()) .child("Anchored to top left") Popover::new("top-center") .anchor(Anchor::TopCenter) .trigger(Button::new("btn").label("Top Center").outline()) .child("Anchored to top center") Popover::new("top-right") .anchor(Anchor::TopRight) .trigger(Button::new("btn").label("Top Right").outline()) .child("Anchored to top right") // Bottom positions Popover::new("bottom-left") .anchor(Anchor::BottomLeft) .trigger(Button::new("btn").label("Bottom Left").outline()) .child("Anchored to bottom left") Popover::new("bottom-center") .anchor(Anchor::BottomCenter) .trigger(Button::new("btn").label("Bottom Center").outline()) .child("Anchored to bottom center") Popover::new("bottom-right") .anchor(Anchor::BottomRight) .trigger(Button::new("btn").label("Bottom Right").outline()) .child("Anchored to bottom right") ``` ### View in Popover You can add any `Entity` that implemented [Render] as the popover content. ```rust let view = cx.new(|_| MyView::new()); Popover::new("form-popover") .anchor(Corner::BottomLeft) .trigger(Button::new("show-form").label("Open Form").outline()) .child(view.clone()) ``` ### Add content by `content` method The `content` method allows you to create more complex popover content using a closure. This is useful when you need to build dynamic content or need access to the popover's context. This method will let us to have `&mut PopoverState`, `&mut Window` and `&mut Context` parameters in the closure is to allow you to interact with the popover's state and the overall application context if needed. :::warning This `content` callback will called every time on render the popover. So, you should avoid creating new elements or entities in the content closure or other heavy operations that may impact performance. ::: And `content` will works with `child`, `children` methods together. ```rust use gpui::ParentElement as _; use gpui_component::popover::Popover; Popover::new("complex-popover") .anchor(Corner::BottomLeft) .trigger(Button::new("complex").label("Complex Content").outline()) .content(|_, _, _| { div() .child("This popover has complex content.") .child( Button::new("action-btn") .label("Perform Action") .outline() ) }) ``` ### Right-Click Popover Sometimes you may want to show a popover on right-click, for example, to create a special your ownen context menu. The `mouse_button` method allows you to specify which mouse button triggers the popover. ```rust use gpui::MouseButton; Popover::new("context-menu") .anchor(Corner::BottomRight) .mouse_button(MouseButton::Right) .trigger(Button::new("right-click").label("Right Click Me").outline()) .child("Context Menu") .child(Divider::horizontal()) .child("This is a custom context menu.") ``` ### Dismiss Popover manually If you want to dismiss the popover programmatically from within the content, you can emit a `DismissEvent`. In this case, you should use `content` method to create the popover content so you have access to the `cx: &mut Context`. ```rust use gpui_component::{DismissEvent, popover::Popover}; Popover::new("dismiss-popover") .trigger(Button::new("dismiss").label("Dismiss Popover").outline()) .content(|_, cx| { div() .child("Click the button below to dismiss this popover.") .child( Button::new("close-btn") .label("Close Popover") .on_click(cx.listener(|_, _, _, cx| { // NOTE: Here `cx` is `&mut Context` type, so we can emit DismissEvent. cx.emit(DismissEvent); })) ) }) ``` ### Styling Popover Like the others components in GPUI Component, the `appearance(false)` method can be used to disable the default styling of the popover, allowing you to fully customize its appearance. And the `Popover` has implemented the [Styled] trait, so you can use all the styling methods provided by GPUI to style the popover content as you like. ```rust // For custom styled popovers or when you want full control Popover::new("custom-popover") .appearance(false) .trigger(Button::new("custom").label("Custom Style")) .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .p_6() .rounded_xl() .shadow_2xl() .child("Fully custom styled popover") ``` ### Control Open State There have `open` and `on_open_change` methods to control the open state of the popover programmatically. This is useful when you want to synchronize the popover's open state with other UI elements or application state. :::tip When you use `open` to control the popover's open state, that means you have take full control of it, so you need to update the state in `on_open_change` callback to keep the popover working correctly. ::: ```rust use gpui_component::popover::Popover; struct MyView { popover_open: bool, } Popover::new("controlled-popover") .open(self.open) .on_open_change(cx.listener(|this, open: &bool, _, cx| { this.popover_open = *open; cx.notify(); })) .trigger(Button::new("control-btn").label("Control Popover").outline()) .child("This popover's open state is controlled programmatically.") ``` ### Default Open The `default_open` method allows you to set the initial open state of the popover when it is first rendered. Please note that if you use the `open` method to control the popover's open state, the `default_open` setting will be ignored. ```rust use gpui_component::popover::Popover; Popover::new("default-open-popover") .default_open(true) .trigger(Button::new("default-open-btn").label("Default Open").outline()) .child("This popover is open by default when first rendered.") ``` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [Selectable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Selectable.html [Render]: https://docs.rs/gpui/latest/gpui/trait.Render.html [RenderOnce]: https://docs.rs/gpui/latest/gpui/trait.RenderOnce.html [Styled]: https://docs.rs/gpui/latest/gpui/trait.Styled.html ================================================ FILE: docs/docs/components/progress.md ================================================ --- title: Progress description: Displays an indicator showing the completion progress of a task, typically displayed as a progress bar or circular indicator. --- # Progress Progress components visually represent the completion percentage of a task. The library provides two variants: - **[Progress](#progress)** - A linear horizontal progress bar - **[ProgressCircle](#progresscircle)** - A circular progress indicator Both components feature smooth animations, customizable colors, and automatic styling that adapts to the current theme. ## Progress ```rust use gpui_component::progress::{Progress, ProgressCircle}; ``` ### Usage ```rust Progress::new("my-progress") .value(50.0) // 50% complete ``` ### Different Progress Values ```rust // Starting state (0%) Progress::new("progress-0") .value(0.0) // Partially complete (25%) Progress::new("progress-25") .value(25.0) // Nearly complete (75%) Progress::new("progress-75") .value(75.0) // Complete (100%) Progress::new("progress-100") .value(100.0) ``` ### Indeterminate State ```rust // For unknown progress duration Progress::new("indeterminate") .value(-1.0) // Any negative value shows as 0% // Or explicitly set to 0 for starting state Progress::new("starting") .value(0.0) ``` ### Dynamic Progress Updates ```rust struct MyComponent { progress_value: f32, } impl MyComponent { fn update_progress(&mut self, new_value: f32) { self.progress_value = new_value.clamp(0.0, 100.0); } fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( h_flex() .gap_2() .child(Button::new("decrease").label("-").on_click( cx.listener(|this, _, _, _| { this.update_progress(this.progress_value - 10.0); }) )) .child(Button::new("increase").label("+").on_click( cx.listener(|this, _, _, _| { this.update_progress(this.progress_value + 10.0); }) )) ) .child(Progress::new("progress").value(self.progress_value)) .child(format!("{}%", self.progress_value as i32)) } } ``` ### File Upload Progress ```rust struct FileUpload { bytes_uploaded: u64, total_bytes: u64, } impl FileUpload { fn progress_percentage(&self) -> f32 { if self.total_bytes == 0 { 0.0 } else { (self.bytes_uploaded as f32 / self.total_bytes as f32) * 100.0 } } fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child("Uploading file...") .child(Progress::new("upload-progress").value(self.progress_percentage())) .child(format!( "{} / {} bytes", self.bytes_uploaded, self.total_bytes )) } } ``` ### Loading States ```rust struct LoadingComponent { is_loading: bool, progress: f32, } impl LoadingComponent { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_3() .when(self.is_loading, |this| { this.child("Loading...") .child(Progress::new("loading-progress").value(self.progress)) }) .when(!self.is_loading, |this| { this.child("Task completed!") .child(Progress::new("loading-progress").value(100.0)) }) } } ``` ### Multi-Step Process ```rust enum ProcessStep { Initializing, Processing, Finalizing, Complete, } struct MultiStepProcess { current_step: ProcessStep, step_progress: f32, } impl MultiStepProcess { fn overall_progress(&self) -> f32 { let base_progress = match self.current_step { ProcessStep::Initializing => 0.0, ProcessStep::Processing => 33.33, ProcessStep::Finalizing => 66.66, ProcessStep::Complete => 100.0, }; if matches!(self.current_step, ProcessStep::Complete) { 100.0 } else { base_progress + (self.step_progress / 3.0) } } fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child(match self.current_step { ProcessStep::Initializing => "Initializing...", ProcessStep::Processing => "Processing data...", ProcessStep::Finalizing => "Finalizing...", ProcessStep::Complete => "Complete!", }) .child(Progress::new("overall-progress").value(self.overall_progress())) .child(format!("{:.1}% complete", self.overall_progress())) } } ``` ## ProgressCircle A circular progress indicator component that displays progress as an arc around a circle. Perfect for compact spaces, button icons, or when you want a more modern, space-efficient progress display. ```rust use gpui_component::progress::ProgressCircle; ``` ### Basic ProgressCircle ```rust ProgressCircle::new("my-progress-circle") .value(50.0) // 50% complete ``` ### Different Sizes ProgressCircle supports different sizes through the `Sizable` trait: ```rust // Extra small ProgressCircle::new("progress-xs") .value(25.0) .xsmall() // Small ProgressCircle::new("progress-sm") .value(50.0) .small() // Medium (default) ProgressCircle::new("progress-md") .value(75.0) .medium() // Large ProgressCircle::new("progress-lg") .value(100.0) .large() // Custom size ProgressCircle::new("progress-custom") .value(60.0) .size(px(80.)) ``` ### Custom Colors ```rust // Use theme colors (default) ProgressCircle::new("progress-default") .value(50.0) // Custom color ProgressCircle::new("progress-green") .value(75.0) .color(cx.theme().green) // Different color variants ProgressCircle::new("progress-blue") .value(60.0) .color(cx.theme().blue) ProgressCircle::new("progress-yellow") .value(40.0) .color(cx.theme().yellow) ProgressCircle::new("progress-red") .value(80.0) .color(cx.theme().red) ``` ### With Labels ```rust h_flex() .gap_2() .items_center() .child( ProgressCircle::new("download-progress") .value(65.0) .size_4() ) .child("Downloading... 65%") ``` ## Examples ### Task Progress with Status ```rust struct TaskProgress { completed_tasks: usize, total_tasks: usize, } impl TaskProgress { fn progress_value(&self) -> f32 { if self.total_tasks == 0 { 0.0 } else { (self.completed_tasks as f32 / self.total_tasks as f32) * 100.0 } } fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child( h_flex() .justify_between() .child("Task Progress") .child(format!("{}/{}", self.completed_tasks, self.total_tasks)) ) .child(Progress::new("task-progress").value(self.progress_value())) .when(self.completed_tasks == self.total_tasks, |this| { this.child("All tasks completed!") }) } } ``` ### Download Progress with Speed ```rust struct DownloadProgress { downloaded: u64, total_size: u64, speed_mbps: f32, } impl DownloadProgress { fn eta_seconds(&self) -> u64 { if self.speed_mbps == 0.0 { 0 } else { let remaining_mb = (self.total_size - self.downloaded) as f32 / 1_000_000.0; (remaining_mb / self.speed_mbps) as u64 } } fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { let progress = (self.downloaded as f32 / self.total_size as f32) * 100.0; v_flex() .gap_2() .child( h_flex() .justify_between() .child("Downloading...") .child(format!("{:.1}%", progress)) ) .child(Progress::new("download-progress").value(progress)) .child( h_flex() .justify_between() .child(format!("{:.1} MB/s", self.speed_mbps)) .child(format!("ETA: {}s", self.eta_seconds())) ) } } ``` ### Installation Progress ```rust struct InstallationProgress { current_package: String, package_index: usize, total_packages: usize, package_progress: f32, } impl InstallationProgress { fn overall_progress(&self) -> f32 { if self.total_packages == 0 { 0.0 } else { let completed_packages = self.package_index as f32; let current_package_contribution = self.package_progress / 100.0; let total_progress = (completed_packages + current_package_contribution) / self.total_packages as f32; total_progress * 100.0 } } fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child("Installing packages...") .child( v_flex() .gap_2() .child( h_flex() .justify_between() .child(format!("Overall Progress")) .child(format!("{}/{}", self.package_index + 1, self.total_packages)) ) .child(Progress::new("overall-progress").value(self.overall_progress())) ) .child( v_flex() .gap_2() .child(format!("Installing: {}", self.current_package)) .child(Progress::new("package-progress").value(self.package_progress)) ) } } ``` ## Styling and Theming The Progress component automatically adapts to the current theme: ### Theme Colors ```rust // The progress bar uses theme colors automatically // Background: theme.progress_bar with 20% opacity // Fill: theme.progress_bar at full opacity // These colors adapt to light/dark theme automatically Progress::new("themed-progress").value(75.0) // Uses theme colors ``` ================================================ FILE: docs/docs/components/radio.md ================================================ --- title: Radio description: A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time. --- # Radio Radio buttons allow users to select a single option from a set of mutually exclusive choices. Use radio buttons when you want to give users a choice between multiple options and only one selection is allowed. ## Import ```rust use gpui_component::radio::{Radio, RadioGroup}; ``` ## Usage ### Basic Radio Button ```rust Radio::new("radio-option-1") .label("Option 1") .checked(false) .on_click(|checked, _, _| { println!("Radio is now: {}", checked); }) ``` ### Controlled Radio Button ```rust struct MyView { radio_checked: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Radio::new("radio") .label("Select this option") .checked(self.radio_checked) .on_click(cx.listener(|view, checked, _, cx| { view.radio_checked = *checked; cx.notify(); })) } } ``` ### Radio Group (Recommended) ```rust struct MyView { selected_option: Option, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { RadioGroup::horizontal("options") .children(["Option 1", "Option 2", "Option 3"]) .selected_index(self.selected_option) .on_change(cx.listener(|view, selected_index: &usize, _, cx| { view.selected_option = Some(*selected_index); cx.notify(); })) } } ``` ### Different Sizes ```rust Radio::new("small").label("Small").xsmall() Radio::new("medium").label("Medium") // default Radio::new("large").label("Large").large() ``` ### Disabled State ```rust Radio::new("disabled") .label("Disabled option") .disabled(true) .checked(false) Radio::new("disabled-checked") .label("Disabled and checked") .checked(true) .disabled(true) ``` ### Multi-line Label with Custom Content ```rust Radio::new("custom") .label("Primary option") .child( div() .text_color(cx.theme().muted_foreground) .child("This is additional descriptive text that provides more context.") ) .w(px(300.)) ``` ### Custom Tab Order ```rust Radio::new("radio") .label("Custom tab order") .tab_index(2) .tab_stop(true) ``` ## Radio Group Usage ### Horizontal Layout ```rust RadioGroup::horizontal("horizontal-group") .children(["First", "Second", "Third"]) .selected_index(Some(0)) .on_change(cx.listener(|view, index, _, cx| { println!("Selected index: {}", index); cx.notify(); })) ``` ### Vertical Layout ```rust RadioGroup::vertical("vertical-group") .child(Radio::new("option1").label("United States")) .child(Radio::new("option2").label("Canada")) .child(Radio::new("option3").label("Mexico")) .selected_index(Some(1)) .disabled(false) ``` ### Styled Radio Group ```rust RadioGroup::vertical("styled-group") .w(px(220.)) .p_2() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .child(Radio::new("option1").label("Option 1")) .child(Radio::new("option2").label("Option 2")) .child(Radio::new("option3").label("Option 3")) .selected_index(Some(0)) ``` ### Disabled Radio Group ```rust RadioGroup::vertical("disabled-group") .children(["Option A", "Option B", "Option C"]) .selected_index(Some(1)) .disabled(true) // Disables all radio buttons in the group ``` ## API Reference ### Radio | Method | Description | | ------------------ | ----------------------------------------------------------- | | `new(id)` | Create a new radio button with the given ID | | `label(text)` | Set label text | | `checked(bool)` | Set checked state | | `disabled(bool)` | Set disabled state | | `on_click(fn)` | Callback when clicked, receives `&bool` (new checked state) | | `tab_stop(bool)` | Enable/disable tab navigation (default: true) | | `tab_index(isize)` | Set tab order index (default: 0) | ### RadioGroup | Method | Description | | ------------------------------- | ------------------------------------------------------------------- | | `horizontal(id)` | Create a new horizontal radio group | | `vertical(id)` | Create a new vertical radio group | | `layout(Axis)` | Set layout direction (Vertical or Horizontal) | | `child(Radio)` | Add a single radio button to the group | | `children(items)` | Add multiple radio buttons from an iterator | | `selected_index(Option)` | Set the selected option by index | | `disabled(bool)` | Disable all radio buttons in the group | | `on_change(fn)` | Callback when selection changes, receives `&usize` (selected index) | ### Styling Both Radio and RadioGroup implement `Styled` trait for custom styling: Radio also implements `Sizable` trait: - `xsmall()` - Extra small size - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size ## Examples ### Settings Panel ```rust struct SettingsView { theme: Option, // 0: Light, 1: Dark, 2: Auto language: Option, // 0: English, 1: Spanish, 2: French } impl Render for SettingsView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_6() .child( v_flex() .gap_2() .child(div().text_sm().font_semibold().child("Theme")) .child( RadioGroup::vertical("theme") .child(Radio::new("light").label("Light")) .child(Radio::new("dark").label("Dark")) .child(Radio::new("auto").label("Auto")) .selected_index(self.theme) .on_change(cx.listener(|view, index, _, cx| { view.theme = Some(*index); cx.notify(); })) ) ) .child( v_flex() .gap_2() .child(div().text_sm().font_semibold().child("Language")) .child( RadioGroup::horizontal("language") .children(["English", "Español", "Français"]) .selected_index(self.language) .on_change(cx.listener(|view, index, _, cx| { view.language = Some(*index); cx.notify(); })) ) ) } } ``` ### Survey Form ```rust struct SurveyView { satisfaction: Option, recommendation: Option, } impl Render for SurveyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_8() .child( v_flex() .gap_3() .child( div() .text_base() .font_medium() .child("How satisfied are you with our service?") ) .child( RadioGroup::vertical("satisfaction") .child(Radio::new("very-satisfied").label("Very satisfied")) .child(Radio::new("satisfied").label("Satisfied")) .child(Radio::new("neutral").label("Neutral")) .child(Radio::new("dissatisfied").label("Dissatisfied")) .child(Radio::new("very-dissatisfied").label("Very dissatisfied")) .selected_index(self.satisfaction) .on_change(cx.listener(|view, index, _, cx| { view.satisfaction = Some(*index); cx.notify(); })) ) ) .child( v_flex() .gap_3() .child( div() .text_base() .font_medium() .child("How likely are you to recommend us?") ) .child( RadioGroup::horizontal("recommendation") .children((0..=10).map(|i| i.to_string())) .selected_index(self.recommendation) .on_change(cx.listener(|view, index, _, cx| { view.recommendation = Some(*index); cx.notify(); })) ) ) } } ``` ### Payment Method Selection ```rust struct PaymentView { payment_method: Option, } impl Render for PaymentView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( div() .text_lg() .font_semibold() .child("Select Payment Method") ) .child( RadioGroup::vertical("payment") .child( Radio::new("credit-card") .label("Credit Card") .child( div() .text_color(cx.theme().muted_foreground) .child("Visa, MasterCard, American Express") ) ) .child( Radio::new("paypal") .label("PayPal") .child( div() .text_color(cx.theme().muted_foreground) .child("Pay with your PayPal account") ) ) .child( Radio::new("bank-transfer") .label("Bank Transfer") .child( div() .text_color(cx.theme().muted_foreground) .child("Direct bank account transfer") ) ) .selected_index(self.payment_method) .on_change(cx.listener(|view, index, _, cx| { view.payment_method = Some(*index); cx.notify(); })) ) } } ``` ## Best Practices 1. **Use RadioGroup**: Always prefer `RadioGroup` over individual `Radio` components for mutually exclusive choices 2. **Clear Labels**: Provide descriptive labels that clearly indicate what each option represents 3. **Default Selection**: Consider providing a sensible default selection, especially for required fields 4. **Logical Order**: Arrange options in a logical order (alphabetical, frequency of use, or importance) 5. **Limit Options**: Keep the number of radio options reasonable (typically 2-7 options) 6. **Group Related Options**: Use visual grouping and clear headings for multiple radio groups 7. **Responsive Design**: Consider using horizontal layout for fewer options and vertical for more options ================================================ FILE: docs/docs/components/rating.md ================================================ --- title: Rating description: A simple interactive star rating component. --- # Rating A star rating component that allows users to select a rating value. Supports different sizes, custom colors, disabled state, and click handlers. ## Import ```rust use gpui_component::rating::Rating; ``` ## Usage ### Basic Rating ```rust Rating::new("my-rating") .value(3) .max(5) .on_click(|value, _, _| { println!("Rating changed to: {}", value); }) ``` ### Controlled Rating ```rust struct MyView { rating: usize, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Rating::new("rating") .value(self.rating) .max(5) .on_click(cx.listener(|view, value: &usize, _, cx| { view.rating = *value; cx.notify(); })) } } ``` ### Different Sizes The Rating component supports the [Sizable] trait for different sizes. ```rust Rating::new("rating").xsmall().value(3).max(5) Rating::new("rating").small().value(3).max(5) Rating::new("rating").value(3).max(5) // default (Medium) Rating::new("rating").large().value(3).max(5) ``` ### Custom Color By default, the rating uses the theme's `yellow` color. You can customize it with the `color` method. ```rust Rating::new("rating") .value(4) .max(5) .color(cx.theme().green) ``` ### Disabled State ```rust Rating::new("rating") .value(2) .max(5) .disabled(true) ``` ### Custom Maximum The default maximum is 5 stars, but you can set a different maximum value. ```rust Rating::new("rating") .value(7) .max(10) ``` ### Click Behavior The rating component has special click behavior: - Clicking on a star that's already filled will reduce the rating by 1 - Clicking on an unfilled star will set the rating to that star's value The `on_click` callback receives the new rating value as `&usize`. ```rust Rating::new("rating") .value(3) .max(5) .on_click(|new_value, _, _| { println!("New rating: {}", new_value); }) ``` ## API Reference - [Rating] ### Methods - `new(id: impl Into)` - Create a new Rating component - `with_size(size: impl Into)` - Set the star size (implements [Sizable]) - `value(value: usize)` - Set the initial rating value (0..=max) - `max(max: usize)` - Set the maximum number of stars (default: 5) - `color(color: impl Into)` - Set the active color (default: theme yellow) - `disabled(disabled: bool)` - Disable interaction (implements [Disableable]) - `on_click(handler: Fn(&usize, &mut Window, &mut App))` - Set click handler ## Examples ### Read-only Display ```rust Rating::new("rating") .value(4) .max(5) .disabled(true) ``` ### Interactive Rating with State ```rust struct ProductView { user_rating: usize, } impl Render for ProductView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( Rating::new("product-rating") .value(self.user_rating) .max(5) .on_click(cx.listener(|view, value: &usize, _, cx| { view.user_rating = *value; // Save rating to backend, etc. cx.notify(); })) ) .child(format!("Your rating: {}/5", self.user_rating)) } } ``` ### Large Rating with Custom Color ```rust Rating::new("rating") .large() .value(5) .max(5) .color(cx.theme().orange) ``` [Rating]: https://docs.rs/gpui-component/latest/gpui_component/rating/struct.Rating.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html [Disableable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Disableable.html ================================================ FILE: docs/docs/components/resizable.md ================================================ --- title: Resizable description: A flexible panel layout system with draggable resize handles and adjustable panels. --- # Resizable The resizable component system provides a flexible way to create layouts with resizable panels. It supports both horizontal and vertical resizing, nested layouts, size constraints, and drag handles. Perfect for creating paned interfaces, split views, and adjustable dashboards. ## Import ```rust use gpui_component::resizable::{ h_resizable, v_resizable, resizable_panel, ResizablePanelGroup, ResizablePanel, ResizableState, ResizablePanelEvent }; ``` ## Usage Use `h_resizable` to create a horizontal layout, `v_resizable` to create a vertical layout. The first argument is the `id` for this [ResizablePanelGroup]. :::tip In GPUI, the `id` must be unique within the layout scope (The nearest parent has presents `id`). ::: ```rust h_resizable("my-layout") .on_resize(|state, window, cx| { // Handle resize event // You can read the panel sizes from the state. let state = state.read(cx); let sizes = state.sizes(); }) .child( // Use resizable_panel() to create a sized panel. resizable_panel() .size(px(200.)) .child("Left Panel") ) .child( // Or you can just add AnyElement without a size. div() .child("Right Panel") .into_any_element() ) ``` The `v_resizable` component is used to create a vertical layout. ```rust v_resizable("vertical-layout") .child( resizable_panel() .size(px(100.)) .child("Top Panel") ) .child( div() .child("Bottom Panel") .into_any_element() ) ``` ### Panel Size Constraints ```rust resizable_panel() .size(px(200.)) // Initial size .size_range(px(150.)..px(400.)) // Min and max size .child("Constrained Panel") ``` ### Multiple Panels ```rust h_resizable("multi-panel", state) .child( resizable_panel() .size(px(200.)) .size_range(px(150.)..px(300.)) .child("Left Panel") ) .child( resizable_panel() .child("Center Panel") ) .child( resizable_panel() .size(px(250.)) .child("Right Panel") ) ``` ### Nested Layouts ```rust v_resizable("main-layout", window, cx) .child( resizable_panel() .size(px(300.)) .child( h_resizable("nested-layout", window, cx) .child( resizable_panel() .size(px(200.)) .child("Top Left") ) .child( resizable_panel() .child("Top Right") ) ) ) .child( resizable_panel() .child("Bottom Panel") ) ``` ### Nested Panel Groups ```rust h_resizable("outer", window, cx) .child( resizable_panel() .size(px(200.)) .child("Left Panel") ) .group( v_resizable("inner", window, cx) .child( resizable_panel() .size(px(150.)) .child("Top Right") ) .child( resizable_panel() .child("Bottom Right") ) ) ``` ### Conditional Panel Visibility ```rust resizable_panel() .visible(self.show_sidebar) .size(px(250.)) .child("Sidebar Content") ``` ### Panel with Size Limits ```rust // Panel with minimum size only resizable_panel() .size_range(px(100.)..Pixels::MAX) .child("Flexible Panel") // Panel with both min and max resizable_panel() .size_range(px(200.)..px(500.)) .child("Constrained Panel") // Panel with exact constraints resizable_panel() .size(px(300.)) .size_range(px(300.)..px(300.)) // Fixed size .child("Fixed Panel") ``` ## Examples ### File Explorer Layout ```rust struct FileExplorer { show_sidebar: bool, } impl Render for FileExplorer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { h_resizable("file-explorer", window, cx) .child( resizable_panel() .visible(self.show_sidebar) .size(px(250.)) .size_range(px(200.)..px(400.)) .child( v_flex() .p_4() .child("📁 Folders") .child("• Documents") .child("• Pictures") .child("• Downloads") ) ) .child( v_flex() .p_4() .child("📄 Files") .child("file1.txt") .child("file2.pdf") .child("image.png") .into_any_element() ) } } ``` ### IDE Layout ```rust struct IDELayout { main_state: Entity, sidebar_state: Entity, bottom_state: Entity, } impl Render for IDELayout { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { h_resizable("ide-main", self.main_state.clone()) .child( resizable_panel() .size(px(300.)) .size_range(px(200.)..px(500.)) .child( v_resizable("sidebar", self.sidebar_state.clone()) .child( resizable_panel() .size(px(200.)) .child("File Explorer") ) .child( resizable_panel() .child("Outline") ) ) ) .child( resizable_panel() .child( v_resizable("editor-area", self.bottom_state.clone()) .child( resizable_panel() .child("Code Editor") ) .child( resizable_panel() .size(px(150.)) .size_range(px(100.)..px(300.)) .child("Terminal / Output") ) ) ) } } ``` ### Dashboard with Widgets ```rust struct Dashboard { layout_state: Entity, widget_state: Entity, } impl Render for Dashboard { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_resizable("dashboard", self.layout_state.clone()) .child( resizable_panel() .size(px(120.)) .child("Header / Navigation") ) .child( resizable_panel() .child( h_resizable("widgets", self.widget_state.clone()) .child( resizable_panel() .size(px(300.)) .child("Chart Widget") ) .child( resizable_panel() .child("Data Table") ) .child( resizable_panel() .size(px(250.)) .child("Stats Panel") ) ) ) .child( resizable_panel() .size(px(60.)) .child("Footer") ) } } ``` ### Settings Panel ```rust struct SettingsPanel { settings_state: Entity, } impl SettingsPanel { fn new(cx: &mut Context) -> Self { let settings_state = ResizableState::new(cx); // Listen for resize events to save layout preferences cx.subscribe(&settings_state, |this, _, event: &ResizablePanelEvent, cx| { match event { ResizablePanelEvent::Resized => { this.save_layout_preferences(cx); } } }); Self { settings_state } } fn save_layout_preferences(&self, cx: &mut Context) { let sizes = self.settings_state.read(cx).sizes(); // Save to preferences println!("Saving layout: {:?}", sizes); } } impl Render for SettingsPanel { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { h_resizable("settings", self.settings_state.clone()) .child( resizable_panel() .size(px(200.)) .size_range(px(150.)..px(300.)) .child( v_flex() .gap_2() .p_4() .child("Categories") .child("• General") .child("• Appearance") .child("• Advanced") ) ) .child( resizable_panel() .child( div() .p_6() .child("Settings Content Area") ) ) } } ``` ## Best Practices 1. **State Management**: Use separate ResizableState for independent layouts 2. **Size Constraints**: Always set reasonable min/max sizes for panels 3. **Event Handling**: Subscribe to ResizablePanelEvent for layout persistence 4. **Nested Layouts**: Use `.group()` method for clean nested structures 5. **Performance**: Avoid excessive nesting for better performance 6. **User Experience**: Provide adequate handle padding for easier interaction ================================================ FILE: docs/docs/components/scrollable.md ================================================ --- title: Scrollable description: Scrollable container with custom scrollbars, scroll tracking, and virtualization support. --- # Scrollable A comprehensive scrollable container component that provides custom scrollbars, scroll tracking, and virtualization capabilities. Supports both vertical and horizontal scrolling with customizable appearance and behavior. ## Import ```rust use gpui_component::{ scroll::{ScrollableElement, ScrollbarAxis, ScrollbarShow}, StyledExt as _, }; ``` ## Usage ### Basic Scrollable Container The simplest way to make any element scrollable is using the `overflow_scrollbar()` method from `ScrollableElement` trait. This method is almost like the `overflow_scroll()` method, but it adds scrollbars. - `overflow_scrollbar()` - Adds scrollbars for both axes as needed. - `overflow_x_scrollbar()` - Adds horizontal scrollbar as needed. - `overflow_y_scrollbar()` - Adds vertical scrollbar as needed. ```rust use gpui::{div, Axis}; use gpui_component::ScrollableElement; div() .id("scrollable-container") .size_full() .child("Your content here") .overflow_scrollbar() ``` ### Vertical Scrolling ```rust v_flex() .id("scrollable-container") .overflow_y_scrollbar() .gap_2() .p_4() .child("Scrollable Content") .children((0..100).map(|i| { div() .h(px(40.)) .w_full() .bg(cx.theme().secondary) .child(format!("Item {}", i)) })) ``` ### Horizontal Scrolling ```rust h_flex() .id("scrollable-container") .overflow_x_scrollbar() .gap_2() .p_4() .children((0..50).map(|i| { div() .min_w(px(120.)) .h(px(80.)) .bg(cx.theme().accent) .child(format!("Card {}", i)) })) ``` ### Both Directions ```rust div() .id("scrollable-container") .size_full() .overflow_scrollbar() .child( div() .w(px(2000.)) // Wide content .h(px(2000.)) // Tall content .bg(cx.theme().background) .child("Large content area") ) ``` ## Custom Scrollbars ### Manual Scrollbar Creation For more control, you can create scrollbars manually: ```rust use gpui_component::scroll::{ScrollableElement}; pub struct ScrollableView { scroll_handle: ScrollHandle, } impl Render for ScrollableView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .relative() .size_full() .child( div() .id("content") .track_scroll(&self.scroll_handle) .overflow_scroll() .size_full() .child("Your scrollable content") ) .vertical_scrollbar(&self.scroll_handle) } } ``` ## Virtualization ### VirtualList for Large Datasets For rendering large lists efficiently, use `VirtualList`: ```rust use gpui_component::{VirtualList, VirtualListScrollHandle}; pub struct LargeListView { items: Vec, scroll_handle: VirtualListScrollHandle, } impl Render for LargeListView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let item_count = self.items.len(); VirtualList::new( self.scroll_handle.clone(), item_count, |ix, window, cx| { // Item sizes - can be different for each item size(px(300.), px(40.)) }, |ix, bounds, selected, window, cx| { // Render each item div() .size(bounds.size) .bg(if selected { cx.theme().accent } else { cx.theme().background }) .child(format!("Item {}: {}", ix, self.items[ix])) .into_any_element() }, ) } } ``` ### Scrolling to Specific Items ```rust impl LargeListView { fn scroll_to_item(&mut self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Top); } fn scroll_to_item_centered(&mut self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Center); } } ``` ### Variable Item Sizes ```rust VirtualList::new( scroll_handle, items.len(), |ix, window, cx| { // Different heights based on content let height = if items[ix].len() > 50 { px(80.) // Tall items for long content } else { px(40.) // Normal height }; size(px(300.), height) }, |ix, bounds, selected, window, cx| { // Render logic }, ) ``` ## Theme Customization ### Scrollbar Appearance Customize scrollbar appearance through theme configuration: ```rust // In your theme JSON { "scrollbar.background": "#ffffff20", "scrollbar.thumb.background": "#00000060", "scrollbar.thumb.hover.background": "#000000" } ``` ### Scrollbar Show Modes Control when scrollbars are visible: ```rust use gpui_component::scroll::ScrollbarShow; // In theme initialization theme.scrollbar_show = ScrollbarShow::Scrolling; // Show only when scrolling theme.scrollbar_show = ScrollbarShow::Hover; // Show on hover theme.scrollbar_show = ScrollbarShow::Always; // Always visible ``` ### System Integration Sync scrollbar behavior with system preferences: ```rust // Automatically sync with system settings Theme::sync_scrollbar_appearance(cx); ``` ## Examples ### File Browser with Scrolling ```rust pub struct FileBrowser { files: Vec, } impl Render for FileBrowser { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .border_1() .border_color(cx.theme().border) .size_full() .child( v_flex() .gap_1() .p_2() .overflow_y_scrollbar() .children(self.files.iter().map(|file| { div() .h(px(32.)) .w_full() .px_2() .flex() .items_center() .hover(|style| style.bg(cx.theme().secondary_hover)) .child(file.clone()) })) ) } } ``` ### Chat Messages with Auto-scroll ```rust pub struct ChatView { messages: Vec, scroll_handle: ScrollHandle, should_auto_scroll: bool, } impl ChatView { fn add_message(&mut self, message: String) { self.messages.push(message); if self.should_auto_scroll { // Scroll to bottom for new messages let max_offset = self.scroll_handle.max_offset(); self.scroll_handle.set_offset(point(px(0.), max_offset.y)); } } } ``` ### Data Table with Virtual Scrolling ```rust pub struct DataTable { data: Vec>, scroll_handle: VirtualListScrollHandle, } impl Render for DataTable { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { VirtualList::new( self.scroll_handle.clone(), self.data.len(), |_ix, _window, _cx| size(px(800.), px(32.)), // Fixed row height |ix, bounds, _selected, _window, cx| { h_flex() .size(bounds.size) .border_b_1() .border_color(cx.theme().border) .children(self.data[ix].iter().map(|cell| { div() .flex_1() .px_2() .flex() .items_center() .child(cell.clone()) })) .into_any_element() }, ) } } ``` ================================================ FILE: docs/docs/components/select.md ================================================ --- title: Select description: Displays a list of options for the user to pick from—triggered by a button. --- # Select :::info This component was named `Dropdown` in `<= 0.3.x`. It has been renamed to `Select` to better reflect its purpose. ::: A select component that allows users to choose from a list of options. Supports search functionality, grouped items, custom rendering, and various states. Built with keyboard navigation and accessibility in mind. ## Import ```rust use gpui_component::select::{ Select, SelectState, SelectItem, SelectDelegate, SelectEvent, SearchableVec, SelectGroup }; ``` ## Usage ### Basic Select You can create a basic select dropdown by initializing a `SelectState` with a list of items. The first type parameter of `SelectState` is the items for the state, which must implement the [SelectItem] trait. The built-in implementations of `SelectItem` include common types like `String`, `SharedString`, and `&'static str`. ```rust let state = cx.new(|cx| { SelectState::new( vec!["Apple", "Orange", "Banana"], Some(IndexPath::default()), // Select first item window, cx, ) }); Select::new(&state) ``` ### Placeholder ```rust let state = cx.new(|cx| { SelectState::new( vec!["Rust", "Go", "JavaScript"], None, // No initial selection window, cx, ) }); Select::new(&state) .placeholder("Select a language...") ``` ### Searchable Use `searchable(true)` to enable search functionality within the dropdown. ```rust let fruits = SearchableVec::new(vec![ "Apple", "Orange", "Banana", "Grape", "Pineapple", ]); let state = cx.new(|cx| { SelectState::new(fruits, None, window, cx).searchable(true) }); Select::new(&state) .icon(IconName::Search) // Shows search icon ``` ### Impl SelectItem By default, we have implmemented `SelectItem` for common types like `String`, `SharedString` and `&'static str`. You can also create your own item types by implementing the `SelectItem` trait. This is useful when you want to display complex data structures, and also want get that data type from `select_value` method. You can also customize the search logic by overriding the `matches` method. ```rust #[derive(Debug, Clone)] struct Country { name: SharedString, code: SharedString, } impl SelectItem for Country { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn display_title(&self) -> Option { // Custom display for selected item Some(format!("{} ({})", self.name, self.code).into_any_element()) } fn value(&self) -> &Self::Value { &self.code } fn matches(&self, query: &str) -> bool { // Custom search logic self.name.to_lowercase().contains(&query.to_lowercase()) || self.code.to_lowercase().contains(&query.to_lowercase()) } } ``` ### Group Items ```rust let mut grouped_items = SearchableVec::new(vec![]); // Group countries by first letter grouped_items.push( SelectGroup::new("A") .items(vec![ Country { name: "Australia".into(), code: "AU".into() }, Country { name: "Austria".into(), code: "AT".into() }, ]) ); grouped_items.push( SelectGroup::new("B") .items(vec![ Country { name: "Brazil".into(), code: "BR".into() }, Country { name: "Belgium".into(), code: "BE".into() }, ]) ); let state = cx.new(|cx| { SelectState::new(grouped_items, None, window, cx) }); Select::new(&state) ``` ### Sizes ```rust Select::new(&state).large() Select::new(&state) // medium (default) Select::new(&state).small() ``` ### Disabled State ```rust Select::new(&state).disabled(true) ``` ### Cleanable ```rust Select::new(&state) .cleanable(true) // Show clear button when item is selected ``` ### Custom Appearance ```rust Select::new(&state) .w(px(320.)) // Set dropdown width .menu_width(px(400.)) // Set menu popup width .appearance(false) // Remove default styling .title_prefix("Country: ") // Add prefix to selected title ``` ### Empty State ```rust let state = cx.new(|cx| { SelectState::new(Vec::::new(), None, window, cx) }); Select::new(&state) .empty( h_flex() .h_24() .justify_center() .text_color(cx.theme().muted_foreground) .child("No options available") ) ``` ### Events ```rust cx.subscribe_in(&state, window, |view, state, event, window, cx| { match event { SelectEvent::Confirm(value) => { if let Some(selected_value) = value { println!("Selected: {:?}", selected_value); } else { println!("Selection cleared"); } } } }); ``` ### Mutating ```rust // Set by index state.update(cx, |state, cx| { state.set_selected_index(Some(IndexPath::default().row(2)), window, cx); }); // Set by value (requires PartialEq on Value type) state.update(cx, |state, cx| { state.set_selected_value(&"US".into(), window, cx); }); // Get current selection let current_value = state.read(cx).selected_value(); ``` Update items: ```rust state.update(cx, |state, cx| { let new_items = vec!["New Option 1".into(), "New Option 2".into()]; state.set_items(new_items, window, cx); }); ``` ## Examples ### Language Selector ```rust let languages = SearchableVec::new(vec![ "Rust".into(), "TypeScript".into(), "Go".into(), "Python".into(), "JavaScript".into(), ]); let state = cx.new(|cx| { SelectState::new(languages, None, window, cx) }); Select::new(&state) .placeholder("Select language...") .title_prefix("Language: ") ``` ### Country/Region Selector ```rust #[derive(Debug, Clone)] struct Region { name: SharedString, code: SharedString, flag: SharedString, } impl SelectItem for Region { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn display_title(&self) -> Option { Some( h_flex() .items_center() .gap_2() .child(self.flag.clone()) .child(format!("{} ({})", self.name, self.code)) .into_any_element() ) } fn value(&self) -> &Self::Value { &self.code } } let regions = vec![ Region { name: "United States".into(), code: "US".into(), flag: "🇺🇸".into() }, Region { name: "Canada".into(), code: "CA".into(), flag: "🇨🇦".into() }, ]; let state = cx.new(|cx| { SelectState::new(regions, None, window, cx) }); Select::new(&state) .placeholder("Select country...") ``` ### Integrated with Input Field ```rust // Combined country code + phone input h_flex() .border_1() .border_color(cx.theme().input) .rounded(cx.theme().radius_lg) .w_full() .gap_1() .child( div().w(px(140.)).child( Select::new(&country_state) .appearance(false) // No border/background .py_2() .pl_3() ) ) .child(Divider::vertical()) .child( div().flex_1().child( Input::new(&phone_input) .appearance(false) .placeholder("Phone number") .pr_3() .py_2() ) ) ``` ### Multi-level Grouped Select ```rust let mut grouped_countries = SearchableVec::new(vec![]); for (continent, countries) in countries_by_continent { grouped_countries.push( SelectGroup::new(continent) .items(countries) ); } let state = cx.new(|cx| { SelectState::new(grouped_countries, None, window, cx) }); Select::new(&state) .menu_width(px(350.)) .placeholder("Select country...") ``` ## Keyboard Shortcuts | Key | Action | | --------- | --------------------------------------- | | `Tab` | Focus dropdown | | `Enter` | Open menu or select current item | | `Up/Down` | Navigate options (opens menu if closed) | | `Escape` | Close menu | | `Space` | Open menu | ## Theming The dropdown respects the current theme and uses the following theme tokens: - `background` - Dropdown input background - `input` - Border color - `foreground` - Text color - `muted_foreground` - Placeholder and disabled text - `accent` - Selected item background - `accent_foreground` - Placeholder text color - `border` - Menu border - `radius` - Border radius [SelectItem]: https://docs.rs/gpui-component/latest/gpui_component/select/trait.SelectItem.html ================================================ FILE: docs/docs/components/settings.md ================================================ --- title: Settings description: A settings UI with grouped setting items and pages. --- # Settings > Since: v0.5.0 The Settings component provides a UI for managing application settings. It includes grouped setting items and pages. We can search by title and description to filter the settings to display only relevant settings (Like this macOS, iOS Settings). ## Import ```rust use gpui_component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField}; ``` ## Usage ### Build a settings Here we have components that can be used to build a settings page. - [Settings] - The main settings component that holds multiple setting pages. - [SettingPage] - A page of related setting groups. - [SettingGroup] - A group of related setting items based on [GroupBox] style. - [SettingItem] - A single setting item with title, description, and field. - [SettingField] - Provide different field types like Input, Dropdown, Switch, etc. The layout of the settings is like this: ``` Settings SettingPage SettingGroup SettingItem Title Description (optional) SettingField ``` ### Basic Settings ```rust use gpui_component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField}; Settings::new("my-settings") .pages(vec![ SettingPage::new("General") .group( SettingGroup::new() .title("Basic Options") .item( SettingItem::new( "Enable Feature", SettingField::switch( |cx: &App| true, |val: bool, cx: &mut App| { println!("Feature enabled: {}", val); }, ) ) ) ) ]) ``` ### With Multiple Pages :::info When you want default expland a page, you can use `default_open(true)` on the [SettingPage]. ::: ```rust Settings::new("app-settings") .pages(vec![ SettingPage::new("General") .default_open(true) .group(SettingGroup::new().title("Appearance").items(vec![...])), SettingPage::new("Software Update") .group(SettingGroup::new().title("Updates").items(vec![...])), SettingPage::new("About") .group(SettingGroup::new().items(vec![...])), ]) ``` ### Group Variants ```rust use gpui_component::group_box::GroupBoxVariant; Settings::new("my-settings") .with_group_variant(GroupBoxVariant::Outline) .pages(vec![...]) Settings::new("my-settings") .with_group_variant(GroupBoxVariant::Fill) .pages(vec![...]) ``` ## Setting Page ### Basic Page ```rust SettingPage::new("General") .group(SettingGroup::new().title("Options").items(vec![...])) ``` ### Multiple Groups ```rust SettingPage::new("General") .groups(vec![ SettingGroup::new().title("Appearance").items(vec![...]), SettingGroup::new().title("Font").items(vec![...]), SettingGroup::new().title("Other").items(vec![...]), ]) ``` ### Icon ```rust SettingPage::new("General") .icon(IconName::Settings) .groups(vec![...]) ``` ### Default Open ```rust SettingPage::new("General") .default_open(true) .groups(vec![...]) ``` ### resettable Enable reset functionality for a page: ```rust SettingPage::new("General") .resettable(true) .groups(vec![...]) ``` ## Setting Group ### Basic Group ```rust SettingGroup::new() .title("Appearance") .items(vec![ SettingItem::new(...), SettingItem::new(...), ]) ``` ### Single Item ```rust SettingGroup::new() .title("Font") .item(SettingItem::new(...)) ``` ### Without Title ```rust SettingGroup::new() .items(vec![...]) ``` ## Setting Item ### Basic Item ```rust SettingItem::new("Title", SettingField::switch(...)) .description("Description text") ``` ### Custom Item with a render closure You can create a fully custom setting item using `SettingItem::render`: ```rust SettingItem::render(|options, _, _| { h_flex() .w_full() .justify_between() .child("Custom content") .child( Button::new("action") .label("Action") .with_size(options.size) ) .into_any_element() }) ``` ### Vertical Layout By default, setting items use horizontal layout. Use `layout(Axis::Vertical)` for vertical layout: ```rust SettingItem::new( "CLI Path", SettingField::input(...) ) .layout(Axis::Vertical) .description("This item uses vertical layout.") ``` ### With Markdown Description ```rust use gpui_component::text::markdown; SettingItem::new( "Documentation", SettingField::element(...) ) .description(markdown("Rust doc for the `gpui-component` crate.")) ``` ## Setting Fields The [SettingField] enum provides different field types for various input needs. ### Switch The switch field represents a `boolean` on/off state. ```rust SettingItem::new( "Dark Mode", SettingField::switch( |cx: &App| cx.theme().mode.is_dark(), |val: bool, cx: &mut App| { // Handle value change }, ) .default_value(false) ) ``` ### Checkbox Like the switch, but uses a checkbox UI. ```rust SettingItem::new( "Auto Switch Theme", SettingField::checkbox( |cx: &App| AppSettings::global(cx).auto_switch_theme, |val: bool, cx: &mut App| { AppSettings::global_mut(cx).auto_switch_theme = val; }, ) .default_value(false) ) ``` ### Input Display a single line text input. ```rust SettingItem::new( "CLI Path", SettingField::input( |cx: &App| AppSettings::global(cx).cli_path.clone(), |val: SharedString, cx: &mut App| { AppSettings::global_mut(cx).cli_path = val; }, ) .default_value("/usr/local/bin/bash".into()) ) .layout(Axis::Vertical) .description("Path to the CLI executable.") ``` ### Dropdown A dropdown with a list of options. ```rust SettingItem::new( "Font Family", SettingField::dropdown( vec![ ("Arial".into(), "Arial".into()), ("Helvetica".into(), "Helvetica".into()), ("Times New Roman".into(), "Times New Roman".into()), ], |cx: &App| AppSettings::global(cx).font_family.clone(), |val: SharedString, cx: &mut App| { AppSettings::global_mut(cx).font_family = val; }, ) .default_value("Arial".into()) ) ``` ### NumberInput ```rust use gpui_component::setting::NumberFieldOptions; SettingItem::new( "Font Size", SettingField::number_input( NumberFieldOptions { min: 8.0, max: 72.0, ..Default::default() }, |cx: &App| AppSettings::global(cx).font_size, |val: f64, cx: &mut App| { AppSettings::global_mut(cx).font_size = val; }, ) .default_value(14.0) ) ``` ### Custom Field by Render Closure The `SettingField::render` method allows you to create a custom field using a closure that returns an element. ```rust SettingItem::new( "GitHub Repository", SettingField::render(|options, _window, _cx| { Button::new("open-url") .outline() .label("Repository...") .with_size(options.size) .on_click(|_, _window, cx| { cx.open_url("https://github.com/example/repo"); }) }) ) ``` ### Custom Field Element You may have a complex field that you want to reuse, you may want split the element into a separate struct to do the complex logic. In this case, the [SettingFieldElement] trait can help you to create a custom field element. ```rust use gpui_component::setting::{SettingFieldElement, RenderOptions}; struct OpenURLSettingField { label: SharedString, url: SharedString, } impl SettingFieldElement for OpenURLSettingField { type Element = Button; fn render_field(&self, options: &RenderOptions, _: &mut Window, _: &mut App) -> Self::Element { let url = self.url.clone(); Button::new("open-url") .outline() .label(self.label.clone()) .with_size(options.size) .on_click(move |_, _window, cx| { cx.open_url(url.as_str()); }) } } ``` Then use it in the setting item: ```rust SettingItem::new( "GitHub Repository", SettingField::element(OpenURLSettingField { label: "Repository...".into(), url: "https://github.com/longbridge/gpui-component".into(), }) ) ``` ## API Reference - [Settings] - [SettingPage] - [SettingGroup] - [SettingItem] - [SettingField] - [NumberFieldOptions] ### Sizing Implements [Sizable] trait: - `xsmall()` - Extra small size - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size - `with_size(Size)` - Set specific size ## Examples ### Complete Settings Example ```rust use gpui::{App, SharedString}; use gpui_component::{ Settings, SettingPage, SettingGroup, SettingItem, SettingField, setting::NumberFieldOptions, group_box::GroupBoxVariant, Size, }; Settings::new("app-settings") .with_size(Size::Medium) .with_group_variant(GroupBoxVariant::Outline) .pages(vec![ SettingPage::new("General") .resettable(true) .default_open(true) .groups(vec![ SettingGroup::new() .title("Appearance") .items(vec![ SettingItem::new( "Dark Mode", SettingField::switch( |cx: &App| cx.theme().mode.is_dark(), |val: bool, cx: &mut App| { // Handle theme change }, ) ) .description("Switch between light and dark themes."), ]), SettingGroup::new() .title("Font") .items(vec![ SettingItem::new( "Font Family", SettingField::dropdown( vec![ ("Arial".into(), "Arial".into()), ("Helvetica".into(), "Helvetica".into()), ], |cx: &App| "Arial".into(), |val: SharedString, cx: &mut App| { // Handle font change }, ) ), SettingItem::new( "Font Size", SettingField::number_input( NumberFieldOptions { min: 8.0, max: 72.0, ..Default::default() }, |cx: &App| 14.0, |val: f64, cx: &mut App| { // Handle size change }, ) ), ]), ]), SettingPage::new("Software Update") .resettable(true) .group( SettingGroup::new() .title("Updates") .items(vec![ SettingItem::new( "Auto Update", SettingField::switch( |cx: &App| true, |val: bool, cx: &mut App| { // Handle auto update }, ) ) .description("Automatically download and install updates."), ]) ), ]) ``` [Settings]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.Settings.html [SettingPage]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingPage.html [SettingGroup]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingGroup.html [SettingItem]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingItem.html [SettingField]: https://docs.rs/gpui-component/latest/gpui_component/setting/enum.SettingField.html [SettingFieldElement]: https://docs.rs/gpui-component/latest/gpui_component/setting/trait.SettingFieldElement.html [NumberFieldOptions]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.NumberFieldOptions.html [GroupBox]: ./group-box.md [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ================================================ FILE: docs/docs/components/sheet.md ================================================ --- title: Sheet description: A sliding panel that appears from the edges of the screen for displaying content. --- # Sheet A Sheet (also known as a sidebar or slide-out panel) is a navigation component that slides in from the edges of the screen. It provides additional space for content without taking up the main view, and can be used for navigation menus, forms, or any supplementary content. ## Import ```rust use gpui_component::WindowExt; use gpui_component::Placement; ``` ## Usage ### Setup application root view for display of sheets You need to set up your application's root view to render the sheet layer. This is typically done in your main application struct's render method. The [Root::render_sheet_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_sheet_layer) function handles rendering any active modals on top of your app content. ```rust use gpui_component::TitleBar; struct MyApp { view: AnyView, } impl Render for MyApp { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let sheet_layer = Root::render_sheet_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(TitleBar::new()) .child(div().flex_1().overflow_hidden().child(self.view.clone())), ) // Render the sheet layer on top of the app content .children(sheet_layer) } } ``` ### Basic Sheet ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Navigation") .child("Sheet content goes here") }) ``` ### Sheet with Placement ```rust // Left sheet (default) window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet.title("Left Sheet") }) // Right sheet window.open_sheet_at(Placement::Right, cx, |sheet, _, _| { sheet.title("Right Sheet") }) // Top sheet window.open_sheet_at(Placement::Top, cx, |sheet, _, _| { sheet.title("Top Sheet") }) // Bottom sheet window.open_sheet_at(Placement::Bottom, cx, |sheet, _, _| { sheet.title("Bottom Sheet") }) ``` ### Sheet with Custom Size ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Wide Sheet") .size(px(500.)) // Custom width for left/right, height for top/bottom .child("This sheet is 500px wide") }) ``` ### Sheet with Form Content ```rust let input = cx.new(|cx| InputState::new(window, cx)); let date = cx.new(|cx| DatePickerState::new(window, cx)); window.open_sheet(cx, |sheet, _, _| { sheet .title("User Profile") .child( v_flex() .gap_4() .child("Enter your information:") .child(Input::new(&input).placeholder("Full Name")) .child(DatePicker::new(&date).placeholder("Date of Birth")) ) .footer( h_flex() .gap_3() .child(Button::new("save").primary().label("Save")) .child(Button::new("cancel").label("Cancel")) ) }) ``` ### Overlay Options ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Settings") .overlay(true) // Show overlay background (default: true) .overlay_closable(true) // Click overlay to close (default: true) .child("Sheet settings content") }) // No overlay window.open_sheet(cx, |sheet, _, _| { sheet .title("Side Panel") .overlay(false) // No overlay background .child("This sheet has no overlay") }) ``` ### Resizable Sheet ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Resizable Panel") .resizable(true) // Allow user to resize (default: true) .size(px(300.)) .child("You can resize this sheet by dragging the edge") }) ``` ### Custom Margin and Positioning ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Below Title Bar") .margin_top(px(32.)) // Space for window title bar .child("This sheet appears below the title bar") }) ``` ### Sheet with List ```rust let delegate = ListDelegate::new(items); let list = cx.new(|cx| List::new(delegate, window, cx)); window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("File Explorer") .size(px(400.)) .child( div() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .size_full() .child(list.clone()) ) }) ``` ### Close Event Handling ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Sheet with Handler") .child("This sheet has a custom close handler") .on_close(|_, window, cx| { window.push_notification("Sheet was closed", cx); }) }) ``` ### Navigation Sheet ```rust window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("Navigation") .size(px(280.)) .child( v_flex() .gap_2() .child(Button::new("home").ghost().label("Home").w_full()) .child(Button::new("profile").ghost().label("Profile").w_full()) .child(Button::new("settings").ghost().label("Settings").w_full()) .child(Button::new("logout").ghost().label("Logout").w_full()) ) }) ``` ### Custom Styling ```rust window.open_sheet(cx, |sheet, _, cx| { sheet .title("Styled Sheet") .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .border_color(cx.theme().primary) .child("Custom styled sheet content") }) ``` ### Programmatic Close ```rust // Close sheet from inside Button::new("close") .label("Close Sheet") .on_click(|_, window, cx| { window.close_sheet(cx); }) // Close sheet from outside window.close_sheet(cx); ``` ## API Reference ### Window Extensions | Method | Description | | ---------------------------------- | ----------------------------------------- | | `open_sheet(cx, fn)` | Open sheet with default placement (Right) | | `open_sheet_at(placement, cx, fn)` | Open sheet at specific placement | | `close_sheet(cx)` | Close current sheet | ### Sheet Builder | Method | Description | | ------------------------ | --------------------------------------- | | `title(str)` | Set sheet title | | `child(el)` | Add content to sheet body | | `footer(el)` | Set footer content | | `size(px)` | Set sheet size (width or height) | | `margin_top(px)` | Set top margin (for title bars) | | `resizable(bool)` | Allow resizing (default: true) | | `overlay(bool)` | Show overlay background (default: true) | | `overlay_closable(bool)` | Click overlay to close (default: true) | | `on_close(fn)` | Close event callback | ### Placement Options | Value | Description | | ------------------- | ----------------------------------- | | `Placement::Left` | Slides in from left edge | | `Placement::Right` | Slides in from right edge (default) | | `Placement::Top` | Slides in from top edge | | `Placement::Bottom` | Slides in from bottom edge | ### Styling Methods | Method | Description | | --------------------- | ------------------------ | | `bg(color)` | Set background color | | `text_color(color)` | Set text color | | `border_color(color)` | Set border color | | `px_*()/py_*()` | Custom padding | | `gap_*()` | Spacing between children | ## Examples ### Settings Panel ```rust window.open_sheet_at(Placement::Right, cx, |sheet, _, _| { sheet .title("Settings") .size(px(350.)) .child( v_flex() .gap_4() .child("Appearance") .child(Checkbox::new("dark-mode").label("Dark Mode")) .child(Checkbox::new("animations").label("Enable Animations")) .child("Notifications") .child(Checkbox::new("push-notifications").label("Push Notifications")) ) .footer( h_flex() .justify_end() .gap_2() .child(Button::new("apply").primary().label("Apply")) .child(Button::new("cancel").label("Cancel")) ) }) ``` ### File Browser ```rust window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("Files") .size(px(300.)) .child( v_flex() .size_full() .child( h_flex() .gap_2() .p_2() .child(Button::new("new-folder").small().icon(IconName::FolderPlus)) .child(Button::new("upload").small().icon(IconName::Upload)) ) .child( div() .flex_1() .overflow_hidden() .child(file_tree_list) ) ) }) ``` ### Help Panel ```rust window.open_sheet_at(Placement::Bottom, cx, |sheet, _, _| { sheet .title("Help & Documentation") .size(px(200.)) .child( h_flex() .gap_4() .child("Keyboard Shortcuts") .child(Kbd::new("⌘").child("K")) .child("Search") .child(Kbd::new("⌘").child("P")) .child("Command Palette") ) }) ``` ## Best Practices 1. **Appropriate Placement**: Use left/right for navigation, top/bottom for temporary content 2. **Consistent Sizing**: Maintain consistent sheet sizes across your application 3. **Clear Headers**: Always provide descriptive titles 4. **Close Options**: Provide multiple ways to close (ESC, overlay click, close button) 5. **Content Organization**: Use proper spacing and grouping for sheet content 6. **Responsive Design**: Consider sheet behavior on smaller screens 7. **Performance**: Lazy load sheet content when possible for better performance ================================================ FILE: docs/docs/components/sidebar.md ================================================ --- title: Sidebar description: A composable, themeable and customizable sidebar component for navigation and content organization. --- # Sidebar A flexible sidebar component that provides navigation structure for applications. Features collapsible states, nested menu items, header and footer sections, and responsive design. Perfect for creating application navigation panels, admin dashboards, and complex hierarchical interfaces. ## Import ```rust use gpui_component::sidebar::{ Sidebar, SidebarHeader, SidebarFooter, SidebarGroup, SidebarMenu, SidebarMenuItem, SidebarToggleButton }; ``` ## Usage ### Basic Sidebar ```rust use gpui_component::{sidebar::*, Side}; Sidebar::new() .header( SidebarHeader::new() .child("My Application") ) .child( SidebarGroup::new("Navigation") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Dashboard") .icon(IconName::LayoutDashboard) .on_click(|_, _, _| println!("Dashboard clicked")) ) .child( SidebarMenuItem::new("Settings") .icon(IconName::Settings) .on_click(|_, _, _| println!("Settings clicked")) ) ) ) .footer( SidebarFooter::new() .child("User Profile") ) ``` ### Collapsible Sidebar ```rust let mut collapsed = false; Sidebar::new() .collapsed(collapsed) .collapsible(true) .header( SidebarHeader::new() .child( h_flex() .child(Icon::new(IconName::Home)) .when(!collapsed, |this| this.child("Home")) ) ) .child( SidebarGroup::new("Menu") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Files") .icon(IconName::Folder) ) ) ) // Toggle button SidebarToggleButton::new() .collapsed(collapsed) .on_click(|_, _, _| { collapsed = !collapsed; }) ``` ### Nested Menu Items ```rust SidebarMenuItem::new("Projects") .icon(IconName::FolderOpen) .active(true) .children([ SidebarMenuItem::new("Web App") .active(false) .on_click(|_, _, _| println!("Web App selected")), SidebarMenuItem::new("Mobile App") .active(true) .on_click(|_, _, _| println!("Mobile App selected")), SidebarMenuItem::new("Desktop App") .on_click(|_, _, _| println!("Desktop App selected")), ]) .on_click(|_, _, _| { // Toggle project group }) ``` ### Multiple Groups ```rust Sidebar::new() .child( SidebarGroup::new("Main") .child( SidebarMenu::new() .child(SidebarMenuItem::new("Dashboard").icon(IconName::Home)) .child(SidebarMenuItem::new("Analytics").icon(IconName::BarChart)) ) ) .child( SidebarGroup::new("Content") .child( SidebarMenu::new() .child(SidebarMenuItem::new("Posts").icon(IconName::FileText)) .child(SidebarMenuItem::new("Media").icon(IconName::Image)) .child(SidebarMenuItem::new("Comments").icon(IconName::MessageCircle)) ) ) .child( SidebarGroup::new("Settings") .child( SidebarMenu::new() .child(SidebarMenuItem::new("General").icon(IconName::Settings)) .child(SidebarMenuItem::new("Users").icon(IconName::Users)) ) ) ``` ### With Badges and Suffixes ```rust use gpui_component::{Badge, Switch}; SidebarMenuItem::new("Notifications") .icon(IconName::Bell) .suffix( Badge::new() .count(5) .child("5") ) SidebarMenuItem::new("Dark Mode") .icon(IconName::Moon) .suffix( Switch::new("dark-mode") .checked(true) .xsmall() ) SidebarMenuItem::new("Settings") .icon(IconName::Settings) .suffix(IconName::ChevronRight) ``` ### Right-Side Placement ```rust Sidebar::new() .side(Side::Right) .width(300) .header( SidebarHeader::new() .child("Right Panel") ) .child( SidebarGroup::new("Tools") .child( SidebarMenu::new() .child(SidebarMenuItem::new("Inspector").icon(IconName::Search)) .child(SidebarMenuItem::new("Console").icon(IconName::Terminal)) ) ) ``` ### Context Menus Add right-click context menus to sidebar menu items for additional actions: ```rust use gpui_component::menu::PopupMenu; SidebarMenuItem::new("Project Files") .icon(IconName::Folder) .context_menu(|menu, _, _| { menu.link("Open in Editor", "https://editor.example.com") .separator() .menu_with_description("Rename", "Rename this project", Box::new(RenameAction)) .menu_with_description("Delete", "Delete this project", Box::new(DeleteAction)) .separator() .submenu("Share", |submenu| { submenu.menu("Copy Link", Box::new(CopyLinkAction)) .menu("Send via Email", Box::new(EmailAction)) }) }) // Multiple items with context menus SidebarMenu::new() .child( SidebarMenuItem::new("Documentation") .icon(IconName::BookOpen) .context_menu(|menu, _, _| { menu.menu("View Online", Box::new(ViewOnlineAction)) .menu("Download PDF", Box::new(DownloadPdfAction)) }) ) .child( SidebarMenuItem::new("Settings") .icon(IconName::Settings) .children([ SidebarMenuItem::new("General") .context_menu(|menu, _, _| { menu.menu("Reset to Defaults", Box::new(ResetAction)) }), SidebarMenuItem::new("Advanced") .context_menu(|menu, _, _| { menu.menu("Export Settings", Box::new(ExportAction)) .menu("Import Settings", Box::new(ImportAction)) }) ]) ) ``` ### Custom Width and Styling ```rust Sidebar::new() .width(280) // Custom width in pixels .border_width(2) // Custom border width .header( SidebarHeader::new() .p_4() // Custom padding .rounded(cx.theme().radius) .child("Custom Styled Sidebar") ) ``` ### Interactive Header with Popup Menu ```rust use gpui_component::menu::DropdownMenu; SidebarHeader::new() .child( h_flex() .gap_2() .child(Icon::new(IconName::Building)) .child("Company Name") .child(Icon::new(IconName::ChevronsUpDown)) ) .dropdown_menu(|menu, _, _| { menu.menu("Acme Corp", Box::new(SelectCompany("acme"))) .menu("Tech Solutions", Box::new(SelectCompany("tech"))) .separator() .menu("Switch Organization", Box::new(SwitchOrg)) }) ``` ### Footer with User Information ```rust SidebarFooter::new() .justify_between() .child( h_flex() .gap_2() .child(Icon::new(IconName::User)) .when(!collapsed, |this| { this.child( v_flex() .child("John Doe") .child(div().text_xs().text_color(cx.theme().muted_foreground).child("john@example.com")) ) }) ) .when(!collapsed, |this| { this.child(Icon::new(IconName::MoreHorizontal)) }) ``` ### Responsive Sidebar ```rust let is_mobile = window_width < 768; Sidebar::new() .collapsed(is_mobile || manually_collapsed) .width(if is_mobile { 60 } else { 240 }) .header( SidebarHeader::new() .child( div() .when(!is_mobile, |this| this.child("Full App Name")) .when(is_mobile, |this| this.child(Icon::new(IconName::Menu))) ) ) ``` ## Theming The sidebar uses dedicated theme colors: ```rust // Theme colors used by sidebar cx.theme().sidebar // Background cx.theme().sidebar_foreground // Text color cx.theme().sidebar_border // Border color cx.theme().sidebar_accent // Hover/active background cx.theme().sidebar_accent_foreground // Hover/active text cx.theme().sidebar_primary // Primary elements cx.theme().sidebar_primary_foreground // Primary text ``` ## Examples ### File Explorer Sidebar ```rust Sidebar::new() .header( SidebarHeader::new() .child( h_flex() .gap_2() .child(IconName::Folder) .child("Explorer") ) ) .child( SidebarGroup::new("Folders") .child( SidebarMenu::new() .child( SidebarMenuItem::new("src") .icon(IconName::FolderOpen) .active(true) .children([ SidebarMenuItem::new("components") .icon(IconName::Folder), SidebarMenuItem::new("utils") .icon(IconName::Folder), SidebarMenuItem::new("main.rs") .icon(IconName::FileCode) .active(true), ]) ) .child( SidebarMenuItem::new("tests") .icon(IconName::Folder) ) .child( SidebarMenuItem::new("Cargo.toml") .icon(IconName::FileText) ) ) ) ``` ### Admin Dashboard Sidebar ```rust Sidebar::new() .header( SidebarHeader::new() .child( h_flex() .gap_2() .child( div() .size_8() .rounded_full() .bg(cx.theme().primary) .child(Icon::new(IconName::Crown)) ) .child("Admin Panel") ) ) .child( SidebarGroup::new("Overview") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Dashboard") .icon(IconName::LayoutDashboard) .active(true) ) .child( SidebarMenuItem::new("Analytics") .icon(IconName::TrendingUp) .suffix(Badge::new().count(2)) ) ) ) .child( SidebarGroup::new("Management") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Users") .icon(IconName::Users) .suffix("1,234") ) .child( SidebarMenuItem::new("Orders") .icon(IconName::ShoppingCart) .suffix(Badge::new().dot().variant_destructive()) ) .child( SidebarMenuItem::new("Products") .icon(IconName::Package) ) ) ) .footer( SidebarFooter::new() .child( h_flex() .gap_2() .child(IconName::User) .child("Administrator") ) .child(IconName::LogOut) ) ``` ### Settings Sidebar ```rust Sidebar::new() .width(300) .header( SidebarHeader::new() .child("Settings") ) .child( SidebarGroup::new("General") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Appearance") .icon(IconName::Palette) .active(true) ) .child( SidebarMenuItem::new("Notifications") .icon(IconName::Bell) .suffix( Switch::new("notifications") .checked(true) .xsmall() ) ) .child( SidebarMenuItem::new("Privacy") .icon(IconName::Shield) ) ) ) .child( SidebarGroup::new("Advanced") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Developer") .icon(IconName::Code) .children([ SidebarMenuItem::new("Debug Mode") .suffix( Switch::new("debug") .checked(false) .xsmall() ), SidebarMenuItem::new("Console") .on_click(|_, _, _| println!("Open console")), ]) ) .child( SidebarMenuItem::new("Performance") .icon(IconName::Zap) ) ) ) ``` ================================================ FILE: docs/docs/components/skeleton.md ================================================ --- title: Skeleton description: Use to show a placeholder while content is loading. --- # Skeleton The Skeleton component displays animated placeholder content while actual content is loading. It provides visual feedback to users that content is being loaded and helps maintain layout structure during loading states. ## Import ```rust use gpui_component::skeleton::Skeleton; ``` ## Usage ### Basic Skeleton ```rust Skeleton::new() ``` ### Text Line Skeleton ```rust // Single line of text Skeleton::new() .w(px(250.)) .h_4() .rounded_md() // Multiple text lines v_flex() .gap_2() .child(Skeleton::new().w(px(250.)).h_4().rounded_md()) .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) .child(Skeleton::new().w(px(180.)).h_4().rounded_md()) ``` ### Circle Skeleton ```rust // Avatar placeholder Skeleton::new() .size_12() .rounded_full() // Profile picture placeholder Skeleton::new() .w(px(64.)) .h(px(64.)) .rounded_full() ``` ### Rectangle Skeleton ```rust // Card image placeholder Skeleton::new() .w(px(250.)) .h(px(125.)) .rounded_md() // Button placeholder Skeleton::new() .w(px(120.)) .h(px(40.)) .rounded_md() ``` ### Different Shapes ```rust // Text content Skeleton::new().w(px(200.)).h_4().rounded_sm() // Square image Skeleton::new().size_20().rounded_md() // Wide banner Skeleton::new().w_full().h(px(200.)).rounded_lg() // Small icon Skeleton::new().size_6().rounded_md() ``` ### Secondary Variant ```rust // Use secondary color (more subtle) Skeleton::new() .secondary() .w(px(200.)) .h_4() .rounded_md() ``` ## Animation The Skeleton component includes a built-in pulse animation that: - Runs continuously with a 2-second duration - Uses a bounce easing function with ease-in-out - Animates opacity from 100% to 50% and back - Automatically repeats to indicate loading state The animation cannot be disabled as it's essential for indicating loading state. ## Sizes The Skeleton component doesn't have predefined size variants. Instead, use gpui's sizing utilities: ```rust // Height utilities Skeleton::new().h_3() // 12px height Skeleton::new().h_4() // 16px height Skeleton::new().h_5() // 20px height Skeleton::new().h_6() // 24px height // Width utilities Skeleton::new().w(px(100.)) // 100px width Skeleton::new().w(px(200.)) // 200px width Skeleton::new().w_full() // Full width Skeleton::new().w_1_2() // 50% width // Square sizes Skeleton::new().size_4() // 16x16px Skeleton::new().size_8() // 32x32px Skeleton::new().size_12() // 48x48px Skeleton::new().size_16() // 64x64px ``` ## Examples ### Loading Profile Card ```rust v_flex() .gap_4() .p_4() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius_lg) .child( h_flex() .gap_3() .items_center() .child(Skeleton::new().size_12().rounded_full()) // Avatar .child( v_flex() .gap_2() .child(Skeleton::new().w(px(120.)).h_4().rounded_md()) // Name .child(Skeleton::new().w(px(100.)).h_3().rounded_md()) // Email ) ) .child( v_flex() .gap_2() .child(Skeleton::new().w_full().h_4().rounded_md()) // Bio line 1 .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) // Bio line 2 ) ``` ### Loading Article List ```rust v_flex() .gap_6() .children((0..3).map(|_| { h_flex() .gap_4() .child(Skeleton::new().w(px(120.)).h(px(80.)).rounded_md()) // Thumbnail .child( v_flex() .gap_2() .flex_1() .child(Skeleton::new().w_full().h_5().rounded_md()) // Title .child(Skeleton::new().w(px(300.)).h_4().rounded_md()) // Excerpt line 1 .child(Skeleton::new().w(px(250.)).h_4().rounded_md()) // Excerpt line 2 .child(Skeleton::new().w(px(100.)).h_3().rounded_md()) // Date ) })) ``` ### Loading Table Rows ```rust v_flex() .gap_2() .children((0..5).map(|_| { h_flex() .gap_4() .p_3() .border_b_1() .border_color(cx.theme().border) .child(Skeleton::new().size_8().rounded_full()) // Status indicator .child(Skeleton::new().w(px(150.)).h_4().rounded_md()) // Name .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) // Email .child(Skeleton::new().w(px(80.)).h_4().rounded_md()) // Role .child(Skeleton::new().w(px(60.)).h_4().rounded_md()) // Actions })) ``` ### Loading Button States ```rust h_flex() .gap_3() .child(Skeleton::new().w(px(80.)).h(px(36.)).rounded_md()) // Primary button .child(Skeleton::new().w(px(70.)).h(px(36.)).rounded_md()) // Secondary button .child(Skeleton::new().size_9().rounded_md()) // Icon button ``` ### Loading Form Fields ```rust v_flex() .gap_4() .child( v_flex() .gap_1() .child(Skeleton::new().w(px(60.)).h_4().rounded_md()) // Label .child(Skeleton::new().w_full().h(px(40.)).rounded_md()) // Input ) .child( v_flex() .gap_1() .child(Skeleton::new().w(px(80.)).h_4().rounded_md()) // Label .child(Skeleton::new().w_full().h(px(120.)).rounded_md()) // Textarea ) ``` ### Conditional Loading ```rust if loading { Skeleton::new().w(px(200.)).h_4().rounded_md() } else { div().child("Actual content here") } ``` ## Theming The Skeleton component uses the theme's `skeleton` color, which defaults to the `secondary` color if not specified. You can customize it in your theme: ```json { "skeleton.background": "#e2e8f0" } ``` The `secondary(true)` variant applies 50% opacity to the skeleton color for more subtle loading indicators. ================================================ FILE: docs/docs/components/slider.md ================================================ --- title: Slider description: A control that allows the user to select values from a range using a draggable thumb. --- # Slider A slider component for selecting numeric values within a specified range. Supports both single value and range selection modes, horizontal and vertical orientations, custom styling, and step intervals. ## Import ```rust use gpui_component::slider::{Slider, SliderState, SliderEvent, SliderValue}; ``` ## Usage ### Basic Slider ```rust let slider_state = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(50.0) .step(1.0) }); Slider::new(&slider_state) ``` ### Slider with Event Handling ```rust struct MyView { slider_state: Entity, current_value: f32, } impl MyView { fn new(cx: &mut Context) -> Self { let slider_state = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(25.0) .step(5.0) }); let subscription = cx.subscribe(&slider_state, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.current_value = value.start(); cx.notify(); } } }); Self { slider_state, current_value: 25.0, } } } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child(Slider::new(&self.slider_state)) .child(format!("Value: {}", self.current_value)) } } ``` ### Range Slider ```rust let range_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(20.0..80.0) // Range from 20 to 80 .step(1.0) }); Slider::new(&range_slider) ``` ### Vertical Slider ```rust Slider::new(&slider_state) .vertical() .h(px(200.)) ``` ### Custom Step Intervals ```rust // Integer steps let integer_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(10.0) .step(1.0) .default_value(5.0) }); // Decimal steps let decimal_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(0.5) }); ``` ### Min/Max Configuration ```rust // Temperature slider let temp_slider = cx.new(|_| { SliderState::new() .min(-10.0) .max(40.0) .default_value(20.0) .step(0.5) }); // Percentage slider let percent_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(75.0) .step(5.0) }); ``` ### Disabled State ```rust Slider::new(&slider_state) .disabled(true) ``` ### Custom Styling ```rust Slider::new(&slider_state) .bg(cx.theme().success) .text_color(cx.theme().success_foreground) .rounded(px(4.)) ``` ### Scale There have 2 types of scale for the slider: - `Linear` (default) - `Logarithmic` The logarithmic scale is useful when the range of values is large and you want to give more precision to smaller values. ```rust let log_slider = cx.new(|_| { SliderState::new() .min(1.0) // min must be greater than 0 for log scale .max(1000.0) .default_value(10.0) .step(1.0) .scale(SliderScale::Logarithmic) }); ``` In this case: :::info $$ v = min \times (max/min)^p $$ The value `v` is calculated using the formula above, where `p` is the slider percentage (0 to 1). ::: - If slider at 25%, value will be approximately `5.62`. - If slider at 50%, value will be approximately `31.62`. - If slider at 75%, value will be approximately `177.83`. - If slider at 100%, value will be `1000.0`. #### Conversions ```rust // From f32 let single_value: SliderValue = 42.0.into(); // From tuple let range_value: SliderValue = (10.0, 90.0).into(); // From Range let range_value: SliderValue = (10.0..90.0).into(); ``` ### SliderEvent | Event | Description | | --------------------- | --------------------------------- | | `Change(SliderValue)` | Emitted when slider value changes | ### Styling The slider component implements `Styled` trait and supports: - Background color for track and thumb - Text color for thumb - Border radius - Size customization ## Examples ### Color Picker ```rust struct ColorPicker { hue_slider: Entity, saturation_slider: Entity, lightness_slider: Entity, alpha_slider: Entity, current_color: Hsla, } impl ColorPicker { fn new(cx: &mut Context) -> Self { let hue_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(0.5) }); let saturation_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(1.0) }); // Subscribe to all sliders to update color let subscriptions = [&hue_slider, &saturation_slider /* ... */] .iter() .map(|slider| { cx.subscribe(slider, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(_) => { this.update_color(cx); } } }) }) .collect::>(); Self { hue_slider, saturation_slider, // ... other fields } } fn update_color(&mut self, cx: &mut Context) { let h = self.hue_slider.read(cx).value().start(); let s = self.saturation_slider.read(cx).value().start(); // ... calculate color self.current_color = hsla(h, s, l, a); cx.notify(); } } impl Render for ColorPicker { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .gap_4() .child( v_flex() .gap_2() .child("Hue") .child(Slider::new(&self.hue_slider).vertical().h(px(120.))) ) .child( v_flex() .gap_2() .child("Saturation") .child(Slider::new(&self.saturation_slider).vertical().h(px(120.))) ) // ... other sliders } } ``` ### Volume Control ```rust struct VolumeControl { volume_slider: Entity, volume: f32, } impl VolumeControl { fn new(cx: &mut Context) -> Self { let volume_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .step(1.0) .default_value(50.0) }); let subscription = cx.subscribe(&volume_slider, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.volume = value.start(); this.apply_volume_change(); cx.notify(); } } }); Self { volume_slider, volume: 50.0, } } fn apply_volume_change(&self) { // Apply volume change to audio system println!("Volume changed to: {}%", self.volume); } } impl Render for VolumeControl { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { h_flex() .items_center() .gap_3() .child("🔊") .child(Slider::new(&self.volume_slider).flex_1()) .child(format!("{}%", self.volume as i32)) } } ``` ### Price Range Filter ```rust struct PriceFilter { price_range: Entity, min_price: f32, max_price: f32, } impl PriceFilter { fn new(cx: &mut Context) -> Self { let price_range = cx.new(|_| { SliderState::new() .min(0.0) .max(1000.0) .step(10.0) .default_value(100.0..500.0) // Range slider }); let subscription = cx.subscribe(&price_range, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.min_price = value.start(); this.max_price = value.end(); this.filter_products(); cx.notify(); } } }); Self { price_range, min_price: 100.0, max_price: 500.0, } } fn filter_products(&self) { println!("Filtering products: ${} - ${}", self.min_price, self.max_price); } } impl Render for PriceFilter { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child("Price Range") .child(Slider::new(&self.price_range)) .child(format!("${} - ${}", self.min_price as i32, self.max_price as i32)) } } ``` ### Temperature Slider with Custom Styling ```rust struct TemperatureControl { temp_slider: Entity, temperature: f32, } impl Render for TemperatureControl { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let temp_color = if self.temperature < 10.0 { cx.theme().info // Cold - blue } else if self.temperature > 25.0 { cx.theme().destructive // Hot - red } else { cx.theme().success // Comfortable - green }; v_flex() .gap_3() .child("Temperature Control") .child( Slider::new(&self.temp_slider) .bg(temp_color) .text_color(cx.theme().background) .rounded(px(8.)) ) .child(format!("{}°C", self.temperature as i32)) } } ``` ## Keyboard Shortcuts | Key | Action | | ------------- | ------------------------------ | | `←` / `↓` | Decrease value by step | | `→` / `↑` | Increase value by step | | `Page Down` | Decrease by larger amount | | `Page Up` | Increase by larger amount | | `Home` | Set to minimum value | | `End` | Set to maximum value | | `Tab` | Move focus to next element | | `Shift + Tab` | Move focus to previous element | ================================================ FILE: docs/docs/components/spinner.md ================================================ --- title: Spinner description: Displays an animated loading showing the completion progress of a task. --- # Spinner Spinner element displays an animated loading. Perfect for showing loading states, progress spinners, and other visual feedback during asynchronous operations. Features customizable icons, colors, sizes, and rotation animations. ## Import ```rust use gpui_component::spinner::Spinner; ``` ## Usage ### Basic ```rust // Default loader icon Spinner::new() ``` ### Spinner with Custom Color ```rust use gpui_component::ActiveTheme; // Blue spinner Spinner::new() .color(cx.theme().blue) // Green spinner for success states Spinner::new() .color(cx.theme().green) // Custom color Spinner::new() .color(cx.theme().cyan) ``` ### Spinner Sizes ```rust // Extra small spinner Spinner::new().xsmall() // Small spinner Spinner::new().small() // Medium spinner (default) Spinner::new() // Large spinner Spinner::new().large() // Custom size Spinner::new().with_size(px(64.)) ``` ### Spinner with Custom Icon ```rust use gpui_component::IconName; // Loading circle icon Spinner::new() .icon(IconName::LoaderCircle) // Large loading circle with custom color Spinner::new() .icon(IconName::LoaderCircle) .large() .color(cx.theme().cyan) // Different loading icons Spinner::new() .icon(IconName::Loader) .color(cx.theme().primary) ``` ## Available Icons The Spinner component supports various loading and progress icons: ### Loading Icons - `Loader` (default) - Rotating line spinner - `LoaderCircle` - Circular loading spinner ### Other Compatible Icons - Any icon from the `IconName` enum can be used, though loading-specific icons work best with the rotation animation ## Animation The Spinner component features a built-in rotation animation: - **Duration**: 0.8 seconds (configurable via speed parameter) - **Easing**: Ease-in-out transition - **Repeat**: Infinite loop - **Transform**: 360-degree rotation ## Size Reference | Size | Method | Approximate Pixels | | ----------- | ------------------- | ------------------ | | Extra Small | `.xsmall()` | ~12px | | Small | `.small()` | ~14px | | Medium | (default) | ~16px | | Large | `.large()` | ~24px | | Custom | `.with_size(px(n))` | n px | ## Examples ### Loading States ```rust // Simple loading spinner Spinner::new() // Loading with custom color Spinner::new() .color(cx.theme().blue) // Large loading spinner Spinner::new() .large() .color(cx.theme().primary) ``` ### Different Loading Icons ```rust // Default loader (line spinner) Spinner::new() .color(cx.theme().muted_foreground) // Circle loader Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().blue) // Large circle loader with custom color Spinner::new() .icon(IconName::LoaderCircle) .large() .color(cx.theme().green) ``` ### Status Spinners ```rust // Loading state Spinner::new() .small() .color(cx.theme().muted_foreground) // Processing state Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().blue) // Success processing (still animating) Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().green) ``` ### Size Variations ```rust // Extra small for inline text Spinner::new() .xsmall() .color(cx.theme().muted_foreground) // Small for buttons Spinner::new() .small() .color(cx.theme().primary_foreground) // Medium for general use (default) Spinner::new() .color(cx.theme().primary) // Large for prominent loading states Spinner::new() .large() .color(cx.theme().blue) // Custom size for specific requirements Spinner::new() .with_size(px(32.)) .color(cx.theme().orange) ``` ### In UI Components ```rust // In a button Button::new("submit-btn") .loading(true) .icon( Spinner::new() .small() .color(cx.theme().primary_foreground) ) .label("Loading...") // In a card header div() .flex() .items_center() .gap_2() .child("Processing...") .child( Spinner::new() .small() .color(cx.theme().muted_foreground) ) // Full-screen loading div() .flex() .items_center() .justify_center() .h_full() .w_full() .child( Spinner::new() .large() .color(cx.theme().primary) ) ``` ## Performance Considerations - The animation uses CSS transforms for optimal performance - Multiple spinners on the same page share the same animation timing - The component is lightweight and suitable for frequent updates - Consider using smaller sizes for better performance with many spinners ## Common Patterns ### Conditional Loading ```rust // Show spinner only when loading .when(is_loading, |this| { this.child( Spinner::new() .small() .color(cx.theme().muted_foreground) ) }) ``` ### Loading with Text ```rust // Loading text with spinner h_flex() .items_center() .gap_2() .child( Spinner::new() .small() .color(cx.theme().primary) ) .child("Loading data...") ``` ### Overlay Loading ```rust // Full overlay with spinner div() .absolute() .inset_0() .flex() .items_center() .justify_center() .bg(cx.theme().background.alpha(0.8)) .child( v_flex() .items_center() .gap_3() .child( Spinner::new() .large() .color(cx.theme().primary) ) .child("Loading...") ) ``` ================================================ FILE: docs/docs/components/stepper.md ================================================ --- title: Stepper description: A step-by-step progress for users to navigate through a series of steps or stages. --- # Stepper A step-by-step progress component that guides users through a series of steps or stages. Supports horizontal and vertical layouts, custom icons, and different sizes. ## Import ```rust use gpui_component::stepper::{Stepper, StepperItem}; ``` ## Usage ### Basic Stepper Use `selected_index` method to set current active step by index (0-based), default is `0`. ```rust Stepper::new("my-stepper") .selected_index(0) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), StepperItem::new().child("Step 3"), ]) .on_click(|step, _, _| { println!("Clicked step: {}", step); }) ``` ### With Icons ```rust use gpui_component::IconName; Stepper::new("icon-stepper") .selected_index(0) .items([ StepperItem::new() .icon(IconName::Calendar) .child("Order Details"), StepperItem::new() .icon(IconName::Inbox) .child("Shipping"), StepperItem::new() .icon(IconName::Frame) .child("Preview"), StepperItem::new() .icon(IconName::Info) .child("Finish"), ]) ``` ### Vertical Layout ```rust Stepper::new("vertical-stepper") .vertical() .selected_index(2) .items_center() .items([ StepperItem::new() .pb_8() .icon(IconName::Building2) .child(v_flex().child("Step 1").child("Description for step 1.")), StepperItem::new() .pb_8() .icon(IconName::Asterisk) .child(v_flex().child("Step 2").child("Description for step 2.")), StepperItem::new() .pb_8() .icon(IconName::Folder) .child(v_flex().child("Step 3").child("Description for step 3.")), StepperItem::new() .icon(IconName::CircleCheck) .child(v_flex().child("Step 4").child("Description for step 4.")), ]) ``` ### Text Center The `text_center` method centers the text within each step item. ```rust Stepper::new("center-stepper") .selected_index(0) .text_center(true) .items([ StepperItem::new().child( v_flex() .items_center() .child("Step 1") .child("Desc for step 1."), ), StepperItem::new().child( v_flex() .items_center() .child("Step 2") .child("Desc for step 2."), ), StepperItem::new().child( v_flex() .items_center() .child("Step 3") .child("Desc for step 3."), ), ]) ``` ### Different Sizes ```rust use gpui_component::{Sizable as _, Size}; Stepper::new("stepper") .xsmall() .items([...]) Stepper::new("stepper") .small() .items([...]) Stepper::new("stepper") .large() .items([...]) ``` ### Disabled State ```rust Stepper::new("disabled-stepper") .disabled(true) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), ]) ``` ### Handle Click Events ```rust Stepper::new("my-stepper") .selected_index(current_step) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), StepperItem::new().child("Step 3"), ]) .on_click(cx.listener(|this, step, _, cx| { this.current_step = *step; cx.notify(); })) ``` ## API Reference - [Stepper] - [StepperItem] ### Sizing Implements [Sizable] trait: - `xsmall()` - Extra small size - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size ## Examples ### Multi-step Form ```rust Stepper::new("form-stepper") .w_full() .selected_index(form_step) .items([ StepperItem::new() .icon(IconName::User) .child("Personal Info"), StepperItem::new() .icon(IconName::CreditCard) .child("Payment"), StepperItem::new() .icon(IconName::CircleCheck) .child("Confirmation"), ]) .on_click(cx.listener(|this, step, _, cx| { this.form_step = *step; cx.notify(); })) ``` ### Disabled Individual Steps ```rust Stepper::new("stepper") .selected_index(0) .items([ StepperItem::new().child("Available"), StepperItem::new().disabled(true).child("Locked"), StepperItem::new().child("Available"), ]) ``` [Stepper]: https://docs.rs/gpui-component/latest/gpui_component/stepper/struct.Stepper.html [StepperItem]: https://docs.rs/gpui-component/latest/gpui_component/stepper/struct.StepperItem.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ================================================ FILE: docs/docs/components/switch.md ================================================ --- title: Switch description: A control that allows the user to toggle between checked and not checked. --- # Switch A toggle switch component for binary on/off states. Features smooth animations, different sizes, labels, disabled state, and customizable positioning. ## Import ```rust use gpui_component::switch::Switch; ``` ## Usage ### Basic Switch ```rust Switch::new("my-switch") .checked(false) .on_click(|checked, _, _| { println!("Switch is now: {}", checked); }) ``` ### Controlled Switch ```rust struct MyView { is_enabled: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Switch::new("switch") .checked(self.is_enabled) .on_click(cx.listener(|view, checked, _, cx| { view.is_enabled = *checked; cx.notify(); })) } } ``` ### With Label ```rust Switch::new("notifications") .label("Enable notifications") .checked(true) .on_click(|checked, _, _| { println!("Notifications: {}", if *checked { "enabled" } else { "disabled" }); }) ``` ### Different Sizes ```rust // Small switch Switch::new("small-switch") .small() .label("Small switch") // Medium switch (default) Switch::new("medium-switch") .label("Medium switch") // Using explicit size Switch::new("custom-switch") .with_size(Size::Small) .label("Custom size") ``` ### Disabled State ```rust // Disabled unchecked Switch::new("disabled-off") .label("Disabled (off)") .disabled(true) .checked(false) // Disabled checked Switch::new("disabled-on") .label("Disabled (on)") .disabled(true) .checked(true) ``` ### With Tooltip ```rust Switch::new("switch") .label("Airplane mode") .tooltip("Enable airplane mode to disable all wireless connections") .checked(false) ``` ## API Reference ### Switch | Method | Description | | ------------------ | ----------------------------------------------------------- | | `new(id)` | Create a new switch with the given ID | | `checked(bool)` | Set the checked/toggled state | | `label(text)` | Set label text for the switch | | `label_side(side)` | Position label (Side::Left or Side::Right) | | `disabled(bool)` | Set disabled state | | `tooltip(text)` | Add tooltip text | | `on_click(fn)` | Callback when clicked, receives `&bool` (new checked state) | ### Styling Implements `Sizable` and `Disableable` traits: - `small()` - Small switch size (28x16px toggle area) - `medium()` - Medium switch size (36x20px toggle area, default) - `with_size(size)` - Set explicit size - `disabled(bool)` - Disabled state ### Styling Properties The switch can also be styled using GPUI's styling methods: - `w(width)` - Custom width - `h(height)` - Custom height - Standard margin, padding, and positioning methods ## Examples ### Settings Panel ```rust struct SettingsView { marketing_emails: bool, security_emails: bool, push_notifications: bool, } v_flex() .gap_4() .child( // Setting with description v_flex() .gap_2() .child( h_flex() .items_center() .justify_between() .child( v_flex() .child(Label::new("Marketing emails").text_lg()) .child( Label::new("Receive emails about new products and features") .text_color(theme.muted_foreground) ) ) .child( Switch::new("marketing") .checked(self.marketing_emails) .on_click(cx.listener(|view, checked, _, cx| { view.marketing_emails = *checked; cx.notify(); })) ) ) ) .child( // Simple setting h_flex() .items_center() .justify_between() .child(Label::new("Push notifications")) .child( Switch::new("push") .checked(self.push_notifications) .on_click(cx.listener(|view, checked, _, cx| { view.push_notifications = *checked; cx.notify(); })) ) ) ``` ### Compact Settings List ```rust v_flex() .gap_3() .child( Switch::new("wifi") .label("Wi-Fi") .label_side(Side::Left) .checked(true) .small() ) .child( Switch::new("bluetooth") .label("Bluetooth") .label_side(Side::Left) .checked(false) .small() ) .child( Switch::new("airplane") .label("Airplane Mode") .label_side(Side::Left) .checked(false) .disabled(true) .small() ) ``` ### Form Integration ```rust struct FormData { subscribe_newsletter: bool, enable_notifications: bool, remember_me: bool, } v_flex() .gap_4() .p_4() .border_1() .border_color(theme.border) .rounded(theme.radius) .child( Switch::new("newsletter") .label("Subscribe to newsletter") .checked(self.subscribe_newsletter) .tooltip("Receive monthly updates about new features") .on_click(cx.listener(|view, checked, _, cx| { view.subscribe_newsletter = *checked; cx.notify(); })) ) .child( Switch::new("notifications") .label("Enable notifications") .checked(self.enable_notifications) .on_click(cx.listener(|view, checked, _, cx| { view.enable_notifications = *checked; cx.notify(); })) ) .child( Switch::new("remember") .label("Remember me") .checked(self.remember_me) .small() .on_click(cx.listener(|view, checked, _, cx| { view.remember_me = *checked; cx.notify(); })) ) ``` ### Custom Styling ```rust Switch::new("custom") .label("Custom styled switch") .w(px(200.)) .checked(true) .on_click(|checked, _, _| { println!("Custom switch: {}", checked); }) ``` ## Animation The switch features smooth animations: - **Toggle animation**: 150ms duration when switching states - **Background color transition**: Changes from switch color to primary color - **Position animation**: Smooth movement of the toggle indicator - **Disabled state**: Animations are disabled when the switch is disabled ================================================ FILE: docs/docs/components/table.md ================================================ --- title: Table description: A basic table component for directly rendering tabular data. --- # Table A simple, stateless, composable table component for rendering tabular data. Unlike [DataTable], this component does not include virtual scrolling, sorting, or column management — it is designed for straightforward data display using a declarative API. ## Import ```rust use gpui_component::table::{ Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption, }; ``` ## Usage ### Basic Table ```rust Table::new() .child(TableHeader::new().child( TableRow::new() .child(TableHead::new().child("Name")) .child(TableHead::new().child("Email")) .child(TableHead::new().text_right().child("Amount")) )) .child(TableBody::new() .child(TableRow::new() .child(TableCell::new().child("John")) .child(TableCell::new().child("john@example.com")) .child(TableCell::new().text_right().child("$100.00"))) .child(TableRow::new() .child(TableCell::new().child("Jane")) .child(TableCell::new().child("jane@example.com")) .child(TableCell::new().text_right().child("$200.00"))) ) .child(TableCaption::new().child("A list of recent invoices.")) ``` ### With Footer ```rust Table::new() .child(TableHeader::new().child( TableRow::new() .child(TableHead::new().child("Invoice")) .child(TableHead::new().child("Status")) .child(TableHead::new().text_right().child("Amount")) )) .child(TableBody::new() .child(TableRow::new() .child(TableCell::new().child("INV001")) .child(TableCell::new().child("Paid")) .child(TableCell::new().text_right().child("$250.00"))) ) .child(TableFooter::new().child( TableRow::new() .child(TableCell::new().child("Total")) .child(TableCell::new().child("")) .child(TableCell::new().text_right().child("$250.00")) )) ``` ### Column Widths Use `.w()` on `TableHead` and `TableCell` to set fixed column widths: ```rust TableRow::new() .child(TableHead::new().w(px(80.)).child("ID")) .child(TableHead::new().child("Name")) // flex-1 .child(TableHead::new().w(px(120.)).child("Date")) ``` ### Text Alignment ```rust // Center-aligned header TableHead::new().text_center().child("Status") // Right-aligned cell (e.g., for numbers) TableCell::new().text_right().child("$1,000.00") ``` ### Without Border (via Styled) All table sub-components implement the `Styled` trait, so you can customize styles directly: ```rust // Remove border and rounded corners Table::new() .border_0() .rounded_none() .child(/* ... */) ``` ### Custom Styling Since all components implement `Styled`, you can apply any GPUI style: ```rust // Custom row hover TableRow::new() .bg(cx.theme().table_even) .child(/* ... */) // Custom cell padding TableCell::new() .px_4() .child("Custom padded content") ``` ## Sub-components | Component | Description | |-----------|-------------| | `Table` | Root container with border, rounded corners, and background | | `TableHeader` | Header section with distinct background and font weight | | `TableBody` | Body section wrapping data rows | | `TableFooter` | Footer section with top border | | `TableRow` | A flex row with bottom border | | `TableHead` | Header cell with alignment and width options | | `TableCell` | Data cell with alignment and width options | | `TableCaption` | Caption text below the table | ## API Reference ### Table - `new()` - Create a new table - Implements `Styled`, `ParentElement`, `Sizable`, `RenderOnce` ### TableHead / TableCell - `new()` - Create a new head/cell - `w(width)` - Set fixed width (otherwise flex-1) - `text_center()` - Center-align content - `text_right()` - Right-align content - Implements `Styled`, `ParentElement`, `RenderOnce` ### TableHeader / TableBody / TableFooter / TableRow / TableCaption - `new()` - Create a new instance - Implements `Styled`, `ParentElement`, `RenderOnce` ## Table vs DataTable | Feature | Table | DataTable | |---------|-------|-----------| | Virtual scrolling | No | Yes | | Column sorting | No | Yes | | Column resizing | No | Yes | | Column moving | No | Yes | | Cell selection | No | Yes | | Row selection | No | Yes | | Infinite loading | No | Yes | | Keyboard navigation | No | Yes | | State management | Stateless | TableState | | Best for | Small, static data | Large, interactive datasets | [DataTable]: ./data-table.md ================================================ FILE: docs/docs/components/tabs.md ================================================ --- title: Tabs description: A set of layered sections of content—known as tab panels—that are displayed one at a time. --- # Tabs A tabbed interface component for organizing content into separate sections. Supports multiple variants, sizes, navigation controls, and interactive features like reordering and prefix/suffix elements. ## Import ```rust use gpui_component::tab::{Tab, TabBar}; ``` ## Usage ### Basic Tabs ```rust TabBar::new("tabs") .selected_index(0) .on_click(|selected_index, _, _| { println!("Tab {} selected", selected_index); }) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Settings")) ``` ### Tab Variants #### Default Tabs ```rust TabBar::new("default-tabs") .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Underline Tabs ```rust TabBar::new("underline-tabs") .underline() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Pill Tabs ```rust TabBar::new("pill-tabs") .pill() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Outline Tabs ```rust TabBar::new("outline-tabs") .outline() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Segmented Tabs ```rust use gpui_component::IconName; TabBar::new("segmented-tabs") .segmented() .selected_index(0) .child(IconName::Bot) .child(IconName::Calendar) .child(IconName::Map) .children(vec!["Settings", "About"]) ``` ### Tab Sizes ```rust // Extra Small TabBar::new("tabs").xsmall() .child(Tab::new().label("Small")) // Small TabBar::new("tabs").small() .child(Tab::new().label("Small")) // Medium (default) TabBar::new("tabs") .child(Tab::new().label("Medium")) // Large TabBar::new("tabs").large() .child(Tab::new().label("Large")) ``` ### Tabs with Icons ```rust use gpui_component::{Icon, IconName}; TabBar::new("icon-tabs") .child(Tab::default().icon(IconName::User).with_variant(TabVariant::Tab)) .child(Tab::default().icon(IconName::Settings).with_variant(TabVariant::Tab)) .child(Tab::default().icon(IconName::Mail).with_variant(TabVariant::Tab)) ``` ### Tabs with Prefix and Suffix ```rust use gpui_component::button::Button; use gpui_component::{h_flex, IconName}; TabBar::new("tabs-with-controls") .prefix( h_flex() .gap_1() .child(Button::new("back").ghost().xsmall().icon(IconName::ArrowLeft)) .child(Button::new("forward").ghost().xsmall().icon(IconName::ArrowRight)) ) .suffix( h_flex() .gap_1() .child(Button::new("inbox").ghost().xsmall().icon(IconName::Inbox)) .child(Button::new("more").ghost().xsmall().icon(IconName::Ellipsis)) ) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Settings")) ``` ### Disabled Tabs ```rust TabBar::new("tabs-with-disabled") .child(Tab::new().label("Account")) .child(Tab::new().label("Profile").disabled(true)) .child(Tab::new().label("Settings")) ``` ### Dynamic Tabs ```rust struct TabsView { active_tab: usize, tabs: Vec, } impl Render for TabsView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { TabBar::new("dynamic-tabs") .selected_index(self.active_tab) .on_click(cx.listener(|view, index, _, cx| { view.active_tab = *index; cx.notify(); })) .children( self.tabs .iter() .map(|tab_name| Tab::new().label(tab_name.clone())) ) } } ``` ### Tabs with Menu Use `menu` option to enable a dropdown menu for tab selection when there are many tabs, this is default `false`. If enable, the will have a dropdown button at the end of the tab bar to show all tabs in a menu. ```rust TabBar::new("tabs-with-menu") .menu(true) .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) .child(Tab::new().label("Mail")) .child(Tab::new().label("Settings")) ``` ### Scrollable Tabs ```rust use gpui::ScrollHandle; struct ScrollableTabsView { scroll_handle: ScrollHandle, } impl Render for ScrollableTabsView { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { TabBar::new("scrollable-tabs") .track_scroll(&self.scroll_handle) .child(Tab::new().label("Very Long Tab Name 1")) .child(Tab::new().label("Very Long Tab Name 2")) .child(Tab::new().label("Very Long Tab Name 3")) .child(Tab::new().label("Very Long Tab Name 4")) .child(Tab::new().label("Very Long Tab Name 5")) } } ``` ### Individual Tab Configuration ```rust TabBar::new("custom-tabs") .child( Tab::new().label("Custom Tab") .id("custom-id") .prefix(IconName::Star) .suffix(IconName::X) .on_click(|_, _, _| { println!("Custom tab clicked"); }) ) ``` ## API Reference ### TabBar | Method | Description | | --------------------------- | -------------------------------------------------- | | `new(id)` | Create a new tab bar with the given ID | | `child(tab)` | Add a tab to the bar | | `children(tabs)` | Add multiple tabs to the bar | | `selected_index(index)` | Set the active tab index | | `on_click(fn)` | Callback when a tab is clicked, receives tab index | | `prefix(element)` | Add element before the tabs | | `suffix(element)` | Add element after the tabs | | `last_empty_space(element)` | Custom element for empty space at the end | | `track_scroll(handle)` | Enable scrolling with a scroll handle | | `with_menu(bool)` | Enable dropdown menu for tab selection | ### TabBar Variants | Method | Description | | ----------------------- | ------------------------------------ | | `with_variant(variant)` | Set the tab variant for all children | | `underline()` | Use underline variant | | `pill()` | Use pill variant | | `outline()` | Use outline variant | | `segmented()` | Use segmented variant | ### Tab | Method | Description | | ----------------------- | ---------------------------------------------- | | `new(label)` | Create a new tab with a label | | `empty()` | Create an empty tab | | `icon(icon)` | Create a tab with only an icon | | `id(id)` | Set custom ID for the tab | | `with_variant(variant)` | Set the tab variant | | `pill()` | Use pill variant | | `outline()` | Use outline variant | | `segmented()` | Use segmented variant | | `underline()` | Use underline variant | | `prefix(element)` | Add element before tab content | | `suffix(element)` | Add element after tab content | | `disabled(bool)` | Set disabled state | | `selected(bool)` | Set selected state (usually handled by TabBar) | | `on_click(fn)` | Custom click handler for individual tab | ### TabVariant ```rust pub enum TabVariant { Tab, // Default bordered tabs Outline, // Rounded outline tabs Pill, // Rounded pill-shaped tabs Segmented, // Segmented control style Underline, // Underline indicator tabs } ``` ### Styling Both `TabBar` and `Tab` implement `Sizable` trait: - `xsmall()` - Extra small size - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size ## Advanced Examples ### Custom Tab Content ```rust Tab::empty() .child( h_flex() .items_center() .gap_2() .child(IconName::Folder) .child("Documents") .child( div() .px_1() .py_0p5() .text_xs() .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .rounded(cx.theme().radius.half()) .child("12") ) ) ``` ### Tabs with State Management ```rust struct TabsWithContent { active_tab: usize, tab_contents: Vec, } impl TabsWithContent { fn render_tab_content(&self, cx: &mut Context) -> impl IntoElement { match self.active_tab { 0 => div().child("Account content"), 1 => div().child("Profile content"), 2 => div().child("Settings content"), _ => div().child("Unknown content"), } } } impl Render for TabsWithContent { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .child( TabBar::new("content-tabs") .selected_index(self.active_tab) .on_click(cx.listener(|view, index, _, cx| { view.active_tab = *index; cx.notify(); })) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Settings")) ) .child( div() .flex_1() .p_4() .child(self.render_tab_content(cx)) ) } } ``` ### Tabs with Close Buttons While the basic Tab component doesn't include closeable functionality, you can create closeable tabs using suffix elements: ```rust struct CloseableTabsView { tabs: Vec, active_tab: usize, } impl CloseableTabsView { fn close_tab(&mut self, index: usize, cx: &mut Context) { if self.tabs.len() > 1 { self.tabs.remove(index); if self.active_tab >= index && self.active_tab > 0 { self.active_tab -= 1; } cx.notify(); } } } impl Render for CloseableTabsView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { TabBar::new("closeable-tabs") .selected_index(self.active_tab) .on_click(cx.listener(|view, index, _, cx| { view.active_tab = *index; cx.notify(); })) .children( self.tabs .iter() .enumerate() .map(|(index, tab_name)| { Tab::new().label(tab_name.clone()) .suffix( Button::new(format!("close-{}", index)) .icon(IconName::X) .ghost() .xsmall() .on_click(cx.listener(move |view, _, _, cx| { view.close_tab(index, cx); })) ) }) ) } } ``` ## Notes - The `TabBar` manages the selection state of all child tabs - Individual tab `on_click` handlers are ignored when `TabBar.on_click` is set - Tabs automatically inherit the variant and size from their parent `TabBar` - The `with_menu` option adds a dropdown for tab selection when there are many tabs - Scrolling is automatically enabled when tabs overflow the container width - The dock system provides advanced closeable tab functionality for complex layouts ================================================ FILE: docs/docs/components/tag.md ================================================ --- title: Tag description: A short item that can be used to categorize or label content. --- # Tag A versatile tag component for categorizing and labeling content. Tags are compact visual indicators that help organize information and display metadata like categories, status, or properties. ## Import ```rust use gpui_component::tag::Tag; ``` ## Usage ### Basic Tags ```rust // Primary tag (default filled style) Tag::primary().child("Primary") // Secondary tag Tag::secondary().child("Secondary") // Status tags Tag::danger().child("Danger") Tag::success().child("Success") Tag::warning().child("Warning") Tag::info().child("Info") ``` ### Tag Variants ```rust // Semantic variants Tag::primary().child("Featured") Tag::secondary().child("Category") Tag::danger().child("Critical") Tag::success().child("Completed") Tag::warning().child("Pending") Tag::info().child("Information") ``` ### Outline Tags ```rust // Outline style variants Tag::primary().outline().child("Primary Outline") Tag::secondary().outline().child("Secondary Outline") Tag::danger().outline().child("Error Outline") Tag::success().outline().child("Success Outline") Tag::warning().outline().child("Warning Outline") Tag::info().outline().child("Info Outline") ``` ### Tag Sizes ```rust // Small size Tag::primary().small().child("Small Tag") // Medium size (default) Tag::primary().child("Medium Tag") ``` ### Custom Colors ```rust use gpui_component::ColorName; // Using predefined color names Tag::color(ColorName::Blue).child("Blue Tag") Tag::color(ColorName::Green).child("Green Tag") Tag::color(ColorName::Purple).child("Purple Tag") Tag::color(ColorName::Pink).child("Pink Tag") Tag::color(ColorName::Indigo).child("Indigo Tag") Tag::color(ColorName::Yellow).child("Yellow Tag") Tag::color(ColorName::Red).child("Red Tag") ``` ### Custom HSLA Colors ```rust use gpui::{hsla, Hsla}; // Custom colors with HSLA values let color = hsla(220.0 / 360.0, 0.8, 0.5, 1.0); let foreground = hsla(0.0, 0.0, 1.0, 1.0); let border = hsla(220.0 / 360.0, 0.8, 0.4, 1.0); Tag::custom(color, foreground, border).child("Custom Color") ``` ### Rounded Corners ```rust use gpui::px; // Fully rounded tags Tag::primary().rounded_full().child("Rounded Full") // Custom border radius Tag::primary().rounded(px(4.0)).child("Custom Radius") // Square corners Tag::primary().rounded(px(0.0)).child("Square Tag") ``` ### Combined Styles ```rust // Small tags with full rounding Tag::primary().small().rounded_full().child("Small Pill") Tag::success().small().rounded_full().child("Success Pill") // Outline tags with custom rounding Tag::warning().outline().rounded(px(2.0)).child("Custom Outline") // Color tags with outline style Tag::color(ColorName::Purple).outline().child("Purple Outline") ``` ## Tag Categories and Use Cases ### Status Tags ```rust // Task or item status Tag::success().child("Completed") Tag::warning().child("In Progress") Tag::danger().child("Failed") Tag::info().child("Pending Review") ``` ### Category Labels ```rust // Content categorization Tag::secondary().child("Technology") Tag::color(ColorName::Blue).child("Design") Tag::color(ColorName::Green).child("Development") Tag::color(ColorName::Purple).child("Marketing") ``` ### Priority Indicators ```rust // Priority levels Tag::danger().child("High Priority") Tag::warning().child("Medium Priority") Tag::secondary().child("Low Priority") ``` ### Feature Tags ```rust // Feature flags or attributes Tag::primary().small().child("New") Tag::success().small().child("Popular") Tag::info().small().child("Beta") Tag::warning().small().child("Limited") ``` ## API Reference ### Tag Creation Methods | Method | Description | | --------------------------- | ------------------------------------------ | | `primary()` | Create a primary tag (blue theme) | | `secondary()` | Create a secondary tag (gray theme) | | `danger()` | Create a danger tag (red theme) | | `success()` | Create a success tag (green theme) | | `warning()` | Create a warning tag (yellow/orange theme) | | `info()` | Create an info tag (blue theme) | | `color(ColorName)` | Create a tag with predefined color | | `custom(color, fg, border)` | Create a tag with custom HSLA colors | ### Style Methods | Method | Description | | ----------------- | -------------------------------------------- | | `outline()` | Apply outline style (transparent background) | | `rounded(radius)` | Set custom border radius | | `rounded_full()` | Apply full rounding (pill shape) | ### Size Methods (from Sizable trait) | Method | Description | | ----------------- | -------------------------------- | | `small()` | Small tag size (reduced padding) | | `with_size(size)` | Set custom size | ### Content Methods (from ParentElement trait) | Method | Description | | ---------------- | ---------------------------- | | `child(element)` | Add child content to the tag | ## Examples ### Tag Collections ```rust use gpui_component::{h_flex, v_flex}; // Horizontal tag group h_flex() .gap_2() .child(Tag::primary().child("React")) .child(Tag::success().child("TypeScript")) .child(Tag::info().child("Next.js")) .child(Tag::warning().child("Beta")) // Vertical tag stack v_flex() .gap_1() .child(Tag::danger().small().child("Critical")) .child(Tag::warning().small().child("Important")) .child(Tag::secondary().small().child("Normal")) ``` ### Status Dashboard Tags ```rust // System status indicators h_flex() .gap_3() .child( v_flex() .child("API Status:") .child(Tag::success().child("Operational")) ) .child( v_flex() .child("Database:") .child(Tag::warning().child("Maintenance")) ) .child( v_flex() .child("Cache:") .child(Tag::danger().child("Down")) ) ``` ### Interactive Tag Lists ```rust // Note: Event handling would require additional state management // Tags themselves are display components // Filter tags (would need click handlers) h_flex() .gap_2() .child(Tag::primary().small().child("All")) .child(Tag::secondary().outline().small().child("Active")) .child(Tag::secondary().outline().small().child("Completed")) .child(Tag::secondary().outline().small().child("Archived")) ``` ### Color-Coded Categories ```rust use gpui_component::ColorName; // Content type tags h_flex() .gap_2() .flex_wrap() .child(Tag::color(ColorName::Red).child("Bug")) .child(Tag::color(ColorName::Blue).child("Feature")) .child(Tag::color(ColorName::Green).child("Enhancement")) .child(Tag::color(ColorName::Purple).child("Documentation")) .child(Tag::color(ColorName::Yellow).child("Question")) .child(Tag::color(ColorName::Pink).child("Discussion")) ``` ### Pill-Style Tags ```rust // Skill tags with pill styling h_flex() .gap_2() .flex_wrap() .child(Tag::color(ColorName::Blue).rounded_full().small().child("Rust")) .child(Tag::color(ColorName::Green).rounded_full().small().child("JavaScript")) .child(Tag::color(ColorName::Purple).rounded_full().small().child("Python")) .child(Tag::color(ColorName::Red).rounded_full().small().child("Go")) ``` ## Behavior Notes - Tags automatically adjust their appearance based on the current theme - Outline tags maintain border visibility across different backgrounds - Small tags use reduced padding and border radius for compact layouts - Custom colors support both light and dark theme adaptations - Tags are display components and don't include built-in interaction handlers - Multiple tags can be combined in flex layouts for tag clouds or lists - Border radius automatically scales based on tag size unless explicitly overridden ## Design Guidelines ### When to Use Tags - **Categorization**: Group content by type, topic, or theme - **Status Indication**: Show state, progress, or health status - **Metadata Display**: Present attributes, properties, or classifications - **Filtering**: Visual indicators for active filters or selections - **Feature Flags**: Highlight new, beta, or special features ### Color Usage - **Semantic Colors**: Use danger (red) for errors, success (green) for completion, warning (yellow) for caution, info (blue) for information - **Category Colors**: Use the ColorName variants for content categorization where color coding helps with recognition - **Custom Colors**: Reserve for brand colors or specific design system requirements ### Size Guidelines - **Small Tags**: Use for compact layouts, metadata, or when space is limited - **Medium Tags**: Default size for most use cases, provides good readability and click targets - **Rounding**: Use `rounded_full()` for pill-style tags, custom `rounded()` for specific design requirements ================================================ FILE: docs/docs/components/title-bar.md ================================================ --- title: TitleBar description: A custom window title bar component with window controls and custom content support. --- # TitleBar TitleBar provides a customizable window title bar that can replace the default OS title bar. It includes platform-specific window controls (minimize, maximize, close) and supports custom content and styling. The component automatically adapts to different operating systems (macOS, Windows, Linux) with appropriate behaviors and visual styles. ## Import ```rust use gpui_component::TitleBar; ``` ## Usage ### Basic Title Bar ```rust TitleBar::new() .child(div().child("My Application")) ``` ### Title Bar with Custom Content ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_3() .child("App Name") .child(Badge::new().count(5)) ) .child( div() .flex() .items_center() .gap_2() .child(Button::new("settings").icon(IconName::Settings)) .child(Button::new("profile").icon(IconName::User)) ) ``` ### Title Bar with Menu Bar ```rust TitleBar::new() .child( div() .flex() .items_center() .child(AppMenuBar::new(window, cx)) ) .child( div() .flex() .items_center() .justify_end() .gap_2() .child(Button::new("github").icon(IconName::GitHub)) .child(Button::new("notifications").icon(IconName::Bell)) ) ``` ### Title Bar with Window Controls (Linux only) ```rust TitleBar::new() .on_close_window(|_, window, cx| { // Custom close behavior window.push_notification("Saving before close...", cx); // Perform cleanup window.remove_window(); }) .child(div().child("Custom Close Behavior")) ``` ### Styled Title Bar ```rust TitleBar::new() .bg(cx.theme().primary) .border_color(cx.theme().primary_border) .child( div() .text_color(cx.theme().primary_foreground) .child("Styled Title Bar") ) ``` ### Title Bar Options for Window ```rust use gpui::{WindowOptions, TitlebarOptions}; WindowOptions { titlebar: Some(TitleBar::title_bar_options()), ..Default::default() } ``` ## Platform Differences ### macOS - Uses native traffic light buttons (minimize, maximize, close) - Traffic light position is automatically set to `(9px, 9px)` - Double-click behavior calls `window.titlebar_double_click()` - Left padding accounts for traffic light buttons (80px) - Appears transparent by default ### Windows - Custom window control buttons with system integration - Uses `WindowControlArea` for proper window management - Control buttons have hover and active states - Fixed button width of 34px each - Left padding is 12px ### Linux - Custom window control buttons with manual event handling - Supports custom close window callback via `on_close_window()` - Double-click to maximize/restore window - Right-click shows window context menu - Window dragging supported in title bar area ## API Reference ### TitleBar | Method | Description | | --------------------- | ---------------------------------------- | | `new()` | Create a new title bar | | `child(element)` | Add child element to the title bar | | `on_close_window(fn)` | Custom close window handler (Linux only) | | `title_bar_options()` | Get default titlebar options for window | ### Window Configuration | Property | Description | | ------------------------ | --------------------------------------------------- | | `appears_transparent` | Make title bar transparent (default: true) | | `traffic_light_position` | Position of macOS traffic lights | | `title` | Window title (optional when using custom title bar) | ### Title Bar Element (Internal) The `TitleBarElement` provides window dragging functionality on Linux platforms. ### Constants | Constant | Value | Description | | ------------------------ | ------------------------------- | ------------------------- | | `TITLE_BAR_HEIGHT` | `34px` | Standard title bar height | | `TITLE_BAR_LEFT_PADDING` | `80px` (macOS), `12px` (others) | Left padding for content | ## Examples ### Application Title Bar ```rust use gpui_component::{TitleBar, button::Button, menu::AppMenuBar}; struct AppTitleBar { app_menu_bar: Entity, } impl Render for AppTitleBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { TitleBar::new() .child( div() .flex() .items_center() .child(self.app_menu_bar.clone()) ) .child( div() .flex() .items_center() .justify_end() .gap_2() .child( Button::new("settings") .ghost() .icon(IconName::Settings) ) .child( Button::new("help") .ghost() .icon(IconName::HelpCircle) ) ) } } ``` ### Title Bar with Breadcrumbs ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_2() .child("Home") .child(IconName::ChevronRight) .child("Documents") .child(IconName::ChevronRight) .child("Project") ) .child( div() .flex() .items_center() .gap_1() .child(Button::new("search").icon(IconName::Search).ghost()) .child(Button::new("more").icon(IconName::MoreHorizontal).ghost()) ) ``` ### Custom Themed Title Bar ```rust TitleBar::new() .h(px(40.)) // Custom height .bg(cx.theme().accent) .border_b_2() .border_color(cx.theme().accent_border) .child( div() .flex() .items_center() .text_color(cx.theme().accent_foreground) .font_weight_semibold() .child("Custom Theme App") ) ``` ### Title Bar with Status ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_3() .child("My Editor") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("● Unsaved changes") ) ) .child( div() .flex() .items_center() .gap_2() .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("Line 42, Col 12") ) .child( Button::new("sync") .small() .ghost() .icon(IconName::RotateCcw) .tooltip("Sync changes") ) ) ``` ### Minimal Title Bar ```rust TitleBar::new() .child( div() .text_center() .flex_1() .child("Document.txt") ) ``` ### Title Bar with Search ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_3() .child("File Explorer") .child( Input::new("search") .placeholder("Search files...") .w(px(200.)) .small() ) ) ``` ## Notes - The title bar automatically handles platform-specific styling and behavior - Window controls are only rendered on Windows and Linux platforms - The component integrates with GPUI's window management system - Custom styling should consider platform conventions - Window dragging is handled automatically in appropriate areas ================================================ FILE: docs/docs/components/toggle.md ================================================ --- title: Toggle description: A button-style toggle component for binary on/off or selected states. --- # Toggle A button-style toggle component that represents on/off or selected states. Unlike a traditional switch, toggles appear as buttons that can be pressed in or out. They're perfect for toolbar buttons, filter options, or any binary choice that benefits from a button-like appearance. ## Import ```rust use gpui_component::button::{Toggle, ToggleGroup}; ``` ## Usage ### Basic Toggle ```rust Toggle::new("toggle1"). .label("Toggle me") .checked(false) .on_click(|checked, _, _| { println!("Toggle is now: {}", checked); }) ``` Here, we can use `on_click` to handle toggle state changes. The callback receives the **new checked state** as a `bool`. ### Icon Toggle ```rust use gpui_component::IconName; Toggle::new("toggle2") .icon(IconName::Eye) .checked(true) .on_click(|checked, _, _| { println!("Visibility: {}", if *checked { "shown" } else { "hidden" }); }) ``` ### Controlled Toggle ```rust struct MyView { is_active: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Toggle::new("active") .label("Active") .checked(self.is_active) .on_click(cx.listener(|view, checked, _, cx| { view.is_active = *checked; cx.notify(); })) } } ``` ### Toggle Variants ```rust // Ghost toggle (default) Toggle::new("ghost-toggle") .ghost() .label("Ghost") // Outline toggle Toggle::new("outline-toggle") .outline() .label("Outline") ``` ### Different Sizes ```rust // Extra small Toggle::new("xs-toggle") .icon(IconName::Star) .xsmall() // Small Toggle::new("small-toggle") .label("Small") .small() // Medium (default) Toggle::new("medium-toggle") .label("Medium") // Large Toggle::new("large-toggle") .label("Large") .large() ``` ### Disabled State ```rust // Disabled unchecked Toggle::new("disabled-toggle") .label("Disabled") .disabled(true) .checked(false) // Disabled checked Toggle::new("disabled-checked-toggle") .label("Selected (Disabled)") .disabled(true) .checked(true) ``` ## Toggle vs Switch | Feature | Toggle | Switch | | ---------------------- | ------------------------------------------- | ----------------------------------------- | | **Appearance** | Button-like, can be pressed in/out | Traditional switch with sliding indicator | | **Use Cases** | Toolbar buttons, filters, binary options | Settings, preferences, on/off states | | **Visual Style** | Rectangular button shape | Rounded switch track with thumb | | **State Indication** | Background color change, pressed appearance | Position of sliding thumb | | **Multiple Selection** | Supports groups with multiple selection | Individual switches only | **Use Toggle when you want:** - Button-like appearance for binary states - Grouping multiple related options - Toolbar or filter interfaces - Options that feel like "selections" rather than "settings" **Use Switch when you want:** - Traditional on/off control appearance - Settings or preferences interface - Clear visual indication of state with sliding animation - Individual boolean controls ## Integration with ToggleGroup Toggle buttons can be grouped together using `ToggleGroup` for related options: ### Basic Toggle Group ```rust ToggleGroup::new("filter-group") .child(Toggle::new(0).icon(IconName::Bell)) .child(Toggle::new(1).icon(IconName::Bot)) .child(Toggle::new(2).icon(IconName::Inbox)) .child(Toggle::new(3).label("Other")) .on_click(|checkeds, _, _| { println!("Selected toggles: {:?}", checkeds); }) ``` The `on_click` callback receives a `Vec` representing the **new checked state** of each toggle in the group. ### Toggle Group with Controlled State ```rust struct FilterView { notifications: bool, bots: bool, inbox: bool, other: bool, } impl Render for FilterView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { ToggleGroup::new("filters") .child(Toggle::new(0).icon(IconName::Bell).checked(self.notifications)) .child(Toggle::new(1).icon(IconName::Bot).checked(self.bots)) .child(Toggle::new(2).icon(IconName::Inbox).checked(self.inbox)) .child(Toggle::new(3).label("Other").checked(self.other)) .on_click(cx.listener(|view, checkeds, _, cx| { view.notifications = checkeds[0]; view.bots = checkeds[1]; view.inbox = checkeds[2]; view.other = checkeds[3]; cx.notify(); })) } } ``` ### Toggle Group Variants and Sizes ```rust // Outline variant, small size ToggleGroup::new("compact-filters") .outline() .small() .child(Toggle::new(0).icon(IconName::Filter)) .child(Toggle::new(1).icon(IconName::Sort)) .child(Toggle::new(2).icon(IconName::Search)) // Ghost variant (default), extra small ToggleGroup::new("mini-toolbar") .xsmall() .child(Toggle::new(0).icon(IconName::Bold)) .child(Toggle::new(1).icon(IconName::Italic)) .child(Toggle::new(2).icon(IconName::Underline)) ``` ## Event Handling ### Individual Toggle Events ```rust Toggle::new("subscribe-toggle") .label("Subscribe") .on_click(|checked, window, cx| { if *checked { // Handle subscription logic println!("Subscribed!"); } else { // Handle unsubscription logic println!("Unsubscribed!"); } }) ``` ## Examples ### Toolbar with Toggle Buttons ```rust struct EditorToolbar { bold: bool, italic: bool, underline: bool, strikethrough: bool, } h_flex() .gap_1() .p_2() .bg(cx.theme().background) .border_1() .border_color(cx.theme().border) .child( ToggleGroup::new("formatting") .small() .child(Toggle::new(0).icon(IconName::Bold).checked(self.bold)) .child(Toggle::new(1).icon(IconName::Italic).checked(self.italic)) .child(Toggle::new(2).icon(IconName::Underline).checked(self.underline)) .child(Toggle::new(3).icon(IconName::Strikethrough).checked(self.strikethrough)) .on_click(cx.listener(|view, states, _, cx| { view.bold = states[0]; view.italic = states[1]; view.underline = states[2]; view.strikethrough = states[3]; cx.notify(); })) ) ``` ### Filter Interface ```rust struct FilterPanel { show_completed: bool, show_pending: bool, show_cancelled: bool, show_urgent: bool, } v_flex() .gap_3() .p_4() .child(Label::new("Filter by status")) .child( ToggleGroup::new("status-filters") .outline() .child(Toggle::new(0).label("Completed").checked(self.show_completed)) .child(Toggle::new(1).label("Pending").checked(self.show_pending)) .child(Toggle::new(2).label("Cancelled").checked(self.show_cancelled)) .on_click(cx.listener(|view, states, _, cx| { view.show_completed = states[0]; view.show_pending = states[1]; view.show_cancelled = states[2]; cx.notify(); })) ) .child( Toggle::new("urgent-filter") .label("Show urgent only") .checked(self.show_urgent) .on_click(cx.listener(|view, checked, _, cx| { view.show_urgent = *checked; cx.notify(); })) ) ``` ### Settings with Individual Toggles ```rust struct NotificationSettings { email_notifications: bool, push_notifications: bool, marketing_emails: bool, } v_flex() .gap_4() .child( h_flex() .items_center() .justify_between() .child( v_flex() .child(Label::new("Email notifications")) .child( Label::new("Receive notifications via email") .text_color(cx.theme().muted_foreground) .text_sm() ) ) .child( Toggle::new("email-notifications") .icon(IconName::Mail) .checked(self.email_notifications) .on_click(cx.listener(|view, checked, _, cx| { view.email_notifications = *checked; cx.notify(); })) ) ) .child( h_flex() .items_center() .justify_between() .child(Label::new("Push notifications")) .child( Toggle::new("push-notifications") .icon(IconName::Bell) .checked(self.push_notifications) .on_click(cx.listener(|view, checked, _, cx| { view.push_notifications = *checked; cx.notify(); })) ) ) ``` ### Multi-select Options ```rust struct SelectionView { selected_categories: Vec, } impl SelectionView { fn categories() -> Vec<&'static str> { vec!["Technology", "Design", "Business", "Science", "Art"] } } v_flex() .gap_3() .child(Label::new("Select categories of interest")) .child( ToggleGroup::new("categories") .children( Self::categories() .into_iter() .enumerate() .map(|(i, category)| { Toggle::new(i) .label(category) .checked(self.selected_categories.get(i).copied().unwrap_or(false)) }) ) .on_click(cx.listener(|view, states, _, cx| { view.selected_categories = states.clone(); cx.notify(); })) ) ``` ## Best Practices 1. **Use meaningful labels**: Choose clear, descriptive text for toggle labels 2. **Group related options**: Use ToggleGroup for logically related binary choices 3. **Provide visual feedback**: The checked state should be clearly distinguishable 4. **Consider context**: Use toggles for options that feel like "selections" rather than "settings" 5. **Maintain state consistency**: Ensure toggle state reflects the actual application state 6. **Accessible labels**: Provide tooltips or ARIA labels for icon-only toggles ================================================ FILE: docs/docs/components/tooltip.md ================================================ --- title: Tooltip description: Display helpful information on hover or focus, with support for keyboard shortcuts and custom content. --- # Tooltip A versatile tooltip component that displays helpful information when hovering over or focusing on elements. Supports text content, custom elements, keyboard shortcuts, different trigger methods, and positioning options. ## Import ```rust use gpui_component::tooltip::Tooltip; ``` ## Usage ### Basic Tooltip with Text ```rust // Simple text tooltip div() .child("Hover me") .id("basic-tooltip") .tooltip(|window, cx| { Tooltip::new("This is a helpful tooltip").build(window, cx) }) ``` ### Button with Tooltip ```rust Button::new("save-btn") .label("Save") .tooltip("Save the current document") ``` ### Tooltip with Action/Keybinding ```rust actions!(my_actions, [SaveDocument]); Button::new("save-btn") .label("Save") .tooltip_with_action( "Save the current document", &SaveDocument, Some("MyContext") ) ``` ### Custom Element Tooltip ```rust div() .child("Hover for rich content") .id("rich-tooltip") .tooltip(|window, cx| { Tooltip::element(|_, cx| { h_flex() .gap_x_1() .child(IconName::Info) .child( div() .child("Muted Text") .text_color(cx.theme().muted_foreground) ) .child( div() .child("Danger Text") .text_color(cx.theme().danger) ) .child(IconName::ArrowUp) }) .build(window, cx) }) ``` ### Tooltip with Manual Keybinding ```rust div() .child("Custom keybinding") .id("custom-kb") .tooltip(|window, cx| { Tooltip::new("Delete item") .key_binding(Some(Kbd::new("Delete"))) .build(window, cx) }) ``` ## Advanced Usage ### Components with Built-in Tooltip Support Many components have built-in tooltip methods: ```rust // Button Button::new("btn") .label("Click me") .tooltip("This button performs an action") // Switch Switch::new("toggle") .label("Enable notifications") .tooltip("Toggle push notifications on/off") // Checkbox Checkbox::new("check") .label("Remember me") .tooltip("Keep me logged in for 30 days") // Radio Radio::new("option") .label("Option 1") .tooltip("Select this option to enable feature X") ``` ### Complex Tooltip Content ```rust div() .child("Hover for details") .id("complex-tooltip") .tooltip(|window, cx| { Tooltip::element(|_, cx| { v_flex() .gap_2() .child( h_flex() .gap_1() .child(IconName::User) .child("User Information") .text_sm() .font_semibold() ) .child( div() .child("Last login: 2 hours ago") .text_xs() .text_color(cx.theme().muted_foreground) ) .child( div() .child("Status: Active") .text_xs() .text_color(cx.theme().success) ) }) .build(window, cx) }) ``` ### Tooltip in Form Elements ```rust v_flex() .gap_4() .child( Input::new("email") .placeholder("Enter your email") .tooltip("We'll never share your email address") ) .child( Input::new("password") .input_type(InputType::Password) .placeholder("Password") .tooltip("Must be at least 8 characters with special characters") ) ``` ## API Reference ### Tooltip | Method | Description | | ------------------------- | -------------------------------------------- | | `new(text)` | Create a tooltip with text content | | `element(builder)` | Create a tooltip with custom element content | | `action(action, context)` | Set action to display keybinding information | | `key_binding(kbd)` | Set manual keybinding information | | `build(window, cx)` | Build and return the tooltip as AnyView | ### Built-in Tooltip Methods Components with tooltip support typically provide these methods: | Method | Description | | -------------------------------------------- | --------------------------------------- | | `tooltip(text)` | Add simple text tooltip | | `tooltip_with_action(text, action, context)` | Add tooltip with action keybinding | | `tooltip(closure)` | Add custom tooltip with builder closure | ### Tooltip Styling The tooltip automatically applies theme-appropriate styling: - Background: `theme.popover` - Text color: `theme.popover_foreground` - Border: `theme.border` - Shadow: Medium drop shadow - Border radius: 6px - Font: System UI font You can apply additional styling using the `Styled` trait: ```rust Tooltip::new("Custom styled tooltip") .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .build(window, cx) ``` ## Examples ### Toolbar with Tooltips ```rust h_flex() .gap_1() .child( Button::new("new") .icon(IconName::Plus) .tooltip_with_action("Create new file", &NewFile, Some("Editor")) ) .child( Button::new("open") .icon(IconName::FolderOpen) .tooltip_with_action("Open file", &OpenFile, Some("Editor")) ) .child( Button::new("save") .icon(IconName::Save) .tooltip_with_action("Save file", &SaveFile, Some("Editor")) ) ``` ### Status Indicators with Tooltips ```rust h_flex() .gap_2() .child( div() .size_3() .rounded_full() .bg(cx.theme().success) .tooltip(|window, cx| { Tooltip::new("Connected to server").build(window, cx) }) ) .child( div() .size_3() .rounded_full() .bg(cx.theme().warning) .tooltip(|window, cx| { Tooltip::new("Limited connectivity").build(window, cx) }) ) ``` ### Interactive Elements with Rich Tooltips ```rust v_flex() .gap_3() .child( div() .p_2() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .child("File: document.txt") .id("file-item") .tooltip(|window, cx| { Tooltip::element(|_, cx| { v_flex() .gap_1() .child( h_flex() .gap_2() .child(IconName::File) .child("document.txt") .text_sm() .font_medium() ) .child( div() .child("Size: 2.4 KB") .text_xs() .text_color(cx.theme().muted_foreground) ) .child( div() .child("Modified: 2 hours ago") .text_xs() .text_color(cx.theme().muted_foreground) ) .child( h_flex() .gap_1() .child(Kbd::new("Enter")) .child("to open") .text_xs() .text_color(cx.theme().muted_foreground) ) }) .build(window, cx) }) ) ``` ### Form Validation with Tooltips ```rust struct FormView { email_error: Option, password_error: Option, } v_flex() .gap_4() .child( Input::new("email") .placeholder("Email address") .when_some(self.email_error.clone(), |this, error| { this.tooltip(move |window, cx| { Tooltip::element(|_, cx| { h_flex() .gap_1() .child(IconName::AlertCircle) .child(error.clone()) .text_color(cx.theme().destructive) }) .build(window, cx) }) }) ) ``` ## Best Practices ### Content Guidelines - **Be concise**: Keep tooltip text short and to the point - **Be helpful**: Provide additional context, not redundant information - **Use proper tone**: Match your application's voice and tone - **Avoid critical info**: Don't put essential information only in tooltips ### Usage Guidelines - **Progressive disclosure**: Use tooltips for additional context, not primary information - **Consistency**: Use consistent tooltip patterns throughout your application - **Performance**: Avoid complex content in frequently triggered tooltips - **Testing**: Test tooltips with both mouse and keyboard interaction ### Examples of Good Tooltip Content ```rust // Good: Provides helpful context Button::new("delete") .icon(IconName::Trash) .tooltip("Delete this item permanently") // Good: Explains abbreviation div() .child("CPU: 45%") .tooltip("Central Processing Unit usage") // Good: Describes action with keybinding Button::new("undo") .icon(IconName::Undo) .tooltip_with_action("Undo last action", &Undo, Some("Editor")) ``` ### Examples to Avoid ```rust // Avoid: Redundant information Button::new("save") .label("Save") .tooltip("Save") // Doesn't add value // Avoid: Critical information Button::new("delete") .tooltip("This will permanently delete all your files") // Too important for tooltip only ``` ================================================ FILE: docs/docs/components/tree.md ================================================ --- title: Tree description: A hierarchical tree view component for displaying and navigating tree-structured data. --- # Tree A versatile tree component for displaying hierarchical data with expand/collapse functionality, keyboard navigation, and custom item rendering. Perfect for file explorers, navigation menus, or any nested data structure. ## Import ```rust use gpui_component::tree::{tree, TreeState, TreeItem, TreeEntry}; ``` ## Usage ### Basic Tree ```rust // Create tree state let tree_state = cx.new(|cx| { TreeState::new(cx).items(vec![ TreeItem::new("src", "src") .expanded(true) .child(TreeItem::new("src/lib.rs", "lib.rs")) .child(TreeItem::new("src/main.rs", "main.rs")), TreeItem::new("Cargo.toml", "Cargo.toml"), TreeItem::new("README.md", "README.md"), ]) }); // Render tree tree(&tree_state, |ix, entry, selected, window, cx| { ListItem::new(ix) .child( h_flex() .gap_2() .child(entry.item().label.clone()) ) }) ``` ### File Tree with Icons ```rust use gpui_component::{ListItem, IconName, h_flex}; tree(&tree_state, |ix, entry, selected, window, cx| { let item = entry.item(); let icon = if !entry.is_folder() { IconName::File } else if entry.is_expanded() { IconName::FolderOpen } else { IconName::Folder }; ListItem::new(ix) .selected(selected) .pl(px(16.) * entry.depth() + px(12.)) // Indent based on depth .child( h_flex() .gap_2() .child(icon) .child(item.label.clone()) ) .on_click(cx.listener(move |_, _, _, _| { // Handle item click })) }) ``` ### Dynamic Tree Loading ```rust impl MyView { fn load_files(&mut self, path: PathBuf, cx: &mut Context) { let tree_state = self.tree_state.clone(); cx.spawn(async move |cx| { let items = build_file_items(&path).await; tree_state.update(cx, |state, cx| { state.set_items(items, cx); }) }).detach(); } } fn build_file_items(path: &Path) -> Vec { let mut items = Vec::new(); if let Ok(entries) = std::fs::read_dir(path) { for entry in entries.flatten() { let path = entry.path(); let name = path.file_name() .and_then(|n| n.to_str()) .unwrap_or("Unknown") .to_string(); if path.is_dir() { let children = build_file_items(&path); items.push(TreeItem::new(path.to_string_lossy(), name) .children(children)); } else { items.push(TreeItem::new(path.to_string_lossy(), name)); } } } items } ``` ### Tree with Selection Handling ```rust struct MyTreeView { tree_state: Entity, selected_item: Option, } impl MyTreeView { fn handle_selection(&mut self, item: TreeItem, cx: &mut Context) { self.selected_item = Some(item.clone()); println!("Selected: {} ({})", item.label, item.id); cx.notify(); } } // In render method tree(&self.tree_state, { let view = cx.entity(); move |ix, entry, selected, window, cx| { view.update(cx, |this, cx| { ListItem::new(ix) .selected(selected) .child(entry.item().label.clone()) .on_click(cx.listener({ let item = entry.item().clone(); move |this, _, _, cx| { this.handle_selection(item.clone(), cx); } })) }) } }) ``` ### Disabled Items ```rust TreeItem::new("protected", "Protected Folder") .disabled(true) .child(TreeItem::new("secret.txt", "secret.txt")) ``` ### Programmatic Tree Control ```rust // Get current selection if let Some(entry) = tree_state.read(cx).selected_entry() { println!("Current selection: {}", entry.item().label); } // Set selection programmatically (by selected_index) tree_state.update(cx, |state, cx| { state.set_selected_index(Some(2), cx); // Select third item }); // Set selection programmatically (by tree item) tree_state.update(cx, |state, cx| { state.set_selected_item(Some(item), cx); // Select third item }); // Scroll to specific item tree_state.update(cx, |state, _| { state.scroll_to_item(5, gpui::ScrollStrategy::Center); }); // Clear selection (by selected_index) tree_state.update(cx, |state, cx| { state.set_selected_index(None, cx); }); // Clear selection (by tree item) tree_state.update(cx, |state, cx| { state.set_selected_item(None, cx); }); ``` ## API Reference ### TreeState | Method | Description | |--------------------------------|----------------------------------| | `new(cx)` | Create a new tree state | | `items(items)` | Set initial tree items | | `set_items(items, cx)` | Update tree items and notify | | `selected_index()` | Get currently selected index | | `set_selected_index(ix, cx)` | Set selected index | | `set_selected_item(item, cx)` | Set selected by tree item | | `selected_item(item, cx)` | Get currently selected tree item | | `selected_entry()` | Get currently selected entry | | `scroll_to_item(ix, strategy)` | Scroll to specific item | ### TreeItem | Method | Description | | ----------------- | -------------------------------------- | | `new(id, label)` | Create new tree item with ID and label | | `child(item)` | Add single child item | | `children(items)` | Add multiple child items | | `expanded(bool)` | Set expanded state | | `disabled(bool)` | Set disabled state | | `is_folder()` | Check if item has children | | `is_expanded()` | Check if item is expanded | | `is_disabled()` | Check if item is disabled | ### TreeEntry | Method | Description | | --------------- | --------------------------- | | `item()` | Get the source TreeItem | | `depth()` | Get item depth in tree | | `is_folder()` | Check if entry has children | | `is_expanded()` | Check if entry is expanded | | `is_disabled()` | Check if entry is disabled | ### tree() Function | Parameter | Description | | ------------- | ------------------------------------- | | `state` | `Entity` for managing tree | | `render_item` | Closure for rendering each item | #### Render Item Closure ```rust Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem ``` - `usize`: Item index in flattened tree - `&TreeEntry`: Tree entry with item and metadata - `bool`: Whether item is currently selected - `&mut Window`: Current window context - `&mut App`: Application context - Returns: `ListItem` for rendering ## Examples ### Lazy Loading Tree ```rust struct LazyTreeView { tree_state: Entity, loaded_paths: HashSet, } impl LazyTreeView { fn load_children(&mut self, item_id: &str, cx: &mut Context) { if self.loaded_paths.contains(item_id) { return; } let path = PathBuf::from(item_id); if path.is_dir() { let tree_state = self.tree_state.clone(); let item_id = item_id.to_string(); cx.spawn(async move |cx| { let children = load_directory_children(&path).await; tree_state.update(cx, |state, cx| { // Update specific item with loaded children state.update_item_children(&item_id, children, cx); }) }).detach(); self.loaded_paths.insert(item_id.to_string()); } } } ``` ### Search and Filter ```rust struct SearchableTree { tree_state: Entity, original_items: Vec, search_query: String, } impl SearchableTree { fn filter_tree(&mut self, query: &str, cx: &mut Context) { self.search_query = query.to_string(); let filtered_items = if query.is_empty() { self.original_items.clone() } else { filter_tree_items(&self.original_items, query) }; self.tree_state.update(cx, |state, cx| { state.set_items(filtered_items, cx); }); } } fn filter_tree_items(items: &[TreeItem], query: &str) -> Vec { items.iter() .filter_map(|item| { if item.label.to_lowercase().contains(&query.to_lowercase()) { Some(item.clone().expanded(true)) // Auto-expand matches } else { // Check if any children match let filtered_children = filter_tree_items(&item.children, query); if !filtered_children.is_empty() { Some(item.clone() .children(filtered_children) .expanded(true)) } else { None } } }) .collect() } ``` ### Multi-Select Tree ```rust struct MultiSelectTree { tree_state: Entity, selected_items: HashSet, } impl MultiSelectTree { fn toggle_selection(&mut self, item_id: &str, cx: &mut Context) { if self.selected_items.contains(item_id) { self.selected_items.remove(item_id); } else { self.selected_items.insert(item_id.to_string()); } cx.notify(); } fn is_selected(&self, item_id: &str) -> bool { self.selected_items.contains(item_id) } } // In render method tree(&self.tree_state, { let view = cx.entity(); move |ix, entry, _selected, window, cx| { view.update(cx, |this, cx| { let item = entry.item(); let is_multi_selected = this.is_selected(&item.id); ListItem::new(ix) .selected(is_multi_selected) .child( h_flex() .gap_2() .child(checkbox().checked(is_multi_selected)) .child(item.label.clone()) ) .on_click(cx.listener({ let item_id = item.id.clone(); move |this, _, _, cx| { this.toggle_selection(&item_id, cx); } })) }) } }) ``` ## Keyboard Navigation The Tree component supports comprehensive keyboard navigation: | Key | Action | | ------- | ----------------------------------------- | | `↑` | Select previous item | | `↓` | Select next item | | `←` | Collapse current folder or move to parent | | `→` | Expand current folder | | `Enter` | Toggle expand/collapse for folders | | `Space` | Custom action (configurable) | ```rust // Custom keyboard handling tree(&tree_state) .key_context("MyTree") .on_action(cx.listener(|this, action: &MyCustomAction, _, cx| { // Handle custom actions })) ``` ================================================ FILE: docs/docs/components/virtual-list.md ================================================ --- title: VirtualList description: High-performance virtualized list component for rendering large datasets with variable item sizes. --- # VirtualList VirtualList is a high-performance component designed for efficiently rendering large datasets by only rendering visible items. Unlike uniform lists, VirtualList supports variable item sizes, making it perfect for complex layouts like tables with different row heights or dynamic content. ## Import ```rust use gpui_component::{ v_virtual_list, h_virtual_list, VirtualListScrollHandle, scroll::{Scrollbar, ScrollbarState, ScrollbarAxis}, }; use std::rc::Rc; use gpui::{px, size, ScrollStrategy, Size, Pixels}; ``` ## Usage ### Basic Vertical Virtual List ```rust use std::rc::Rc; use gpui::{px, size, Size, Pixels}; pub struct ListViewExample { items: Vec, item_sizes: Rc>>, scroll_handle: VirtualListScrollHandle, } impl ListViewExample { fn new(cx: &mut Context) -> Self { let items = (0..5000).map(|i| format!("Item {}", i)).collect::>(); let item_sizes = Rc::new(items.iter().map(|_| size(px(200.), px(30.))).collect()); Self { items, item_sizes, scroll_handle: VirtualListScrollHandle::new(), } } } impl Render for ListViewExample { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_virtual_list( cx.entity().clone(), "my-list", self.item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { div() .h(px(30.)) .w_full() .bg(cx.theme().secondary) .child(format!("Item {}", ix)) }) .collect() }, ) .track_scroll(&self.scroll_handle) } } ``` ### Horizontal Virtual List ```rust h_virtual_list( cx.entity().clone(), "horizontal-list", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { div() .w(px(120.)) // Width is used for horizontal lists .h_full() .bg(cx.theme().accent) .child(format!("Card {}", ix)) }) .collect() }, ) .track_scroll(&scroll_handle) ``` ### Variable Item Sizes VirtualList excels at handling items with different sizes: ```rust let item_sizes = Rc::new( (0..1000) .map(|i| { // Different heights based on index let height = if i % 5 == 0 { px(60.) // Header items are taller } else if i % 3 == 0 { px(45.) // Some items are medium } else { px(30.) // Regular items }; size(px(300.), height) }) .collect::>() ); v_virtual_list( cx.entity().clone(), "variable-list", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { let content = if ix % 5 == 0 { format!("Header {}", ix / 5) } else { format!("Item {}", ix) }; let bg_color = if ix % 5 == 0 { cx.theme().accent } else { cx.theme().secondary }; div() .w_full() .h(item_sizes[ix].height) .bg(bg_color) .flex() .items_center() .px_4() .child(content) }) .collect() }, ) ``` ### Table-like Layout with Multiple Columns VirtualList can render complex layouts like tables: ```rust v_virtual_list( cx.entity().clone(), "table-list", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|row_ix| { h_flex() .w_full() .h(px(40.)) .border_b_1() .border_color(cx.theme().border) .children( // Multiple columns per row (0..5).map(|col_ix| { div() .flex_1() .h_full() .px_3() .flex() .items_center() .child(format!("R{}C{}", row_ix, col_ix)) }) ) }) .collect() }, ) ``` ## Scroll Handling ### Basic Scroll Control ```rust pub struct ScrollableList { scroll_handle: VirtualListScrollHandle, scroll_state: ScrollbarState, } impl Render for ScrollableList { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .relative() .size_full() .child( v_virtual_list(/* ... */) .track_scroll(&self.scroll_handle) .p_4() .border_1() .border_color(cx.theme().border) ) .child( // Add scrollbars div() .absolute() .top_0() .left_0() .right_0() .bottom_0() .child( Scrollbar::both(&self.scroll_state, &self.scroll_handle) .axis(ScrollbarAxis::Vertical) ) ) } } ``` ### Programmatic Scrolling ```rust impl ScrollableList { // Scroll to specific item fn scroll_to_item(&self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Top); } // Center item in view fn center_item(&self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Center); } // Scroll to bottom fn scroll_to_bottom(&self) { self.scroll_handle.scroll_to_bottom(); } // Get current scroll position fn get_scroll_offset(&self) -> Point { self.scroll_handle.offset() } // Set scroll position manually fn set_scroll_position(&self, offset: Point) { self.scroll_handle.set_offset(offset); } } ``` ### Both Axis Scrolling For content that scrolls in both directions: ```rust v_virtual_list( cx.entity().clone(), "both-axis", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { // Wide content that requires horizontal scrolling h_flex() .gap_2() .children((0..20).map(|col| { div() .min_w(px(100.)) .h(px(30.)) .bg(cx.theme().secondary) .child(format!("R{}C{}", ix, col)) })) }) .collect() }, ) .track_scroll(&scroll_handle) .child( Scrollbar::both(&scroll_state, &scroll_handle) .axis(ScrollbarAxis::Both) ) ``` ## Performance Optimization ### Efficient Item Rendering Only visible items are rendered, making VirtualList highly performant: ```rust // The render function is only called for visible items v_virtual_list( cx.entity().clone(), "efficient-list", item_sizes.clone(), |view, visible_range, _, cx| { // visible_range contains only the items currently visible // This typically contains 10-20 items, not all 10,000 println!("Rendering {} items out of {}", visible_range.len(), view.total_items); visible_range .map(|ix| { // Complex rendering logic here // Only executed for visible items expensive_item_renderer(ix, cx) }) .collect() }, ) ``` ### Memory Management VirtualList automatically manages memory by: - Only rendering visible items - Reusing rendered elements when scrolling - Calculating precise visible ranges ```rust // Large dataset - only visible items use memory let large_dataset = (0..1_000_000).map(|i| format!("Item {}", i)).collect(); // Memory usage remains constant regardless of dataset size v_virtual_list(/* render only visible items */) ``` ### Variable Heights with Caching For dynamic content with calculated heights: ```rust struct DynamicItem { content: String, calculated_height: Option, } impl MyView { fn calculate_item_size(&mut self, ix: usize) -> Size { if let Some(height) = self.items[ix].calculated_height { return size(px(300.), height); } // Calculate height based on content let content_lines = self.items[ix].content.lines().count(); let height = px(20. + content_lines as f32 * 16.); // Cache the calculated height self.items[ix].calculated_height = Some(height); size(px(300.), height) } } ``` ## Examples ### File Explorer with Virtual Scrolling ```rust pub struct FileExplorer { files: Vec, item_sizes: Rc>>, scroll_handle: VirtualListScrollHandle, selected_index: Option, } impl FileExplorer { fn calculate_item_heights(&mut self) { let sizes = self.files.iter().map(|file| { // Different heights for different file types let height = match file.file_type { FileType::Directory => px(40.), FileType::Image => px(60.), // Larger for thumbnails FileType::Document => px(35.), _ => px(30.), }; size(px(400.), height) }).collect(); self.item_sizes = Rc::new(sizes); } } impl Render for FileExplorer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_virtual_list( cx.entity().clone(), "file-list", self.item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { let file = &view.files[ix]; let is_selected = view.selected_index == Some(ix); div() .w_full() .h(view.item_sizes[ix].height) .px_3() .py_1() .flex() .items_center() .gap_2() .bg(if is_selected { cx.theme().accent } else { Color::transparent() }) .hover(|style| style.bg(cx.theme().secondary_hover)) .child(file_icon(&file.file_type)) .child(file.name.clone()) .child( div() .flex_1() .text_right() .text_xs() .text_color(cx.theme().muted_foreground) .child(format_file_size(file.size)) ) .on_click(cx.listener(move |view, _, _, cx| { view.selected_index = Some(ix); cx.notify(); })) }) .collect() }, ) .track_scroll(&self.scroll_handle) } } ``` ### Chat Messages with Auto-scroll ```rust pub struct ChatWindow { messages: Vec, scroll_handle: VirtualListScrollHandle, auto_scroll: bool, } impl ChatWindow { fn add_message(&mut self, message: ChatMessage, cx: &mut Context) { self.messages.push(message); // Recalculate item sizes self.update_item_sizes(); if self.auto_scroll { // Scroll to bottom for new messages self.scroll_handle.scroll_to_bottom(); } cx.notify(); } fn update_item_sizes(&mut self) { let sizes = self.messages.iter().map(|msg| { // Calculate height based on message content let lines = msg.content.lines().count().max(1); let height = px(40. + (lines.saturating_sub(1)) as f32 * 16.); size(px(350.), height) }).collect(); self.item_sizes = Rc::new(sizes); } } impl Render for ChatWindow { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .child( v_virtual_list( cx.entity().clone(), "chat-messages", self.item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { let msg = &view.messages[ix]; div() .w_full() .px_4() .py_2() .child( v_flex() .gap_1() .child( h_flex() .justify_between() .child( div() .text_sm() .font_weight(FontWeight::SEMIBOLD) .child(msg.author.clone()) ) .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child(format_timestamp(msg.timestamp)) ) ) .child( div() .text_sm() .child(msg.content.clone()) ) ) }) .collect() }, ) .track_scroll(&self.scroll_handle) .flex_1() ) .child( // Chat input at bottom div() .w_full() .h(px(60.)) .border_t_1() .border_color(cx.theme().border) .child("Chat input here...") ) } } ``` ### Data Grid with Fixed Headers ```rust pub struct DataGrid { headers: Vec, data: Vec>, column_widths: Vec, scroll_handle: VirtualListScrollHandle, } impl Render for DataGrid { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .child( // Fixed header h_flex() .w_full() .h(px(40.)) .bg(cx.theme().secondary) .border_b_1() .border_color(cx.theme().border) .children( self.headers.iter().zip(&self.column_widths).map(|(header, &width)| { div() .w(width) .h_full() .px_3() .flex() .items_center() .font_weight(FontWeight::SEMIBOLD) .child(header.clone()) }) ) ) .child( // Virtual list for data rows v_virtual_list( cx.entity().clone(), "data-rows", Rc::new(vec![size(px(800.), px(32.)); self.data.len()]), |view, visible_range, _, cx| { visible_range .map(|row_ix| { h_flex() .w_full() .h(px(32.)) .border_b_1() .border_color(cx.theme().border.opacity(0.5)) .children( view.data[row_ix].iter().zip(&view.column_widths).map(|(cell, &width)| { div() .w(width) .h_full() .px_3() .flex() .items_center() .child(cell.clone()) }) ) }) .collect() }, ) .track_scroll(&self.scroll_handle) .flex_1() ) } } ``` ## Best Practices 1. **Item Sizing**: Pre-calculate item sizes when possible for best performance 2. **Memory Management**: Use VirtualList for any list with >50 items 3. **Scroll Performance**: Avoid heavy computations in render functions 4. **State Management**: Keep item state separate from rendering logic 5. **Error Handling**: Handle edge cases like empty lists gracefully 6. **Testing**: Test with various data sizes and scroll positions ## Performance Tips 1. **Pre-calculate Sizes**: Calculate item sizes upfront rather than during render 2. **Minimize Re-renders**: Use stable item keys and avoid recreating render functions 3. **Batch Updates**: Group multiple data changes together 4. **Efficient Rendering**: Keep item render functions lightweight 5. **Memory Monitoring**: Monitor memory usage with very large datasets ================================================ FILE: docs/docs/context.md ================================================ --- title: Context description: Learn about the Window and Context in GPUI. order: -4 --- The [Window], [App], [Context] and [Entity] are most important things in GPUI, it appears everywhere. - [Window] - The current window instance, which for handle the **Window Level** things. - [App] - The current application instance, which for handle the **Application Level** things. - [Context] - The Entity Context instance, which for handle the **Context Level** things. - [Entity] - The Entity instance, which for handle the **Entity Level** things. For example: ```rs fn new(window: &mut Window, cx: &mut App) {} impl RenderOnce for MyElement { fn render(self, window: &mut Window, cx: &mut App) {} } impl Render for MyView { fn render(&mut self, window: &mut Window, cx: &mut Context) {} } ``` :::info As you can see, we always use `cx` to represent `App` and `Context`, which is the standard naming convention for GPUI, we can follow this convention to make our code more readable and maintainable. ::: [Window]: https://docs.rs/gpui/latest/gpui/struct.Window.html [App]: https://docs.rs/gpui/latest/gpui/struct.App.html [Context]: https://docs.rs/gpui/latest/gpui/struct.Context.html [Entity]: https://docs.rs/gpui/latest/gpui/struct.Entity.html ================================================ FILE: docs/docs/element_id.md ================================================ --- title: ElementId description: To introduce the ElementId concept in GPUI. order: -4 --- The [ElementId] is a unique identifier for a GPUI element. It is used to reference elements in the GPUI component tree. Before you start using GPUI and GPUI Component, you need to understand the [ElementId]. For example: ```rs div().id("my-element").child("Hello, World!") ``` In this case, the `div` element has an `id` of `"my-element"`. The add `id` is used for GPUI for binding events, for example `on_click` or `on_mouse_move`, the `element` with `id` in GPUI we call [Stateful\]. We also use `id` (actually, it uses [GlobalElementId] internally in GPUI) to manage the `state` in some elements, by using `window.use_keyed_state`, so it is important to keep the `id` unique. ## Unique The `id` should be unique within the layout scope (In a same [Stateful\] parent). For example we have a list with multiple items: ```rs div().id("app").child( div().id("list1").child(vec![ div().id(1).child("Item 1"), div().id(2).child("Item 2"), div().id(3).child("Item 3"), ]) ).child( div().id("list2").child(vec![ div().id(1).child("Item 1"), ]) ) ``` In this case, we can named the child items with a very simple id, because they are have a parent `list1` element with an `id`. GPUI internal will generate [GlobalElementId] with the parent elements's `id`, in this example, the `Item 1` will have global_id: ```rs ["app", "list1", 1] ``` And the `Item 1` in `list2` will have global_id: ```rs ["app", "list2", 1] ``` So we can named the child items with a very simple id. [ElementId]: https://docs.rs/gpui/latest/gpui/enum.ElementId.html [GlobalElementId]: https://docs.rs/gpui/latest/gpui/struct.GlobalElementId.html [Stateful]: https://docs.rs/gpui/latest/gpui/struct.Stateful.html [Stateful\]: https://docs.rs/gpui/latest/gpui/struct.Stateful.html ================================================ FILE: docs/docs/getting-started.md ================================================ --- title: Getting Started description: Learn how to set up and use GPUI Component in your project order: -2 --- # Getting Started ## Installation Add dependencies to your `Cargo.toml`: ```toml-vue [dependencies] gpui = "{{ GPUI_VERSION }}" gpui-component = "{{ VERSION }}" # Optional, for default bundled assets gpui-component-assets = "{{ VERSION }}" anyhow = "1.0" ``` :::tip The `gpui-component-assets` crate is optional. It provides a default set of icon assets. If you want to manage your own assets, you can skip adding this dependency. See [Icons & Assets](./assets.md) for more details. ::: ## Quick Start Here's a simple example to get you started: ```rust use gpui::*; use gpui_component::{button::*, *}; pub struct HelloWorld; impl Render for HelloWorld { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .v_flex() .gap_2() .size_full() .items_center() .justify_center() .child("Hello, World!") .child( Button::new("ok") .primary() .label("Let's Go!") .on_click(|_, _, _| println!("Clicked!")), ) } } fn main() { let app = gpui_platform::application().with_assets(gpui_component_assets::Assets); app.run(move |cx| { // This must be called before using any GPUI Component features. gpui_component::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| HelloWorld); // This first level on the window, should be a Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` :::info Make sure to call `gpui_component::init(cx);` at first line inside the `app.run` closure. This initializes the GPUI Component system. This is required for theming and other global settings to work correctly. ::: ## Basic Concepts ### Stateless Elements GPUI Component uses stateless [RenderOnce] elements, making them simple and predictable. State management is handled at the view level, not in individual components. The are all implemented [IntoElement] types. For example: ```rs struct MyView; impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .child(Button::new("btn").label("Click Me")) .child(Tag::secondary().child("Secondary")) } } ``` ### Stateful Components There are some stateful components like `Dropdown`, `List`, and `Table` that manage their own internal state for convenience, these components implement the [Render] trait. Those components to use are a bit different, we need create the [Entity] and hold it in the view struct. ```rs struct MyView { input: Entity, } impl MyView { fn new(window: &Window, cx: &mut Context) -> Self { let input = cx.new(|cx| InputState::new(window, cx).default_value("Hello 世界")); Self { input } } } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { self.input.clone() } } ``` ### Theming All components support theming through the built-in `Theme` system: ```rust use gpui_component::{ActiveTheme, Theme}; // Access theme colors in your components cx.theme().primary cx.theme().background cx.theme().foreground ``` ### Sizing Most components support multiple sizes: ```rust Button::new("btn").small() Button::new("btn").medium() // default Button::new("btn").large() Button::new("btn").xsmall() ``` ### Variants Components offer different visual variants: ```rust Button::new("btn").primary() Button::new("btn").danger() Button::new("btn").warning() Button::new("btn").success() Button::new("btn").ghost() Button::new("btn").outline() ``` ## Icons :::info Icons are not bundled with GPUI Component to keep the library lightweight. Continue read [Icons & Assets](./assets.md) to learn how to add icons to your project. ::: GPUI Component has an `Icon` element, but does not include SVG files by default. The examples use [Lucide](https://lucide.dev) icons. You can use any icons you like by naming the SVG files as defined in `IconName`. Add the icons you need to your project. ```rust use gpui_component::{Icon, IconName}; Icon::new(IconName::Check) Icon::new(IconName::Search).small() ``` ## Next Steps Explore the component documentation to learn more about each component: - [Button](./components/button) - Interactive button component - [Input](./components/input) - Text input with validation - [Dialog](./components/dialog) - Dialog and modal windows - [DataTable](./components/data-table) - High-performance data tables - [More components...](./components/index) ## Development To run the component gallery: ```bash cargo run ``` More examples can be found in the `examples` directory: ```bash cargo run --example ``` [RenderOnce]: https://docs.rs/gpui/latest/gpui/trait.RenderOnce.html [IntoElement]: https://docs.rs/gpui/latest/gpui/trait.IntoElement.html [Render]: https://docs.rs/gpui/latest/gpui/trait.Render.html ================================================ FILE: docs/docs/index.md ================================================ --- title: Introduction description: Rust GUI components for building fantastic cross-platform desktop application by using GPUI. --- # GPUI Component Introduction GPUI Component is a Rust UI component library for building fantastic desktop applications using [GPUI](https://gpui.rs). GPUI Component is a comprehensive UI component library for building fantastic desktop applications using [GPUI](https://gpui.rs). It provides 60+ cross-platform components with modern design, theming support, and high performance. ## Features - **Richness**: 60+ cross-platform desktop UI components - **Native**: Inspired by macOS and Windows controls, combined with shadcn/ui design - **Ease of Use**: Stateless `RenderOnce` components, simple and user-friendly - **Customizable**: Built-in `Theme` and `ThemeColor`, supporting multi-theme - **Versatile**: Supports sizes like `xs`, `sm`, `md`, and `lg` - **Flexible Layout**: Dock layout for panel arrangements, resizing, and freeform (Tiles) layouts - **High Performance**: Virtualized Table and List components for smooth large-data rendering - **Content Rendering**: Native support for Markdown and simple HTML - **Charting**: Built-in charts for visualization - **Editor**: High performance code editor with LSP support - **Syntax Highlighting**: Using Tree Sitter ## Quick Example Add `gpui` and `gpui-component` to your `Cargo.toml`: ```toml-vue [dependencies] gpui = "{{ VERSION }}" gpui-component = "{{ VERSION }}" ``` Then create a simple "Hello, World!" application with a button: ```rust use gpui::*; use gpui_component::{button::*, *}; pub struct HelloWorld; impl Render for HelloWorld { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .v_flex() .gap_2() .size_full() .items_center() .justify_center() .child("Hello, World!") .child( Button::new("ok") .primary() .label("Let's Go!") .on_click(|_, _, _| println!("Clicked!")), ) } } fn main() { let app = Application::new(); app.run(move |cx| { // This must be called before using any GPUI Component features. gpui_component::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| HelloWorld); // This first level on the window, should be a Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` ## Community & Support - [GitHub Repository](https://github.com/longbridge/gpui-component) - [Issue Tracker](https://github.com/longbridge/gpui-component/issues) - [Contributing Guide](https://github.com/longbridge/gpui-component/blob/main/CONTRIBUTING.md) ## License Apache-2.0 ================================================ FILE: docs/docs/installation.md ================================================ --- title: Installation order: -1 --- # Installation Before you start to build your application with `gpui-component`, you need to install the library. ## System Requirements We can development application on macOS, Windows or Linux. ### macOS - macOS 15 or later - Xcode command line tools ## Windows - Windows 10 or later There have a bootstrap script to help install the required toolchain and dependencies. You can run the script in PowerShell: ```ps .\script\install-window.ps1 ``` ## Linux Run `./script/bootstrap` to install system dependencies. ## Rust and Cargo We use Rust programming language to build the `gpui-component` library. Make sure you have Rust and Cargo installed on your system. - Rust 1.90 or later - Cargo (comes with Rust) To install the `gpui-component` library, you can use Cargo, the Rust package manager. Add the following line to your `Cargo.toml` file under the `[dependencies]` section: ```toml-vue gpui = "{{ GPUI_VERSION }}" gpui-component = "{{ VERSION }}" ``` ================================================ FILE: docs/docs/root.md ================================================ --- order: -7 --- # Root View The [Root] component for as the root provider of GPUI Component features in a window. We must to use [Root] as the **first level child** of a window to enable GPUI Component features. This is important, if we don't use [Root] as the first level child of a window, there will have some unexpected behaviors. ```rs fn main() { let app = Application::new(); app.run(move |cx| { // This must be called before using any GPUI Component features. gpui_component::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| Example); // This first level on the window, should be a Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` ## Overlays We have dialogs, sheets, notifications, we need placement for them to show, so [Root] provides methods to render these overlays: - [Root::render_dialog_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_dialog_layer) - Render the current opened modals. - [Root::render_sheet_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_sheet_layer) - Render the current opened drawers. - [Root::render_notification_layer](https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html#method.render_notification_layer) - Render the notification list. We can put these layers in the `render` method your first level view (Root > YourFirstView): ```rs struct MyApp; impl Render for MyApp { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .size_full() .child("My App Content") .children(Root::render_dialog_layer(cx)) .children(Root::render_sheet_layer(cx)) .children(Root::render_notification_layer(cx)) } } ``` :::tip Here the example we used `children` method, it because if there is no opened dialogs, sheets, notifications, these methods will return `None`, so GPUI will not render anything. ::: [Root]: https://docs.rs/gpui-component/latest/gpui_component/root/struct.Root.html ================================================ FILE: docs/docs/theme.md ================================================ --- order: -4 --- # Theme All components support theming through the built-in Theme system, the [ActiveTheme] trait provides access to the current theme colors: ```rs use gpui_component::{ActiveTheme as _}; // Access theme colors in your components cx.theme().primary cx.theme().background cx.theme().foreground ``` So if you want use the colors from the current theme, you should keep your component or view have [App] context. ## Theme Registry There have more than 20 built-in themes available in [themes](https://github.com/longbridge/gpui-component/tree/main/themes) folder. https://github.com/longbridge/gpui-component/tree/main/themes And we have a [ThemeRegistry] to help us to load themes. ```rs use std::path::PathBuf; use gpui::{App, SharedString}; use gpui_component::{Theme, ThemeRegistry}; pub fn init(cx: &mut App) { let theme_name = SharedString::from("Ayu Light"); // Load and watch themes from ./themes directory if let Err(err) = ThemeRegistry::watch_dir(PathBuf::from("./themes"), cx, move |cx| { if let Some(theme) = ThemeRegistry::global(cx) .themes() .get(&theme_name) .cloned() { Theme::global_mut(cx).apply_config(&theme); } }) { tracing::error!("Failed to watch themes directory: {}", err); } } ``` [ActiveTheme]: https://docs.rs/gpui-component/latest/gpui_component/theme/trait.ActiveTheme.html [ThemeRegistry]: https://docs.rs/gpui-component/latest/gpui_component/theme/struct.ThemeRegistry.html [App]: https://docs.rs/gpui/latest/gpui/struct.App.html ================================================ FILE: docs/index.md ================================================ --- layout: home --- ## Simple and Intuitive API Get started with just a few lines of code. Stateless components make it easy to build complex UIs. ```rs Button::new("ok") .primary() .label("Click Me") .on_click(|_, _, _| println!("Button clicked!")) ``` ## Install GPUI Component Add the following to your `Cargo.toml`: GPUI and GPUI Component are under active development, recently GPUI have some new features not published on crates.io, so we recommend using the git version for now. The documentation on this site are based on the **Git main branch**, if you use the crates.io version, there may be some differences. ```toml-vue gpui = { git = "https://github.com/zed-industries/zed" } gpui-component = { git = "https://github.com/longbridge/gpui-component" } ``` If you prefer to use the versions on crates.io. Please visit [docs.rs](https://docs.rs/gpui-component/latest/gpui_component/) to check the API differences. ```toml-vue gpui = "{{ GPUI_VERSION }}" gpui-component = "{{ VERSION }}" ``` ## Hello World The following `src/main.rs` is a simple "Hello, World!" application: ```rs use gpui::*; use gpui_component::{button::*, *}; pub struct HelloWorld; impl Render for HelloWorld { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .v_flex() .gap_2() .size_full() .items_center() .justify_center() .child("Hello, World!") .child( Button::new("ok") .primary() .label("Let's Go!") .on_click(|_, _, _| println!("Clicked!")), ) } } fn main() { let app = Application::new(); app.run(move |cx| { // This must be called before using any GPUI Component features. gpui_component::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| HelloWorld); // This first level on the window, should be a Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ``` Run the program with the following command: ```sh $ cargo run ``` ================================================ FILE: docs/index.vue ================================================ ================================================ FILE: docs/package.json ================================================ { "name": "gpui-component-docs", "dependencies": { "@tailwindcss/postcss": "^4.1.15", "@tailwindcss/vite": "^4.1.15", "lucide-react": "^0.546.0", "lucide-vue-next": "^0.546.0", "markdown-it-mathjax3": "^4", "postcss": "^8.5.6", "tailwindcss": "^4.1.15", "vite-plugin-toml": "^0.8.5" }, "devDependencies": { "@types/bun": "^1.3.2", "sass": "^1", "vitepress": "^2.0.0-alpha.12", "vitepress-plugin-llms": "^1.8.0", "vitepress-sidebar": "^1.18.0" }, "scripts": { "dev": "vitepress dev", "build": "vitepress build", "preview": "vitepress preview" }, "private": true, "peerDependencies": { "typescript": "^5" } } ================================================ FILE: docs/postcss.config.mjs ================================================ export default { plugins: { "@tailwindcss/postcss": {}, }, }; ================================================ FILE: docs/skills.md ================================================ --- title: Skills layout: home description: GPUI Component Skills - Available skills for working with GPUI Component --- ================================================ FILE: docs/skills.vue ================================================ ================================================ FILE: docs/src/dark.theme.json ================================================ { "name": "macos-classic-dark", "author": "Jason Lee", "doc": "Based on macOS classic Dark 2 theme.", "maintainers": ["Jason Lee "], "type": "dark", "semanticClass": "theme.macos-classic-dark2", "colors": { "editor.background": "#151515", "editor.foreground": "#CACCCA", "editor.lineHighlightBackground": "#272727", "editorIndentGuide.background": "#303030", "editorLineNumber.foreground": "#8F8F8F", "editorLineNumber.background": "#2C2C2C", "editorBracketMatch.background": "#1E1D1E", "editorBracketMatch.border": "#303030", "editorGroupHeader.tabsBackground": "#1B1B1B", "editorGroupHeader.tabsBorder": "#444444", "editorGroup.border": "#393939", "editorGroupHeader.border": "#393939", "tab.border": "#3F3F3F", "tab.activeBackground": "#101010", "tab.activeForeground": "#FFFFFF", "tab.activeBorder": "#101010", "tab.activeBorderTop": "#101010", "tab.inactiveBackground": "#242424", "tab.inactiveForeground": "#9E9E9E", "statusBar.foreground": "#B2B2B2", "statusBar.noFolderForeground": "#B2B2B2", "statusBar.background": "#262626", "statusBar.noFolderBackground": "#262626", "statusBar.border": "#4A494A", "statusBar.debuggingBackground": "#262626", "statusBar.debuggingForeground": "#B2B2B2", "activityBar.background": "#262626", "activityBar.border": "#2A2A2A", "activityBar.foreground": "#6A6A69", "activityBar.inactiveForeground": "#5A5A59", "activityBarBadge.background": "#010101", "activityBarBadge.foreground": "#E0E0E0", "sideBar.background": "#1c1c1c", "sideBar.foreground": "#B2B2B2", "sideBar.border": "#3A3A3A", "sidebarTitle.background": "#302F30", "sideBarTitle.foreground": "#898989", "sideBarSectionHeader.background": "#302F30", "sideBarSectionHeader.foreground": "#898989", "sideBarSectionHeader.border": "#363636", "list.hoverBackground": "#252426", "list.hoverForeground": "#B2B2B2", "list.activeSelectionBackground": "#353436", "list.activeSelectionForeground": "#E3E4E4", "list.inactiveSelectionBackground": "#353436", "list.inactiveSelectionForeground": "#B2B2B2", "titleBar.inactiveBackground": "#333333", "titleBar.activeBackground": "#292929", "titleBar.border": "#3A393A", "notification.background": "#54a3ff", "panel.background": "#1E1D1E", "panel.border": "#3A3A3A", "panelInput.border": "#303030", "panelTitle.activeBorder": "#3A3A3A", "panelTitle.activeForeground": "#B2B2B2", "inputOption.activeBackground": "#303030", "input.background": "#1E1D1E", "input.foreground": "#DDDDDD", "input.border": "#303030", "focusBorder": "#3A3A3AFF", "dropdown.background": "#1E1D1E", "dropdown.border": "#303030", "dropdown.foreground": "#B2B2B2", "scrollbarSlider.background": "#2C2D2D", "scrollbarSlider.hoverBackground": "#2C2D2D", "scrollbarSlider.activeBackground": "#2C2D2D", "scrollbar.shadow": "#1e1f2099", "editorCursor.foreground": "#CACCCA", "selection.background": "#004a9e", "editor.selectionBackground": "#004a9e", "editor.selectedForeground": "#FFFFFF", "editor.findMatchHighlightBackground": "#705f00", "editor.findMatchBackground": "#194dbd", "editor.wordHighlightBackground": "#444547", "terminal.ansiBlue": "#282BFF", "terminal.ansiBrightBlue": "#272BFF", "terminal.ansiCyan": "#0B0098", "terminal.ansiBrightCyan": "#301CAE", "terminal.ansiGreen": "#277F2B", "terminal.ansiBrightGreen": "#449444", "terminal.ansiMagenta": "#AE30C2", "terminal.ansiBrightMagenta": "#BA3DCA", "terminal.ansiRed": "#C71211", "terminal.ansiBrightRed": "#D9564E", "terminal.ansiWhite": "#131313", "terminal.ansiBrightWhite": "#eeeeee", "editorWidget.background": "#272727", "editorWidget.border": "#272727", "button.border": "#252425", "button.background": "#0854D0", "button.hoverBackground": "#1f6ae2", "button.secondaryBackground": "#3d3d3d", "button.secondaryHoverBackground": "#4c4c4c", "widget.shadow": "#191919", "widget.border": "#3a3a3a", "editorSuggestWidget.background": "#363636", "editorSuggestWidget.selectedBackground": "#6a6a6a", "editorSuggestWidget.selectedForeground": "#ffffff", "editorSuggestWidget.border": "#3A3A3A", "editorSuggestWidget.highlightForeground": "#ffff00", "editorHoverWidget.background": "#363636", "editorHoverWidget.selectedBackground": "#464646", "editorHoverWidget.border": "#3A3A3A", "problemsWarningIcon.foreground": "#f26d0d" }, "tokenColors": [ { "settings": { "foreground": "#CACCCA", "background": "#131313" } }, { "scope": "emphasis", "settings": { "fontStyle": "italic" } }, { "scope": ["strong", "markup.heading.markdown", "markup.bold.markdown"], "settings": { "fontStyle": "bold" } }, { "scope": ["markup.italic.markdown"], "settings": { "fontStyle": "italic" } }, { "scope": "meta.link.inline.markdown", "settings": { "fontStyle": "underline", "foreground": "#4C9BE9" } }, { "scope": ["comment", "markup.fenced_code", "markup.inline"], "settings": { "foreground": "#9E9E9E" } }, { "scope": "string", "settings": { "foreground": "#76BA53" } }, { "scope": [ "variable.other.constant", "variable.other.class", "meta.property-name", "meta.property-value", "support", "constant.language.boolean", "support.function.kernel" ], "settings": { "foreground": "#CACCCA" } }, { "scope": ["constant.language", "constant.other.color"], "settings": { "foreground": "#ca8a04" } }, { "scope": [ "keyword", "storage.modifier", "storage.type", "variable.language.this", "punctuation.definition.template-expression", "constant.numeric", "entity.other.attribute-name" ], "settings": { "foreground": "#c28b12" } }, { "scope": ["meta.tag.structure", "entity.name.tag"], "settings": { "foreground": "#9a9576" } }, { "scope": [ "keyword.operator.accessor", "meta.group.braces.round.function.arguments", "meta.template.expression" ], "settings": { "foreground": "#AE9513" } }, { "scope": [ "entity.name.type.class", "entity.other.inherited-class", "source.css", "entity.name.tag.css", "entity.other.attribute-name.class.css", "punctuation.definition.entity.css", "meta.attribute-selector.scss", "entity.other.attribute-name.attribute.scss" ], "settings": { "foreground": "#AC98AD" } }, { "scope": [ "variable.language.self", "variable.other.readwrite.instance", "meta.definition.variable.scss" ], "settings": { "foreground": "#E1D797" } }, { "scope": [ "entity.name.type", "entity.other.inherited-class", "variable.other.object.property", "meta.instance.constructor" ], "settings": { "foreground": "#b54e05" } }, { "scope": [ "support.constant.property-value", "support.variable.property.dom", "support.type.property-name.json", "punctuation.separator.key-value" ], "settings": { "foreground": "#CACCCA" } }, { "scope": ["meta.function-call", "variable.parameter.function"], "settings": { "foreground": "#CACCCA" } }, { "scope": ["support.function", "entity.name.function"], "settings": { "foreground": "#fdd888" } }, { "scope": ["variable.other.constant", "variable.language.this"], "settings": { "foreground": "#7D7A68" } }, { "scope": ["constant.other.symbol"], "settings": { "foreground": "#7D7A68" } }, { "scope": [ "string.quoted", "string.regexp", "string.interpolated", "string.template", "keyword.other.template" ], "settings": { "foreground": "#62BA46" } }, { "scope": "token.info-token", "settings": { "foreground": "#316bcd" } }, { "scope": "token.warn-token", "settings": { "foreground": "#cd9731" } }, { "scope": "token.error-token", "settings": { "foreground": "#cd3131" } }, { "scope": "token.debug-token", "settings": { "foreground": "#800080" } } ] } ================================================ FILE: docs/src/light.theme.json ================================================ { "name": "macos-classic-light", "author": "Jason Lee", "doc": "Based on macOS classic theme.", "maintainers": ["Jason Lee "], "type": "light", "semanticClass": "theme.macos-classic", "colors": { "editor.background": "#f6f6f7", "editor.foreground": "#000000", "editor.lineHighlightBackground": "#F5F5F5", "editorIndentGuide.activeBackground": "#D2D2D2", "editorIndentGuide.background": "#E3E4E4", "editorLineNumber.foreground": "#929292", "editorLineNumber.background": "#e4e4e4", "editorBracketMatch.background": "#f1f8ff", "editorBracketMatch.border": "#c8e1ff", "editorGroupHeader.tabsBackground": "#EAEAEA", "editorGroupHeader.tabsBorder": "#D2D2D2", "editorGroup.border": "#a09ea0", "editorGroupHeader.border": "#C3C3C3", "tab.activeBackground": "#F9F9F9", "tab.activeForeground": "#474747", "tab.activeBorder": "#DADADA", "tab.activeBorderTop": "#FFFFFF", "tab.inactiveBackground": "#ECECEC", "tab.inactiveForeground": "#6D6D6D", "tab.border": "#D2D2D2", "statusBar.background": "#F5F6F6", "statusBar.foreground": "#494949", "statusBar.noFolderForeground": "#575557", "statusBar.noFolderBackground": "#ededed", "statusBar.border": "#DCDBDA", "statusBar.debuggingBackground": "#fafbfc", "statusBar.debuggingForeground": "#24292e", "activityBar.background": "#ECECEC", "activityBar.border": "#D8D8D8", "activityBar.foreground": "#6A6A69", "activityBar.inactiveForeground": "#6A6A69", "activityBarBadge.background": "#FFFFFF", "activityBarBadge.foreground": "#575557", "sideBar.background": "#EAEAEA", "sideBar.foreground": "#464646", "sideBar.border": "#D2D2D2", "sidebarTitle.background": "#f6f6f6", "sideBarTitle.foreground": "#575557", "sideBarSectionHeader.background": "#f6f6f6", "sideBarSectionHeader.foreground": "#808080", "sideBarSectionHeader.border": "#d5d5d5", "list.hoverBackground": "#D0D0D0", "list.hoverForeground": "#262426", "list.activeSelectionBackground": "#0069d9", "list.activeSelectionForeground": "#ffffff", "list.inactiveSelectionBackground": "#0069d9", "list.inactiveSelectionForeground": "#ffffff", "list.focusHighlightForeground": "#9dddff", "titlebar.inactiveForeground": "#E0E0E0", "titleBar.activeBackground": "#FDFDFD", "titleBar.border": "#D2D2D2", "panel.background": "#F9F9F9", "panel.border": "#DCDBDA", "panelInput.border": "#DCDBDA", "panelTitle.activeBorder": "#101010", "panelTitle.activeForeground": "#24292e", "input.background": "#ffffff", "input.foreground": "#262426", "input.border": "#e0e0e0", "inputOption.activeBackground": "#e0e0e0", "focusBorder": "#eeeeee", "dropdown.background": "#ffffff", "dropdown.border": "#c4c4c4", "dropdown.foreground": "#262426", "scrollbarSlider.background": "#CACACA", "scrollbarSlider.hoverBackground": "#C0C0C0", "scrollbarSlider.activeBackground": "#C0C0C0", "scrollbar.shadow": "#f0f0f0", "editorCursor.foreground": "#101010", "editor.wordHighlightBackground": "#DFEDFF", "terminal.ansiBlue": "#282BFF", "terminal.ansiBrightBlue": "#272BFF", "terminal.ansiCyan": "#0B0098", "terminal.ansiBrightCyan": "#301CAE", "terminal.ansiGreen": "#277F2B", "terminal.ansiBrightGreen": "#449444", "terminal.ansiMagenta": "#AE30C2", "terminal.ansiBrightMagenta": "#BA3DCA", "terminal.ansiRed": "#C71211", "terminal.ansiBrightRed": "#D9564E", "terminal.ansiWhite": "#ffffff", "terminal.ansiBrightWhite": "#eeeeee", "editorWidget.background": "#EAEAEA", "editorWidget.border": "#eaeaea", "button.secondaryBackground": "#505050", "button.secondaryHoverBackground": "#505050", "button.border": "#EAEAEA", "button.background": "#1f6ae2", "button.hoverBackground": "#1f6ae2", "widget.shadow": "#cecece", "widget.border": "#cfcfcf", "notificationCenterHeader.background": "#f7f7f7", "notifications.border": "#e5e5e6", "notification.background": "#54a3ff", "editorSuggestWidget.background": "#FBFBFB", "editorSuggestWidget.selectedBackground": "#0069D9", "editorSuggestWidget.selectedForeground": "#FFFFFF", "editorSuggestWidget.highlightForeground": "#0069D9", "editorSuggestWidget.border": "#E0E0E0", "editorHoverWidget.background": "#FBFBFB", "editorHoverWidget.selectedBackground": "#0069D9", "editorHoverWidget.border": "#E0E0E0", "problemsWarningIcon.foreground": "#f26d0d" }, "tokenColors": [ { "settings": { "foreground": "#000000", "background": "#f6f6f7" } }, { "scope": "emphasis", "settings": { "fontStyle": "italic" } }, { "scope": ["strong", "markup.heading.markdown", "markup.bold.markdown"], "settings": { "fontStyle": "bold" } }, { "scope": ["markup.italic.markdown"], "settings": { "fontStyle": "italic" } }, { "scope": "meta.link.inline.markdown", "settings": { "fontStyle": "underline", "foreground": "#005cc5" } }, { "scope": ["comment", "markup.fenced_code", "markup.inline"], "settings": { "foreground": "#9a9a9a" } }, { "scope": "string", "settings": { "foreground": "#036A07" } }, { "scope": [ "variable.other.constant", "variable.other.class", "meta.property-name", "meta.property-value", "support", "constant.language.boolean", "support.function.kernel" ], "settings": { "foreground": "#8090e5" } }, { "scope": ["constant.language", "constant.other.color"], "settings": { "foreground": "#C5060B" } }, { "scope": [ "keyword", "storage.modifier", "storage.type", "support.function", "variable.language.this", "punctuation.definition.template-expression", "constant.numeric", "entity.name.tag" ], "settings": { "foreground": "#0433ff" } }, { "scope": ["entity.other.attribute-name", "meta.tag.structure"], "settings": { "foreground": "#0000A2" } }, { "scope": [ "keyword.operator.accessor", "meta.group.braces.round.function.arguments", "meta.template.expression" ], "settings": { "foreground": "#000000" } }, { "scope": [ "entity.name.type.class", "entity.other.inherited-class", "meta.property-value", "source.css", "entity.name.tag.css", "entity.other.attribute-name.class.css", "punctuation.definition.entity.css", "meta.attribute-selector.scss", "entity.other.attribute-name.attribute.scss" ], "settings": { "foreground": "#000000" } }, { "scope": [ "variable.language.self", "variable.other.readwrite.instance", "meta.definition.variable.scss" ], "settings": { "foreground": "#318495" } }, { "scope": [ "entity.name.type", "entity.other.inherited-class", "variable.other.object.property", "meta.instance.constructor" ], "settings": { "foreground": "#571ab7" } }, { "scope": ["support.constant.property-value"], "settings": { "foreground": "#119605" } }, { "scope": [ "meta.function-call", "variable.parameter.function", "support.variable.property.dom", "support.type.property-name.json", "punctuation.separator.key-value" ], "settings": { "foreground": "#333333" } }, { "scope": ["entity.name.function"], "settings": { "foreground": "#0000A2" } }, { "scope": ["variable.other.constant", "variable.language.this"], "settings": { "foreground": "#3b96a6" } }, { "scope": ["constant.other.symbol"], "settings": { "foreground": "#d21f07" } }, { "scope": [ "string.quoted", "string.regexp", "string.interpolated", "string.template", "keyword.other.template" ], "settings": { "foreground": "#036A07" } }, { "scope": "token.info-token", "settings": { "foreground": "#316bcd" } }, { "scope": "token.warn-token", "settings": { "foreground": "#cd9731" } }, { "scope": "token.error-token", "settings": { "foreground": "#cd3131" } }, { "scope": "token.debug-token", "settings": { "foreground": "#800080" } } ] } ================================================ FILE: examples/README.md ================================================ # GPUI Component basic examples This folder contains basic examples of how to use the GPUI Component library. Each example demonstrates a specific feature or functionality of the library. Unlike the examples in the `story` folder, these examples focus on 1 example for 1 feature, making it easier to understand and implement specific functionalities in your own projects. ## Contributing Feel free to contribute more examples to this folder! If you have a specific use case or feature you'd like to demonstrate, please create a new example file and submit a pull request. We will happy to merge it into the repository. When creating a new example, please follow these guidelines: 1. Keep 1 example just doing 1 thing for more clarity. 2. Testing the example to ensure it works as expected. 3. Write some comment at some key parts of the code to explain what it does. 4. Following the code style and name style used in the existing examples or in entire of GPUI Component. ================================================ FILE: examples/app_assets/Cargo.toml ================================================ [package] name = "app_assets" description = "Example to load icons or images from assets folder." version = "0.5.1" publish = false edition.workspace = true [dependencies] anyhow.workspace = true gpui.workspace = true gpui_platform.workspace = true gpui-component = { workspace = true } rust-embed = { version = "8", features = ["interpolate-folder-path"] } [lints] workspace = true ================================================ FILE: examples/app_assets/README.md ================================================ ## Icon assets in GPUI Component The [IconName](https://github.com/longbridge/gpui-component/blob/6998708b817024c2ac0f1ea164d74ddfc024e124/crates/ui/src/icon.rs#L9) is a enum that defined a bunch of icon names, because some internal components in GPUI Component will use them. You can see, we have a lot of svg icon files in the `assets/icons` folder, but we are not embed all of the icon files in the library by default. This for keep the library size small. So you must have your own icon files to use the `Icon` component in GPUI Component. You can download the icon files from [here](https://lucide.dev/) or use your own icon files as you wish, just use the same filename as the icon name (match with the `IconName` defined) you want to use. For example your assets folder: ``` app_root assets icons close.svg menu.svg ... src main.rs Cargo.toml ``` You also can just copy the svg files you want from the `assets/icons` folder in GPUI Component repo to your own assets folder. ## How to use You need define a `Assets` struct with rust-embed to register assets to GPUI application. ```rs use anyhow::anyhow; use gpui::*; use rust_embed::RustEmbed; use std::borrow::Cow; #[derive(RustEmbed)] #[folder = "./assets"] #[include = "icons/**/*.svg"] pub struct Assets; impl AssetSource for Assets { fn load(&self, path: &str) -> Result>> { Self::get(path) .map(|f| Some(f.data)) .ok_or_else(|| anyhow!("could not find asset at path \"{path}\"")) } fn list(&self, path: &str) -> Result> { Ok(Self::iter() .filter_map(|p| p.starts_with(path).then(|| p.into())) .collect()) } } fn main() { // Call with_assets to register assets let app = gpui_platform::application().with_assets(Assets); // ... } ``` ## Use default bundled assets. The `gpui-component-assets` crate provide a default bundled assets implementation that include all the icon files in the `assets/icons` folder. If you don't want to manage your own icon files, you can just use the default bundled assets. Just add `gpui-component-assets` as a dependency in your `Cargo.toml`: ```toml [dependencies] gpui-component = "*" gpui-component-assets = "*" ``` And then use it in your application: ```rs let app = gpui_platform::application().with_assets(gpui_component_assets::Assets); ``` ================================================ FILE: examples/app_assets/src/main.rs ================================================ use anyhow::anyhow; use gpui::*; use gpui_component::{IconName, Root, v_flex}; use rust_embed::RustEmbed; use std::borrow::Cow; /// An asset source that loads assets from the `./assets` folder. #[derive(RustEmbed)] #[folder = "./assets"] #[include = "icons/**/*.svg"] pub struct Assets; impl AssetSource for Assets { fn load(&self, path: &str) -> Result>> { if path.is_empty() { return Ok(None); } Self::get(path) .map(|f| Some(f.data)) .ok_or_else(|| anyhow!("could not find asset at path \"{path}\"")) } fn list(&self, path: &str) -> Result> { Ok(Self::iter() .filter_map(|p| p.starts_with(path).then(|| p.into())) .collect()) } } pub struct Example; impl Render for Example { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .size_full() .items_center() .justify_center() .text_center() .child(IconName::Inbox) .child(IconName::Bot) } } fn main() { // Register Assets to GPUI application. let app = gpui_platform::application().with_assets(Assets); app.run(move |cx| { // We must initialize gpui_component before using it. gpui_component::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| Example); // The first level on the window must be Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ================================================ FILE: examples/color_mix_oklab.rs ================================================ use gpui::*; use gpui_component::{ h_flex, theme::{ActiveTheme, Colorize}, v_flex, Root, Sizable, }; actions!(demo, [Quit]); struct ColorMixDemo { focus_handle: FocusHandle, } impl ColorMixDemo { fn new(cx: &mut WindowContext) -> Self { Self { focus_handle: cx.focus_handle(), } } } impl Render for ColorMixDemo { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let destructive = cx.theme().destructive; let transparent = hsla(0.0, 0.0, 0.0, 0.0); // 类似 CSS: color-mix(in oklab, var(--destructive) 20%, transparent) let mixed_20 = destructive.mix_oklab(transparent, 0.2); let mixed_50 = destructive.mix_oklab(transparent, 0.5); let mixed_80 = destructive.mix_oklab(transparent, 0.8); v_flex() .gap_4() .p_4() .track_focus(&self.focus_handle) .child( v_flex() .gap_2() .child("Oklab mix with transparent (使用 premultiplied alpha):") .child( h_flex() .gap_2() .child( div() .size_20() .bg(destructive) .child("100%") .text_color(gpui::white()), ) .child( div() .size_20() .bg(mixed_80) .child("80%") .text_color(gpui::white()), ) .child( div() .size_20() .bg(mixed_50) .child("50%") .text_color(gpui::white()), ) .child( div() .size_20() .bg(mixed_20) .child("20%") .text_color(gpui::white()), ), ), ) .child( v_flex() .gap_2() .child(format!( "原始颜色: {} (alpha: {:.2})", destructive.to_hex(), destructive.a )) .child(format!( "80% 混合: {} (alpha: {:.2})", mixed_80.to_hex(), mixed_80.a )) .child(format!( "50% 混合: {} (alpha: {:.2})", mixed_50.to_hex(), mixed_50.a )) .child(format!( "20% 混合: {} (alpha: {:.2})", mixed_20.to_hex(), mixed_20.a )), ) .child( v_flex() .mt_4() .gap_2() .child("比较 HSL 和 Oklab 混合的差异 (50% transparent):") .child( h_flex() .gap_4() .child( v_flex() .gap_1() .child("HSL 混合:") .child( div() .size_16() .bg(destructive.mix(transparent, 0.5)) .text_color(gpui::white()) .child("HSL"), ) .child(format!( "{}", destructive.mix(transparent, 0.5).to_hex() )), ) .child( v_flex() .gap_1() .child("Oklab 混合 (正确):") .child( div() .size_16() .bg(destructive.mix_oklab(transparent, 0.5)) .text_color(gpui::white()) .child("Oklab"), ) .child(format!("{}", mixed_50.to_hex())), ), ), ) .child( v_flex() .mt_4() .gap_2() .child("说明:") .child("- Oklab 混合保持了原始颜色的色调,只改变透明度") .child("- HSL 混合会使颜色变暗(因为与黑色透明混合)") .child("- Oklab 使用 premultiplied alpha 算法,符合 CSS color-mix 规范"), ) } } fn main() { env_logger::init(); Application::new().run(move |cx| { gpui_component::init(cx); cx.activate(true); cx.on_action(|_: &Quit, cx: &mut AppContext| { cx.quit(); }); cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); cx.spawn(|cx| async move { cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(Bounds::centered( None, size(px(600.), px(400.)), cx, ))), ..Default::default() }, |window, cx| { let view = cx.new(|cx| ColorMixDemo::new(cx)); cx.new(|cx| Root::new(view, window, cx)) }, ) .unwrap(); }) .detach(); }); } ================================================ FILE: examples/dialog_overlay/Cargo.toml ================================================ [package] name = "dialog_overlay" description = "An example of using gpui-component to create a Dialog with overlay." version = "0.5.1" publish = false edition.workspace = true [dependencies] anyhow.workspace = true gpui.workspace = true gpui_platform.workspace = true gpui-component.workspace = true gpui-component-assets.workspace = true [lints] workspace = true ================================================ FILE: examples/dialog_overlay/src/main.rs ================================================ use gpui::*; use gpui_component::{button::*, menu::ContextMenuExt, *}; use gpui_component_assets::Assets; actions!(class_menu, [Open, Delete, Export, Info]); pub struct HelloWorld; impl HelloWorld { fn show_dialog(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { window.open_dialog(cx, move |dialog, _, _| { dialog.title("Test dialog").child("Hello from dialog!") }); } fn show_drawer(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { window.open_sheet(cx, move |drawer, _, _| { drawer.title("Test Drawer").child("Hello from Drawer!") }); } } impl Render for HelloWorld { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .bg(gpui::white()) .size_full() .child(TitleBar::new().child("dialog & Drawer")) .child( div() .p_8() .v_flex() .gap_2() .size_full() .child( h_flex() .gap_4() .child( Button::new("btn1") .outline() .label("Open dialog") .on_click(cx.listener(Self::show_dialog)), ) .child( Button::new("btn2") .outline() .label("Open Drawer") .on_click(cx.listener(Self::show_drawer)), ), ) .child( div() .id("second-area") .v_flex() .h_40() .border_1() .border_dashed() .border_color(gpui::black()) .items_center() .justify_center() .hover(|this| this.bg(gpui::yellow().opacity(0.2))) .child("Hover test here.") .child("Right click to show Context Menu") .context_menu({ move |this, _, _| { this.separator() .menu("Open", Box::new(Open)) .menu("Delete", Box::new(Delete)) .menu("Export", Box::new(Export)) .menu("Info", Box::new(Info)) .separator() } }), ), ) .children(Root::render_dialog_layer(window, cx)) .children(Root::render_sheet_layer(window, cx)) } } fn main() { let app = gpui_platform::application().with_assets(Assets); app.run(move |cx| { gpui_component::init(cx); cx.spawn(async move |cx| { cx.open_window( WindowOptions { titlebar: Some(TitleBar::title_bar_options()), ..Default::default() }, |window, cx| { let view = cx.new(|_| HelloWorld); // This first level on the window, should be a Root. cx.new(|cx| Root::new(view, window, cx)) }, ) .expect("Failed to open window"); }) .detach(); }); } ================================================ FILE: examples/focus_trap/Cargo.toml ================================================ [package] name = "focus_trap" description = "Example demonstrating focus trap functionality." version = "0.5.0" publish = false edition.workspace = true [dependencies] anyhow.workspace = true gpui.workspace = true gpui_platform.workspace = true gpui-component = { workspace = true } [lints] workspace = true ================================================ FILE: examples/focus_trap/src/main.rs ================================================ use gpui::*; use gpui_component::{button::*, h_flex, v_flex, *}; pub struct Example { trap1_handle: FocusHandle, trap2_handle: FocusHandle, } impl Example { fn new(cx: &mut App) -> Self { Self { trap1_handle: cx.focus_handle(), trap2_handle: cx.focus_handle(), } } } impl Render for Example { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .gap_6() .p_8() .child(div().text_xl().font_bold().child("Focus Trap Example")) .child( div() .text_sm() .text_color(cx.theme().muted_foreground) .child("Press Tab to navigate between buttons. Notice how focus cycles within different areas."), ) // Outside buttons - not in focus trap .child( v_flex() .gap_3() .child( div() .text_base() .font_semibold() .child("Outside Area (No Focus Trap)"), ) .child( h_flex() .gap_2() .child(Button::new("outside-1").label("Outside Button 1")) .child(Button::new("outside-2").label("Outside Button 2")) .child(Button::new("outside-3").label("Outside Button 3")), ), ) // Focus trap area 1 .child( v_flex() .gap_3() .child(div().text_base().font_semibold().child("Focus Trap Area 1")) .child( h_flex() .gap_2() .p_4() .bg(cx.theme().secondary) .rounded(cx.theme().radius) .border_1() .border_color(cx.theme().border) .child( Button::new("trap1-1") .label("Trap 1 - Button 1") .on_click(|_, _, _| println!("Trap 1 - Button 1 clicked")), ) .child( Button::new("trap1-2") .label("Trap 1 - Button 2") .on_click(|_, _, _| println!("Trap 1 - Button 2 clicked")), ) .child( Button::new("trap1-3") .label("Trap 1 - Button 3") .on_click(|_, _, _| println!("Trap 1 - Button 3 clicked")), ) .focus_trap("trap1", &self.trap1_handle), ) .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("→ Press Tab in this area, focus cycles through 3 buttons without escaping"), ), ) // Middle outside buttons .child( v_flex() .gap_3() .child( div() .text_base() .font_semibold() .child("Outside Area (No Focus Trap)"), ) .child( h_flex() .gap_2() .child(Button::new("outside-4").label("Outside Button 4")) .child(Button::new("outside-5").label("Outside Button 5")), ), ) // Focus trap area 2 .child( v_flex() .gap_3() .child(div().text_base().font_semibold().child("Focus Trap Area 2")) .child( v_flex() .focus_trap("trap2", &self.trap2_handle) .gap_2() .p_4() .grid() .grid_cols(4) .bg(cx.theme().accent.opacity(0.1)) .rounded(cx.theme().radius) .border_1() .border_color(cx.theme().accent) .child(Button::new("trap2-1").label("Trap 2 - Button 1")) .child(Button::new("trap2-2").label("Trap 2 - Button 2")) .child( Button::new("trap2-3").label("Trap 2 - Button 3"), ) .child(Button::new("trap2-4").label("Trap 2 - Button 4")) ) .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("→ Press Tab in this area, focus cycles through 4 buttons without escaping"), ), ) } } fn main() { let app = gpui_platform::application(); app.run(move |cx| { gpui_component::init(cx); let window_options = WindowOptions { window_bounds: Some(WindowBounds::centered(size(px(800.), px(600.)), cx)), ..Default::default() }; cx.spawn(async move |cx| { cx.open_window(window_options, |window, cx| { let view = cx.new(|cx| Example::new(cx)); cx.new(|cx| Root::new(view, window, cx).bg(cx.theme().background)) }) .expect("Failed to open window"); }) .detach(); }); } ================================================ FILE: examples/hello_world/Cargo.toml ================================================ [package] name = "hello_world" description = "A minimal example of application development with GPUI Component." version = "0.5.1" publish = false edition.workspace = true [dependencies] anyhow.workspace = true gpui.workspace = true gpui_platform.workspace = true gpui-component = { workspace = true } [lints] workspace = true ================================================ FILE: examples/hello_world/src/main.rs ================================================ use gpui::*; use gpui_component::{button::*, *}; pub struct Example; impl Render for Example { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .v_flex() .gap_2() .size_full() .items_center() .justify_center() .child("Hello, World!") .child( Button::new("ok") .primary() .label("Let's Go!") .on_click(|_, _, _| println!("Clicked!")), ) } } fn main() { gpui_platform::application().run(move |cx| { // This must be called before using any GPUI Component features. gpui_component::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = cx.new(|_| Example); // This first level on the window, should be a Root. cx.new(|cx| { // You can refine the root view style by yourself. Root::new(view, window, cx).bg(cx.theme().background) }) }) .expect("Failed to open window"); }) .detach(); }); } ================================================ FILE: examples/input/Cargo.toml ================================================ [package] name = "input" version = "0.5.1" publish = false edition.workspace = true [dependencies] anyhow.workspace = true gpui.workspace = true gpui_platform.workspace = true gpui-component.workspace = true gpui-component-assets.workspace = true [lints] workspace = true ================================================ FILE: examples/input/src/main.rs ================================================ use gpui::*; use gpui_component::{ input::{Input, InputEvent, InputState}, *, }; use gpui_component_assets::Assets; pub struct Example { input_state: Entity, display_text: SharedString, /// We need to keep the subscriptions alive with the Example entity. /// /// So if the Example entity is dropped, the subscriptions are also dropped. /// This is important to avoid memory leaks. _subscriptions: Vec, } impl Example { fn new(window: &mut Window, cx: &mut Context) -> Self { let input_state = cx.new(|cx| InputState::new(window, cx).placeholder("Enter your name")); let _subscriptions = vec![cx.subscribe_in(&input_state, window, { let input_state = input_state.clone(); move |this, _, ev: &InputEvent, _window, cx| match ev { InputEvent::Change => { let value = input_state.read(cx).value(); this.display_text = format!("Hello, {}!", value).into(); cx.notify() } _ => {} } })]; Self { input_state, display_text: SharedString::default(), _subscriptions, } } } impl Render for Example { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .p_5() .gap_2() .size_full() .items_center() .justify_center() .child(Input::new(&self.input_state)) .child(self.display_text.clone()) } } fn main() { let app = gpui_platform::application().with_assets(Assets); app.run(move |cx| { // This must be called before using any GPUI Component features. gpui_component::init(cx); let window_options = WindowOptions { window_bounds: Some(WindowBounds::centered(size(px(800.), px(600.)), cx)), ..Default::default() }; cx.spawn(async move |cx| { cx.open_window(window_options, |window, cx| { let view = cx.new(|cx| Example::new(window, cx)); // This first level on the window, should be a Root. cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ================================================ FILE: examples/system_monitor/Cargo.toml ================================================ [package] name = "system_monitor" description = "A real-time system resource monitor using GPUI Component charts." version = "0.5.0" publish = false edition = "2024" [dependencies] anyhow.workspace = true gpui.workspace = true gpui_platform.workspace = true gpui-component-assets.workspace = true gpui-component.workspace = true sysinfo = "0.37" smol = "2" battery = "0.7" [target.'cfg(target_os = "macos")'.dependencies] metal = "0.33" core-foundation = "0.10" [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.62", features = [ "Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D11", "Win32_Graphics_Dxgi", ] } [lints.clippy] dbg_macro = "deny" todo = "deny" ================================================ FILE: examples/system_monitor/src/main.rs ================================================ use std::collections::VecDeque; use std::time::Duration; use gpui::{actions, prelude::FluentBuilder as _, *}; use gpui_component::ThemeMode; use gpui_component::{ ActiveTheme, Icon, IconName, Root, Sizable, Theme, TitleBar, chart::AreaChart, h_flex, progress::Progress, tab::{Tab, TabBar}, table::{Column, ColumnSort, DataTable, TableDelegate, TableState}, v_flex, }; use smol::Timer; use sysinfo::{Disks, Pid, System}; // Define the Quit action actions!(system_monitor, [Quit]); const INTERVAL: Duration = Duration::from_millis(500); const MAX_DATA_POINTS: usize = 120; /// Tab indices #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] enum MonitorTab { #[default] System = 0, Processes = 1, } impl MonitorTab { fn from_index(index: usize) -> Self { match index { 0 => MonitorTab::System, 1 => MonitorTab::Processes, _ => MonitorTab::System, } } } /// A single data point for system metrics #[derive(Clone)] struct MetricPoint { time: String, cpu: f64, memory: f64, } /// Process info for display #[derive(Clone)] struct ProcessInfo { pid: Pid, name: String, cpu_usage: f32, memory: u64, } /// Disk info for display #[derive(Clone)] struct DiskInfo { #[allow(dead_code)] name: String, total: u64, used: u64, } /// Battery info for display #[derive(Clone)] struct BatteryInfo { #[allow(dead_code)] model: String, icon: IconName, percentage: f32, } /// Sort field for processes #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] enum ProcessSortField { Pid, Name, #[default] Cpu, Memory, } /// Process table delegate struct ProcessTableDelegate { processes: Vec, columns: Vec, sort_field: ProcessSortField, sort_order: ColumnSort, } impl ProcessTableDelegate { fn new() -> Self { Self { processes: Vec::new(), columns: vec![ Column::new("pid", "PID").width(70.).sortable(), Column::new("name", "Name").width(380.).sortable(), Column::new("cpu", "CPU %") .width(80.) .sortable() .sort(ColumnSort::Descending), Column::new("memory", "Memory").width(100.).sortable(), ], sort_field: ProcessSortField::Cpu, sort_order: ColumnSort::Descending, } } fn update_processes(&mut self, sys: &System) { self.processes = sys .processes() .iter() .map(|(pid, process)| ProcessInfo { pid: *pid, name: process.name().to_string_lossy().to_string(), cpu_usage: process.cpu_usage(), memory: process.memory(), }) .collect(); self.sort_processes(); } fn sort_processes(&mut self) { let is_descending = matches!(self.sort_order, ColumnSort::Descending); match self.sort_field { ProcessSortField::Pid => { self.processes.sort_by(|a, b| { let cmp = a.pid.as_u32().cmp(&b.pid.as_u32()); if is_descending { cmp.reverse() } else { cmp } }); } ProcessSortField::Name => { self.processes.sort_by(|a, b| { let cmp = a.name.to_lowercase().cmp(&b.name.to_lowercase()); if is_descending { cmp.reverse() } else { cmp } }); } ProcessSortField::Cpu => { self.processes.sort_by(|a, b| { let cmp = a .cpu_usage .partial_cmp(&b.cpu_usage) .unwrap_or(std::cmp::Ordering::Equal); if is_descending { cmp.reverse() } else { cmp } }); } ProcessSortField::Memory => { self.processes.sort_by(|a, b| { let cmp = a.memory.cmp(&b.memory); if is_descending { cmp.reverse() } else { cmp } }); } } // Keep top 200 processes self.processes.truncate(200); } } impl TableDelegate for ProcessTableDelegate { fn columns_count(&self, _cx: &App) -> usize { self.columns.len() } fn rows_count(&self, _cx: &App) -> usize { self.processes.len() } fn column(&self, col_ix: usize, _cx: &App) -> Column { self.columns[col_ix].clone() } fn render_td( &mut self, row_ix: usize, col_ix: usize, _window: &mut Window, cx: &mut Context>, ) -> impl IntoElement { let Some(process) = self.processes.get(row_ix) else { return div().into_any_element(); }; match col_ix { 0 => div() .text_xs() .text_color(cx.theme().muted_foreground) .child(format!("{}", process.pid)) .into_any_element(), 1 => div() .text_sm() .text_color(cx.theme().foreground) .truncate() .child(process.name.clone()) .into_any_element(), 2 => div() .text_xs() .text_color(if process.cpu_usage > 50.0 { cx.theme().red } else if process.cpu_usage > 20.0 { cx.theme().yellow } else { cx.theme().blue }) .child(format!("{:.1}%", process.cpu_usage)) .into_any_element(), 3 => div() .text_xs() .text_color(cx.theme().green) .child(format_bytes(process.memory)) .into_any_element(), _ => div().into_any_element(), } } fn perform_sort( &mut self, col_ix: usize, sort: ColumnSort, _window: &mut Window, _cx: &mut Context>, ) { self.sort_order = sort; self.sort_field = match col_ix { 0 => ProcessSortField::Pid, 1 => ProcessSortField::Name, 2 => ProcessSortField::Cpu, 3 => ProcessSortField::Memory, _ => ProcessSortField::Cpu, }; self.sort_processes(); } } /// Format bytes to human readable string fn format_bytes(bytes: u64) -> String { const KB: u64 = 1024; const MB: u64 = KB * 1024; const GB: u64 = MB * 1024; if bytes >= GB { format!("{:.1} GB", bytes as f64 / GB as f64) } else if bytes >= MB { format!("{:.1} MB", bytes as f64 / MB as f64) } else if bytes >= KB { format!("{:.1} KB", bytes as f64 / KB as f64) } else { format!("{} B", bytes) } } /// System monitor that collects and displays real-time metrics pub struct SystemMonitor { sys: System, disks: Disks, data: VecDeque, time_index: usize, active_tab: MonitorTab, process_table: Entity>, disk_info: Vec, battery_info: Vec, } impl SystemMonitor { fn new(window: &mut Window, cx: &mut Context) -> Self { let mut sys = System::new_all(); sys.refresh_all(); let disks = Disks::new_with_refreshed_list(); // Create process table let process_delegate = ProcessTableDelegate::new(); let process_table = cx.new(|cx| { TableState::new(process_delegate, window, cx) .col_selectable(false) .col_movable(false) }); let mut monitor = Self { sys, disks, data: VecDeque::with_capacity(MAX_DATA_POINTS), time_index: 0, active_tab: MonitorTab::System, process_table, disk_info: Vec::new(), battery_info: Vec::new(), }; // Collect initial data monitor.collect_metrics(cx); // Start the update loop cx.spawn(async move |this, cx| { loop { Timer::after(INTERVAL).await; let result = this.update(cx, |this, cx| { this.collect_metrics(cx); cx.notify(); }); if result.is_err() { break; } } }) .detach(); monitor } fn collect_metrics(&mut self, cx: &mut Context) { // Refresh system info self.sys.refresh_all(); self.disks.refresh(true); // Calculate CPU usage let cpu_usage = self.sys.global_cpu_usage() as f64; // Calculate memory usage let total_memory = self.sys.total_memory() as f64; let used_memory = self.sys.used_memory() as f64; let memory_usage = if total_memory > 0.0 { (used_memory / total_memory * 100.0).min(100.0) } else { 0.0 }; // Create data point let point = MetricPoint { time: format!("{}s", self.time_index), cpu: cpu_usage, memory: memory_usage, }; // Add to history if self.data.len() >= MAX_DATA_POINTS { self.data.pop_front(); } self.data.push_back(point); self.time_index += 1; // Update process table self.process_table.update(cx, |table, cx| { table.delegate_mut().update_processes(&self.sys); cx.notify(); }); // Update disk info (take first disk for status bar) self.disk_info = self .disks .iter() .map(|disk| DiskInfo { name: disk.name().to_string_lossy().to_string(), total: disk.total_space(), used: disk.total_space() - disk.available_space(), }) .collect(); // Update battery info self.update_battery_info(); } fn update_battery_info(&mut self) { self.battery_info.clear(); if let Ok(manager) = battery::Manager::new() && let Ok(batteries) = manager.batteries() { for battery in batteries.flatten() { let icon = match battery.state() { battery::State::Charging => IconName::BatteryCharging, battery::State::Discharging => IconName::BatteryMedium, battery::State::Full => IconName::BatteryFull, battery::State::Empty => IconName::Battery, _ => IconName::Battery, }; self.battery_info.push(BatteryInfo { model: battery .model() .map(|s| s.to_string()) .unwrap_or_else(|| "Battery".to_string()), icon, percentage: battery.state_of_charge().value * 100.0, }); } } } fn set_active_tab(&mut self, index: usize, _window: &mut Window, cx: &mut Context) { self.active_tab = MonitorTab::from_index(index); cx.notify(); } fn render_chart( &self, title: &str, data: Vec, value_fn: impl Fn(&MetricPoint) -> f64 + 'static, color: Hsla, cx: &Context, ) -> impl IntoElement { v_flex() .min_h(px(160.)) .flex_1() .gap_2() .border_1() .border_color(cx.theme().border) .child( h_flex() .justify_between() .py_1() .px_3() .child( div() .text_sm() .text_color(cx.theme().foreground) .child(title.to_string()), ) .child({ let current_value = data.last().map(&value_fn).unwrap_or(0.0); div() .text_sm() .text_color(color) .child(format!("{:.1}%", current_value)) }), ) .child( AreaChart::new(data) .x(|d| d.time.clone()) .y(value_fn) .stroke(color) .fill(linear_gradient( 0., linear_color_stop(color.opacity(0.4), 1.), linear_color_stop(cx.theme().background.opacity(0.1), 0.), )) .tick_margin(15), ) } fn render_system_tab(&self, cx: &Context) -> impl IntoElement { let data: Vec = self.data.iter().cloned().collect(); v_flex() .p_3() .gap_4() .flex_1() .child(self.render_chart("CPU Usage", data.clone(), |d| d.cpu, cx.theme().red, cx)) .child(self.render_chart( "Memory Usage", data.clone(), |d| d.memory, cx.theme().blue, cx, )) } fn render_processes_tab(&self, _cx: &Context) -> impl IntoElement { v_flex().size_full().child( DataTable::new(&self.process_table) .bordered(false) .stripe(true) .small(), ) } fn render_status_bar(&self, cx: &Context) -> impl IntoElement { let primary_disk = self.disk_info.first(); let primary_battery = self.battery_info.first(); h_flex() .px_3() .gap_4() .h_7() .text_sm() .items_center() .justify_between() .border_t_1() .border_color(cx.theme().border) .bg(cx.theme().tab_bar) .text_color(cx.theme().muted_foreground) .child( h_flex() .gap_4() // Disk info .when_some(primary_disk, |this, disk| { let used_percent = if disk.total > 0 { (disk.used as f64 / disk.total as f64 * 100.0) as f32 } else { 0.0 }; this.child( h_flex() .gap_2() .w(px(135.)) .items_center() .child(Icon::new(IconName::HardDrive)) .child( Progress::new("status-disk") .w_12() .h_2() .value(used_percent), ) .child(format!("{:.0}%", used_percent)), ) }) // Memory info .child({ let mem_percent = self.data.back().map(|p| p.memory as f32).unwrap_or(0.0); h_flex() .gap_2() .w(px(135.)) .items_center() .child(Icon::new(IconName::MemoryStick)) .child(Progress::new("status-mem").w_12().h_2().value(mem_percent)) .child(format!("{:.0}%", mem_percent)) }) // CPU info .child({ let cpu_percent = self.data.back().map(|p| p.cpu as f32).unwrap_or(0.0); h_flex() .gap_2() .w(px(135.)) .items_center() .child(Icon::new(IconName::Cpu)) .child(Progress::new("status-cpu").w_12().h_2().value(cpu_percent)) .child(format!("{:.0}%", cpu_percent)) }), ) .child( // Battery info div().when_some(primary_battery, |this, battery| { this.child( h_flex() .gap_2() .items_center() .child(Icon::new(battery.icon.clone())) .child(format!("{:.0}%", battery.percentage)), ) }), ) } } impl Render for SystemMonitor { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let active_tab_index = self.active_tab as usize; v_flex() .size_full() .child( TitleBar::new() .child( TabBar::new("monitor-tabs") .mt(px(1.)) .segmented() .px_0() .py(px(2.)) .bg(cx.theme().title_bar) .selected_index(active_tab_index) .on_click(cx.listener(|this, ix: &usize, window, cx| { this.set_active_tab(*ix, window, cx); })) .child(Tab::new().label("System")) .child(Tab::new().label("Processes")), ) .child( div() .mr_4() .text_xs() .text_color(cx.theme().muted_foreground) .child(format!( "{:.1} GB", self.sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0 )), ), ) .bg(cx.theme().background) .child( div() .id("tab-content") .flex_1() .overflow_y_scroll() .map(|this| match self.active_tab { MonitorTab::System => this.child(self.render_system_tab(cx)), MonitorTab::Processes => this.child(self.render_processes_tab(cx)), }), ) .child(self.render_status_bar(cx)) } } fn main() { let app = gpui_platform::application().with_assets(gpui_component_assets::Assets); app.run(move |cx| { gpui_component::init(cx); cx.bind_keys([ #[cfg(target_os = "macos")] KeyBinding::new("cmd-q", Quit, None), #[cfg(not(target_os = "macos"))] KeyBinding::new("alt-f4", Quit, None), ]); // Handle the Quit action cx.on_action(|_: &Quit, cx: &mut App| { cx.quit(); }); let window_options = WindowOptions { titlebar: Some(TitleBar::title_bar_options()), window_bounds: Some(WindowBounds::centered(size(px(680.), px(600.)), cx)), ..Default::default() }; cx.spawn(async move |cx| { cx.open_window(window_options, |window, cx| { window.activate_window(); window.set_window_title("System Monitor"); Theme::change(ThemeMode::Dark, Some(window), cx); let view = cx.new(|cx| SystemMonitor::new(window, cx)); cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ================================================ FILE: examples/webview/Cargo.toml ================================================ [package] name = "webview" description = "A minimal example of webview inside the GPUI application." version = "0.5.0" publish = false edition.workspace = true [features] inspector = [] [dependencies] anyhow.workspace = true gpui.workspace = true gpui_platform.workspace = true gpui-component = { workspace = true } # WebView dependencies gpui-wry = { path = "../../crates/webview" } wry = { version = "0.53.3", package = "lb-wry" } raw-window-handle = { version = "0.6", features = ["std"] } [target."cfg(target_os = \"linux\")".dependencies] gtk = "0.18.2" [lints] workspace = true ================================================ FILE: examples/webview/src/main.rs ================================================ use gpui::*; use gpui_component::{ ActiveTheme as _, Root, h_flex, input::{Input, InputEvent, InputState}, v_flex, }; use gpui_wry::WebView; pub struct Example { focus_handle: FocusHandle, webview: Entity, address_input: Entity, } impl Example { pub fn new(window: &mut Window, cx: &mut App) -> Entity { let webview = cx.new(|cx| { let builder = wry::WebViewBuilder::new(); #[cfg(any(debug_assertions, feature = "inspector"))] let builder = builder.with_devtools(true); #[cfg(not(any( target_os = "windows", target_os = "macos", target_os = "ios", target_os = "android" )))] let webview = { use gtk::prelude::*; use wry::WebViewBuilderExtUnix; // borrowed from https://github.com/tauri-apps/wry/blob/dev/examples/gtk_multiwebview.rs // doesn't work yet // TODO: How to initialize this fixed? let fixed = gtk::Fixed::builder().build(); fixed.show_all(); builder.build_gtk(&fixed).unwrap() }; #[cfg(any( target_os = "windows", target_os = "macos", target_os = "ios", target_os = "android" ))] let webview = { use raw_window_handle::HasWindowHandle; let window_handle = window.window_handle().expect("No window handle"); builder.build_as_child(&window_handle).unwrap() }; WebView::new(webview, window, cx) }); let address_input = cx.new(|cx| { InputState::new(window, cx).default_value("https://longbridge.github.io/gpui-component") }); let url = address_input.read(cx).value().clone(); webview.update(cx, |view, _| { view.load_url(&url); }); cx.new(|cx| { let this = Self { focus_handle: cx.focus_handle(), webview, address_input: address_input.clone(), }; cx.subscribe( &address_input, |this: &mut Self, input, event: &InputEvent, cx| match event { InputEvent::PressEnter { .. } => { let url = input.read(cx).value().clone(); this.webview.update(cx, |view, _| { view.load_url(&url); }); } _ => {} }, ) .detach(); this }) } pub fn hide(&self, _: &mut Window, cx: &mut App) { self.webview.update(cx, |webview, _| webview.hide()) } #[allow(unused)] fn go_back(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { self.webview.update(cx, |webview, _| { webview.back().unwrap(); }); } } impl Focusable for Example { fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { self.focus_handle.clone() } } impl Render for Example { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let webview = self.webview.clone(); v_flex() .p_2() .gap_3() .size_full() .child( h_flex() .gap_2() .items_center() .child(Input::new(&self.address_input)), ) .child( div() .flex_1() .border_1() .h(gpui::px(400.)) .border_color(cx.theme().border) .child(webview.clone()), ) } } fn main() { // Required this for Windows to render the WebView. #[cfg(target_os = "windows")] unsafe { std::env::set_var("GPUI_DISABLE_DIRECT_COMPOSITION", "true"); } gpui_platform::application().run(move |cx| { // This must be called before using any GPUI Component features. gpui_component::init(cx); cx.spawn(async move |cx| { cx.open_window(WindowOptions::default(), |window, cx| { let view = Example::new(window, cx); cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ================================================ FILE: examples/window_title/Cargo.toml ================================================ [package] name = "window_title" description = "An example of using gpui-component to create a window with a custom title bar." version = "0.5.1" publish = false edition.workspace = true [dependencies] anyhow.workspace = true gpui.workspace = true gpui_platform.workspace = true gpui-component.workspace = true gpui-component-assets.workspace = true [lints] workspace = true ================================================ FILE: examples/window_title/src/main.rs ================================================ use gpui::*; use gpui_component::{ Root, TitleBar, button::{Button, ButtonVariants}, h_flex, v_flex, }; pub struct Example; impl Render for Example { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .size_full() .child( // Render custom title bar on top of Root view. TitleBar::new().child( h_flex() .w_full() .pr_2() .justify_between() .child("App with Custom title bar") .child("Right Item"), ), ) .child( div() .id("window-body") .p_5() .size_full() .items_center() .justify_center() .child("Hello, World!") .child( Button::new("ok") .primary() .label("Let's Go!") .on_click(|_, _, _| println!("Clicked!")), ), ) } } fn main() { let app = gpui_platform::application().with_assets(gpui_component_assets::Assets); app.run(move |cx| { gpui_component::init(cx); cx.spawn(async move |cx| { let window_options = WindowOptions { // Setup GPUI to use custom title bar titlebar: Some(TitleBar::title_bar_options()), ..Default::default() }; cx.open_window(window_options, |window, cx| { let view = cx.new(|_| Example); cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); }) .detach(); }); } ================================================ FILE: flake.nix ================================================ { description = "gpui-component"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; rust-overlay.url = "github:oxalica/rust-overlay"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: let overlays = [ (import rust-overlay) ]; pkgs = import nixpkgs { inherit system overlays; }; in { devShells.default = with pkgs; mkShell { buildInputs = [ openssl pkg-config xorg.libX11 glib pango atkmm gdk-pixbuf gtk3 libsoup_3 webkitgtk_4_1 libxkbcommon vulkan-loader (rust-bin.beta.latest.default.override { extensions = [ "rust-src" ]; }) ]; env = { RUST_BACKTRACE = "1"; LD_LIBRARY_PATH = lib.makeLibraryPath [ vulkan-loader ]; }; }; } ); } ================================================ FILE: script/bootstrap ================================================ #!/usr/bin/env bash if [[ "$OSTYPE" == "linux-gnu"* ]]; then echo "Install Linux dependencies..." script/install-linux.sh else echo "Install macOS dependencies..." fi ================================================ FILE: script/bump-version.sh ================================================ #!/bin/bash # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' BOLD='\033[1m' RESET='\033[0m' # Check if version argument is provided new_version=$1 if [ -z "$new_version" ] then echo -e "${RED}${BOLD}Error:${RESET} Version argument is required" echo -e "${YELLOW}USAGE:${RESET} ./bump.sh [VERSION]" exit 1 fi # Logging functions function log_header() { local message=$1 echo "" echo -e "${BOLD}${BLUE}╔════════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD}${BLUE}║${RESET} ${CYAN}${BOLD}$message${RESET}" echo -e "${BOLD}${BLUE}╚════════════════════════════════════════════════════════╝${RESET}" echo "" } function log_step() { local step=$1 local message=$2 echo -e "${MAGENTA}${BOLD}[$step]${RESET} ${message}" } function log_success() { local message=$1 echo -e "${GREEN}${BOLD}✓${RESET} ${message}" } function log_info() { local message=$1 echo -e "${CYAN}ℹ${RESET} ${message}" } function log_error() { local message=$1 echo -e "${RED}${BOLD}✗${RESET} ${message}" } # Start release process log_header "Starting Release Process for v$new_version" # Step 1: Update crates version log_step "1/4" "Updating crates to version ${BOLD}v$new_version${RESET}" if cargo set-version "$new_version"; then log_success "Crates version updated successfully" else log_error "Failed to update crates version" exit 1 fi echo "" # Step 2: Stage changes log_step "2/4" "Staging modified files" if git add -u .; then log_success "Files staged successfully" else log_error "Failed to stage files" exit 1 fi echo "" # Step 3: Create commit and tag log_step "3/4" "Creating commit and tag" if git commit -m "Bump v$new_version"; then log_success "Commit created: ${BOLD}Bump v$new_version${RESET}" else log_error "Failed to create commit" exit 1 fi if git tag "v$new_version"; then log_success "Tag created: ${BOLD}v$new_version${RESET}" else log_error "Failed to create tag" exit 1 fi echo "" # Step 4: Push to remote log_step "4/4" "Pushing tag to remote" log_info "Pushing ${BOLD}v$new_version${RESET} to origin..." if git push origin "v$new_version"; then log_success "Tag pushed to remote successfully" else log_error "Failed to push tag to remote" exit 1 fi echo "" # Success message echo -e "${GREEN}${BOLD}╔════════════════════════════════════════════════════════╗${RESET}" echo -e "${GREEN}${BOLD}║${RESET} ${BOLD}🚀 Release v$new_version standby!${RESET}" echo -e "${GREEN}${BOLD}║${RESET} ${GREEN}Let's ship it!${RESET}" echo -e "${GREEN}${BOLD}╚════════════════════════════════════════════════════════╝${RESET}" echo "" ================================================ FILE: script/install-linux.sh ================================================ #!/usr/bin/env bash sudo apt update # Test on Ubuntu 24.04 sudo apt install -y \ gcc g++ clang libfontconfig-dev libwayland-dev \ libwebkit2gtk-4.1-dev libxkbcommon-x11-dev libx11-xcb-dev \ libssl-dev libzstd-dev \ vulkan-validationlayers libvulkan1 ================================================ FILE: script/install-window.ps1 ================================================ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression winget install Microsoft.VisualStudio.2022.Community --silent --override "--wait --quiet --add ProductLang En-us --add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended" scoop bucket add extras scoop install cmake ================================================ FILE: themes/adventure.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Adventure", "author": "iTerm2-Color-Schemes", "url": "https://github.com/mbadolato/iTerm2-Color-Schemes", "themes": [ { "name": "Adventure", "mode": "dark", "colors": { "accent.background": "#25292d", "accent.foreground": "#feffff", "background": "#040404", "border": "#282828", "danger.background": "#d84a33", "foreground": "#feffff", "input.border": "#333333", "link.active.foreground": "#417AB3", "link.foreground": "#417AB3", "link.hover.foreground": "#417AB3", "list.active.background": "#5da60222", "list.active.border": "#5da602", "list.even.background": "#0e0e0e", "muted.background": "#171717", "muted.foreground": "#5d6165", "panel.background": "#003a5b", "popover.background": "#040404", "popover.foreground": "#feffff", "primary.active.background": "#0265a799", "primary.background": "#4384ad", "primary.foreground": "#feffff", "primary.hover.background": "#417AB3", "scrollbar.background": "#003a5b00", "scrollbar.thumb.background": "#606060", "secondary.background": "#171a1c", "secondary.active.background": "#191d1e", "secondary.foreground": "#e4d5c7", "secondary.hover.background": "#1e2224", "tab.active.background": "#040404", "tab.active.foreground": "#feffff", "tab.background": "#04040400", "tab.foreground": "#677179", "tab_bar.background": "#040404", "table.background": "#040404", "table.head.foreground": "#feffffb3", "table.row.border": "#4b647970", "title_bar.background": "#0f1112", "ring": "#5da602", "base.red": "#d84a33", "base.red.light": "#d76b42", "base.green": "#5da602", "base.green.light": "#99b52c", "base.blue": "#417ab3", "base.blue.light": "#97d7ef", "base.cyan": "#41b3a9", "base.cyan.light": "#97efd6", "base.magenta": "#882252", "base.magenta.light": "#e599bc", "base.yellow": "#aa7900", "base.yellow.light": "#ffb670" }, "highlight": { "editor.foreground": "#feffff", "editor.background": "#040404", "editor.active_line.background": "#0e0e0e", "editor.line_number": "#5d6165", "editor.active_line_number": "#feffff", "editor.invisible": "#5d616566", "conflict": "#d84a33", "created": "#5da602", "deleted": "#d84a33", "error": "#d84a33", "hidden": "#5d6165", "hint": "#99b52c", "ignored": "#5d6165", "info": "#417ab3", "modified": "#eebb6e", "predictive": "#5d6165", "renamed": "#5da602", "success": "#5da602", "unreachable": "#5d6165", "warning": "#eebb6e", "syntax": { "attribute": { "color": "#eebb6e" }, "boolean": { "color": "#5da602" }, "comment": { "color": "#5d6165", "font_style": "italic" }, "comment.doc": { "color": "#5d6165", "font_style": "italic" }, "constant": { "color": "#d84a33" }, "constructor": { "color": "#eebb6e" }, "embedded": { "color": "#feffff" }, "function": { "color": "#5da602" }, "keyword": { "color": "#417ab3" }, "link_text": { "color": "#97d7ef", "font_style": "normal" }, "link_uri": { "color": "#4b6479", "font_style": "italic" }, "number": { "color": "#d84a33" }, "string": { "color": "#5da602" }, "string.escape": { "color": "#5da602" }, "string.regex": { "color": "#5da602" }, "string.special": { "color": "#eebb6e" }, "string.special.symbol": { "color": "#eebb6e" }, "tag": { "color": "#eebb6e" }, "text.literal": { "color": "#d84a33" }, "title": { "color": "#97d7ef", "font_weight": 600 }, "type": { "color": "#eebb6e" }, "property": { "color": "#feffff" }, "variable.special": { "color": "#d84a33" } } } }, { "name": "Adventure Time", "mode": "dark", "colors": { "accent.background": "#36345f", "accent.foreground": "#C7C7D4", "background": "#1f1d45", "border": "#333150", "foreground": "#C7C7D4", "input.border": "#555465", "link.active.foreground": "#4b61bf", "link.foreground": "#5f72c6", "link.hover.foreground": "#8593d3", "list.active.background": "#4ab11822", "list.active.border": "#549235", "list.even.background": "#1c1a37", "muted.background": "#29274a", "muted.foreground": "#717192", "panel.background": "#003a5b", "popover.background": "#1f1d45", "popover.foreground": "#C7C7D4", "primary.active.background": "#5f72c699", "primary.background": "#5f72c6", "primary.foreground": "#ffffff", "primary.hover.background": "#5f72c6aa", "scrollbar.background": "#003a5b00", "scrollbar.thumb.background": "#555465", "secondary.background": "#2e2c51", "secondary.active.background": "#36345f", "secondary.foreground": "#C7C7D4", "secondary.hover.background": "#34325b", "tab.active.background": "#1f1d45", "tab.active.foreground": "#C7C7D4", "tab.background": "#1f1d4500", "tab.foreground": "#aeab9e", "tab_bar.background": "#1f1d45", "table.active.background": "#4ab11811", "table.active.border": "#549235", "table.head.foreground": "#f8dcc0b3", "table.row.border": "#333333", "title_bar.background": "#1c1a3e", "title_bar.border": "#333150", "base.red": "#a02733", "base.red.light": "#fc5f5a", "base.green": "#549235", "base.green.light": "#9eff6e", "base.blue": "#2b53ab", "base.blue.light": "#1997c6", "base.cyan": "#26977b", "base.cyan.light": "#5afcd4", "base.magenta": "#665993", "base.magenta.light": "#9b5953", "base.yellow": "#ce7837", "base.yellow.light": "#efc11a" }, "highlight": { "editor.foreground": "#f8dcc0", "editor.background": "#1f1d45", "editor.active_line.background": "#36345f", "editor.line_number": "#8F8F8F", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#5d616566", "conflict": "#D2602D", "created": "#3f72e2", "hidden": "#9E9E9E", "hint": "#b283f8", "modified": "#B0A878", "predictive": "#5D5945", "syntax": { "attribute": { "color": "#be9a52" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#E1D797" }, "keyword": { "color": "#E19773" }, "link_text": { "color": "#A86D3B", "font_style": "normal" }, "link_uri": { "color": "#6F6D66", "font_style": "italic" }, "number": { "color": "#E19773" }, "string": { "color": "#76BA53" }, "string.escape": { "color": "#76BA53" }, "string.regex": { "color": "#76BA53" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#A76D3B", "font_weight": 600 }, "type": { "color": "#A86D3B" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } } ] } ================================================ FILE: themes/alduin.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Alduin", "author": "AlessandroYorba", "url": "https://github.com/AlessandroYorba/Alduin", "themes": [ { "name": "Alduin", "mode": "dark", "colors": { "accent.background": "#2e2b29", "accent.foreground": "#ebdbb2", "background": "#1C1C1C", "border": "#3a3a3a", "danger.background": "#cc241d", "danger.active.background": "#cc241d", "danger.hover.background": "#fb4934", "foreground": "#9E9E9E", "input.border": "#504945", "link.active.foreground": "#83a598", "link.foreground": "#83a598", "link.hover.foreground": "#83a598", "list.active.background": "#262626", "list.active.border": "#9E9E9E", "list.even.background": "#262626", "list.head.background": "#1C1C1C", "muted.background": "#262626", "muted.foreground": "#878787", "panel.background": "#282828", "primary.active.background": "#45858899", "primary.background": "#458588", "primary.foreground": "#F0F0F0", "primary.hover.background": "#458588AA", "scrollbar.background": "#28282800", "scrollbar.thumb.background": "#504945", "secondary.background": "#282828", "secondary.active.background": "#35322f", "secondary.foreground": "#9E9E9E", "secondary.hover.background": "#36323099", "tab.active.background": "#1C1C1C", "tab.active.foreground": "#ebdbb2", "tab.background": "#14141400", "tab.foreground": "#848382", "table.row.border": "#50494570", "title_bar.background": "#262626", "title_bar.border": "#3a3a3a", "ring": "#9E9E9E", "base.blue": "#87afaf", "base.cyan": "#878787", "base.green": "#7a875f", "base.magenta": "#af8787", "base.magenta.light": "#af878788", "base.red": "#8b5f61", "base.yellow": "#9d906c" }, "highlight": { "editor.foreground": "#DDDDDD", "editor.background": "#000000", "editor.active_line.background": "#131313", "editor.line_number": "#8F8F8F", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#9E9E9E66", "conflict": "#8b5f61", "created": "#87afaf", "error": "#8b5f61", "hidden": "#9E9E9E", "hint": "#af8787", "info": "#878787", "modified": "#B0A878", "predictive": "#5D5945", "syntax": { "attribute": { "color": "#be9a52" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#E1D797" }, "keyword": { "color": "#E19773" }, "link_text": { "color": "#A86D3B", "font_style": "normal" }, "link_uri": { "color": "#6F6D66", "font_style": "italic" }, "number": { "color": "#E19773" }, "string": { "color": "#76BA53" }, "string.escape": { "color": "#76BA53" }, "string.regex": { "color": "#76BA53" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#A76D3B", "font_weight": 600 }, "type": { "color": "#A86D3B" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } } ] } ================================================ FILE: themes/asciinema.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Asciinema", "author": "asciinema.org", "url": "https://asciinema.org", "themes": [ { "name": "Asciinema", "mode": "dark", "colors": { "accent.background": "#262829", "accent.foreground": "#cccccc", "background": "#121314", "border": "#3a3a3a", "danger.background": "#dd3c69", "danger.active.background": "#dd3c69", "danger.hover.background": "#e94f7a", "foreground": "#cccccc", "input.border": "#3a3a3a", "link.active.foreground": "#26b0d7", "link.foreground": "#26b0d7", "link.hover.foreground": "#3cc0e7", "list.active.background": "#1a1b1c", "list.active.border": "#cccccc", "list.even.background": "#1a1b1c", "list.head.background": "#121314", "muted.background": "#1a1b1c", "muted.foreground": "#6d6d6d", "panel.background": "#181919", "primary.active.background": "#26b0d799", "primary.background": "#26b0d7", "primary.foreground": "#ffffff", "primary.hover.background": "#26b0d7AA", "scrollbar.background": "#12131400", "scrollbar.thumb.background": "#2a2a2a", "secondary.background": "#181919", "secondary.active.background": "#1f2020", "secondary.foreground": "#cccccc", "secondary.hover.background": "#1f202099", "tab.active.background": "#121314", "tab.active.foreground": "#cccccc", "tab.background": "#12131400", "tab.foreground": "#6d6d6d", "table.row.border": "#2a2a2a70", "title_bar.background": "#1a1b1c", "title_bar.border": "#3a3a3a", "ring": "#5d5d5d", "base.blue": "#26b0d7", "base.cyan": "#54e1b9", "base.green": "#4ebf22", "base.magenta": "#b954e1", "base.red": "#dd3c69", "base.yellow": "#ddaf3c" }, "highlight": { "editor.foreground": "#cccccc", "editor.background": "#121314", "editor.active_line.background": "#1a1b1c", "editor.line_number": "#6d6d6d", "editor.active_line_number": "#cccccc", "editor.invisible": "#6d6d6d66", "conflict": "#dd3c69", "created": "#4ebf22", "error": "#dd3c69", "hidden": "#6d6d6d", "hint": "#b954e1", "info": "#26b0d7", "modified": "#ddaf3c", "predictive": "#2a2a2a", "syntax": { "attribute": { "color": "#ddaf3c" }, "boolean": { "color": "#b954e1" }, "comment": { "color": "#5d5d5d" }, "comment.doc": { "color": "#5d5d5d" }, "constant": { "color": "#b954e1" }, "constructor": { "color": "#26b0d7" }, "embedded": { "color": "#cccccc" }, "function": { "color": "#54e1b9" }, "keyword": { "color": "#dd3c69" }, "link_text": { "color": "#26b0d7", "font_style": "normal" }, "link_uri": { "color": "#54e1b9", "font_style": "italic" }, "number": { "color": "#b954e1" }, "string": { "color": "#4ebf22" }, "string.escape": { "color": "#54e1b9" }, "string.regex": { "color": "#4ebf22" }, "string.special": { "color": "#ddaf3c" }, "string.special.symbol": { "color": "#ddaf3c" }, "tag": { "color": "#dd3c69" }, "text.literal": { "color": "#cccccc" }, "title": { "color": "#ddaf3c", "font_weight": 600 }, "type": { "color": "#26b0d7" }, "property": { "color": "#cccccc" }, "variable.special": { "color": "#dd3c69" } } } } ] } ================================================ FILE: themes/ayu.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Ayu Light", "author": "Zed Industries", "url": "https://github.com/zed-industries/zed/blob/e62dd2a0e584154886ff86cc1e9e3e060558b977/assets/themes/ayu", "themes": [ { "name": "Ayu Light", "mode": "light", "colors": { "accent.background": "#dadadc", "accent.foreground": "#5C6773", "background": "#FCFCFC", "foreground": "#5c6166", "border": "#cfd1d2ff", "ring": "#FF9940", "input.border": "#D9DCE3", "list.active.background": "#55B4D422", "list.active.border": "#55B4D4", "list.even.background": "#E6E6E6", "list.head.background": "#F4F4F599", "muted.background": "#F4F4F5", "muted.foreground": "#99a0a6", "panel.background": "#F3F4F5", "popover.background": "#ECECED", "popover.foreground": "#5C6773", "primary.active.background": "#42accf", "primary.background": "#55b4d3", "primary.foreground": "#FAFAFA", "primary.hover.background": "#5fb9d7", "scrollbar.background": "#FAFAFA00", "scrollbar.thumb.background": "#5c61664c", "secondary.active.background": "#CECECE", "secondary.background": "#ECECED", "secondary.foreground": "#5C6773", "secondary.hover.background": "#e0e0e0", "tab.active.background": "#FCFCFC", "tab.active.foreground": "#5C6773", "tab.background": "#ECECED00", "tab_bar.background": "#F4F4F5", "title_bar.background": "#ECECED", "title_bar.border": "#CFD1D2", "base.yellow": "#F1AD49", "base.red": "#F07171", "base.blue": "#55b4d3", "base.green": "#85b304", "base.magenta": "#9371f0", "base.cyan": "#4dbf99" }, "highlight": { "editor.foreground": "#5C6773", "editor.background": "#FCFCFC", "editor.active_line.background": "#ECECED", "editor.line_number": "#ABB0B6", "editor.active_line_number": "#5C6773", "editor.invisible": "#73777b66", "conflict": "#f1ad49ff", "conflict.background": "#ffeedaff", "conflict.border": "#ffe1beff", "created": "#85b304ff", "created.background": "#e9efd2ff", "created.border": "#d7e3aeff", "deleted": "#ef7271ff", "deleted.background": "#ffe3e1ff", "deleted.border": "#ffcdcaff", "error": "#ef7271ff", "error.background": "#ffe3e1ff", "error.border": "#ffcdcaff", "hidden": "#a9acaeff", "hidden.background": "#dcdddeff", "hidden.border": "#d5d6d8ff", "hint": "#8ca7c2ff", "hint.background": "#deebfaff", "hint.border": "#c4daf6ff", "ignored": "#a9acaeff", "ignored.background": "#dcdddeff", "ignored.border": "#cfd1d2ff", "info": "#3b9ee5ff", "info.background": "#deebfaff", "info.border": "#c4daf6ff", "modified": "#f1ad49ff", "modified.background": "#ffeedaff", "modified.border": "#ffe1beff", "predictive": "#9eb9d3ff", "predictive.background": "#e9efd2ff", "predictive.border": "#d7e3aeff", "renamed": "#3b9ee5ff", "renamed.background": "#deebfaff", "renamed.border": "#c4daf6ff", "success": "#85b304ff", "success.background": "#e9efd2ff", "success.border": "#d7e3aeff", "unreachable": "#8b8e92ff", "unreachable.background": "#dcdddeff", "unreachable.border": "#cfd1d2ff", "warning": "#f1ad49ff", "warning.background": "#ffeedaff", "warning.border": "#ffe1beff", "syntax": { "attribute": { "color": "#3b9ee5ff" }, "boolean": { "color": "#a37accff" }, "comment": { "color": "#898d90ff" }, "comment.doc": { "color": "#898d90ff" }, "constant": { "color": "#a37accff" }, "constructor": { "color": "#3b9ee5ff" }, "embedded": { "color": "#5c6166ff" }, "emphasis": { "color": "#3b9ee5ff" }, "emphasis.strong": { "color": "#3b9ee5ff", "font_weight": 700 }, "enum": { "color": "#f98d3fff" }, "function": { "color": "#f2ad48ff" }, "hint": { "color": "#8ca7c2ff", "font_weight": 700 }, "keyword": { "color": "#fa8d3eff" }, "label": { "color": "#3b9ee5ff" }, "link_text": { "color": "#f98d3fff", "font_style": "italic" }, "link_uri": { "color": "#85b304ff" }, "namespace": { "color": "#5c6166ff" }, "number": { "color": "#a37accff" }, "operator": { "color": "#ed9365ff" }, "predictive": { "color": "#9eb9d3ff", "font_style": "italic" }, "preproc": { "color": "#5c6166ff" }, "primary": { "color": "#5c6166ff" }, "property": { "color": "#3b9ee5ff" }, "punctuation": { "color": "#73777bff" }, "punctuation.bracket": { "color": "#73777bff" }, "punctuation.delimiter": { "color": "#73777bff" }, "punctuation.list_marker": { "color": "#73777bff" }, "punctuation.markup": { "color": "#73777bff" }, "punctuation.special": { "color": "#a37accff" }, "selector": { "color": "#a37accff" }, "selector.pseudo": { "color": "#3b9ee5ff" }, "string": { "color": "#86b300ff" }, "string.escape": { "color": "#898d90ff" }, "string.regex": { "color": "#4bbf98ff" }, "string.special": { "color": "#e6ba7eff" }, "string.special.symbol": { "color": "#f98d3fff" }, "tag": { "color": "#3b9ee5ff" }, "text.literal": { "color": "#f98d3fff" }, "title": { "color": "#5c6166ff", "font_weight": 700 }, "type": { "color": "#389ee6ff" }, "variable": { "color": "#5c6166ff" }, "variant": { "color": "#3b9ee5ff" } } } }, { "name": "Ayu Dark", "mode": "dark", "colors": { "accent.background": "#20242b", "accent.foreground": "#B3B1AD", "background": "#0D1016", "border": "#292a2c", "ring": "#FFB454", "foreground": "#B3B1AD", "input.border": "#2D2F34", "list.active.background": "#36A3D922", "list.active.border": "#36A3D9", "list.even.background": "#191F2A99", "muted.background": "#16191F", "muted.foreground": "#52514f", "popover.background": "#0D1016", "popover.foreground": "#B3B1AD", "primary.active.background": "#36A3D9", "primary.background": "#5ac1fe", "primary.foreground": "#1F2430", "primary.hover.background": "#3DAEE9", "scrollbar.background": "#0A0E1400", "scrollbar.thumb.background": "#bfbdb64c", "secondary.active.background": "#26282f", "secondary.background": "#1F2127", "secondary.foreground": "#B3B1AD", "secondary.hover.background": "#26282f99", "tab.active.foreground": "#B3B1AD", "tab.active.background": "#0D1016", "tab_bar.background": "#16191F", "title_bar.background": "#16191F", "title_bar.border": "#38393b", "base.yellow": "#FEB454", "base.red": "#ef7177", "base.blue": "#5ac1fe", "base.green": "#aad84c", "base.magenta": "#d2a6ff", "base.cyan": "#5a728b" }, "highlight": { "editor.foreground": "#bfbdb6", "editor.background": "#0d1016", "editor.active_line.background": "#1f2127bf", "editor.line_number": "#4b4c4e", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#73777b66", "conflict": "#feb454ff", "conflict.background": "#572815ff", "conflict.border": "#754221ff", "created": "#aad84cff", "created.background": "#294113ff", "created.border": "#405c1cff", "deleted": "#ef7177ff", "deleted.background": "#48161bff", "deleted.border": "#66272dff", "error": "#ef7177ff", "error.background": "#48161bff", "error.border": "#66272dff", "hidden": "#696a6aff", "hidden.background": "#313337ff", "hidden.border": "#383a3eff", "hint": "#628b80ff", "hint.background": "#0d2f4eff", "hint.border": "#1b4a6eff", "ignored": "#696a6aff", "ignored.background": "#313337ff", "ignored.border": "#3f4043ff", "info": "#5ac1feff", "info.background": "#0d2f4eff", "info.border": "#1b4a6eff", "modified": "#feb454ff", "modified.background": "#572815ff", "modified.border": "#754221ff", "predictive": "#5a728bff", "predictive.background": "#294113ff", "predictive.border": "#405c1cff", "renamed": "#5ac1feff", "renamed.background": "#0d2f4eff", "renamed.border": "#1b4a6eff", "success": "#aad84cff", "success.background": "#294113ff", "success.border": "#405c1cff", "unreachable": "#8a8986ff", "unreachable.background": "#313337ff", "unreachable.border": "#3f4043ff", "warning": "#feb454ff", "warning.background": "#572815ff", "warning.border": "#754221ff", "syntax": { "attribute": { "color": "#5ac1feff" }, "boolean": { "color": "#d2a6ffff" }, "comment": { "color": "#8c8b88ff" }, "comment.doc": { "color": "#8c8b88ff" }, "constant": { "color": "#d2a6ffff" }, "constructor": { "color": "#5ac1feff" }, "embedded": { "color": "#bfbdb6ff" }, "emphasis": { "color": "#5ac1feff" }, "emphasis.strong": { "color": "#5ac1feff", "font_weight": 700 }, "enum": { "color": "#fe8f40ff" }, "function": { "color": "#ffb353ff" }, "hint": { "color": "#628b80ff", "font_weight": 700 }, "keyword": { "color": "#ff8f3fff" }, "label": { "color": "#5ac1feff" }, "link_text": { "color": "#fe8f40ff", "font_style": "italic" }, "link_uri": { "color": "#aad84cff" }, "namespace": { "color": "#bfbdb6ff" }, "number": { "color": "#d2a6ffff" }, "operator": { "color": "#f29668ff" }, "predictive": { "color": "#5a728bff", "font_style": "italic" }, "preproc": { "color": "#bfbdb6ff" }, "primary": { "color": "#bfbdb6ff" }, "property": { "color": "#5ac1feff" }, "punctuation": { "color": "#a6a5a0ff" }, "punctuation.bracket": { "color": "#a6a5a0ff" }, "punctuation.delimiter": { "color": "#a6a5a0ff" }, "punctuation.list_marker": { "color": "#a6a5a0ff" }, "punctuation.markup": { "color": "#a6a5a0ff" }, "punctuation.special": { "color": "#d2a6ffff" }, "selector": { "color": "#d2a6ffff" }, "selector.pseudo": { "color": "#5ac1feff" }, "string": { "color": "#a9d94bff" }, "string.escape": { "color": "#8c8b88ff" }, "string.regex": { "color": "#95e6cbff" }, "string.special": { "color": "#e5b572ff" }, "string.special.symbol": { "color": "#fe8f40ff" }, "tag": { "color": "#5ac1feff" }, "text.literal": { "color": "#fe8f40ff" }, "title": { "color": "#bfbdb6ff", "font_weight": 700 }, "type": { "color": "#59c2ffff" }, "variable": { "color": "#bfbdb6ff" }, "variant": { "color": "#5ac1feff" } } } } ] } ================================================ FILE: themes/catppuccin.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Catppuccin", "author": "Catppuccino", "url": "https://github.com/catppuccin/catppuccin", "themes": [ { "name": "Catppuccin Latte", "mode": "light", "colors": { "accent.background": "#d3d8e0", "accent.foreground": "#4c4f69", "background": "#E5E9EF", "border": "#CCD0DA", "ring": "#7287fd", "foreground": "#4c4f69", "input.border": "#acb0be", "link.active.foreground": "#7287fd", "link.foreground": "#7287fd", "link.hover.foreground": "#7287fd", "list.active.background": "#7287fd22", "list.active.border": "#7287fd", "list.even.background": "#EFF1F5", "list.head.background": "#dce0e8", "muted.background": "#dce0e8", "muted.foreground": "#9a9db2", "panel.background": "#dce0e8", "primary.active.background": "#7287fd", "primary.background": "#7287fd", "primary.foreground": "#EFF1F5", "scrollbar.background": "#EFF1F500", "scrollbar.thumb.background": "#acb0be", "secondary.active.background": "#CCD2DE", "secondary.background": "#dce0e8", "secondary.foreground": "#4c4f69", "secondary.hover.background": "#CCD2DE99", "tab.active.background": "#E5E9EF", "tab.active.foreground": "#4c4f69", "tab.background": "#D2D7E200", "tab.foreground": "#82848c", "tab_bar.background": "#DCE0E8", "title_bar.background": "#DCE0E8", "title_bar.border": "#bec3d0", "base.red": "#d26a53", "base.green": "#5aa93b", "base.yellow": "#df8e1d", "base.blue": "#78acdc", "base.magenta": "#8778dc", "base.magenta.light": "#9978dc66", "base.cyan": "#53d2b0" }, "highlight": { "editor.foreground": "#4c4f69", "editor.background": "#EFF1F5", "editor.active_line.background": "#dce0e8", "editor.line_number": "#9a9db2", "editor.active_line_number": "#4c4f69", "editor.invisible": "#7c7f9366", "conflict": "#d20f39", "created": "#85dc78", "deleted": "#dc8a78", "error": "#d20f39", "hidden": "#9a9db2", "hint": "#7287fd", "ignored": "#acb0be", "info": "#1e66f5", "modified": "#df8e1d", "predictive": "#9a9db2", "renamed": "#9978dc", "success": "#85dc78", "unreachable": "#acb0be", "warning": "#df8e1d", "syntax": { "variable": { "color": "#4c4f69" }, "variable.builtin": { "color": "#d20f39" }, "variable.parameter": { "color": "#e64553" }, "variable.member": { "color": "#1e66f5" }, "variable.special": { "color": "#d20f39", "font_style": "italic" }, "constant": { "color": "#fe640b" }, "constant.builtin": { "color": "#fe640b" }, "constant.macro": { "color": "#8839ef" }, "module": { "color": "#df8e1d", "font_style": "italic" }, "label": { "color": "#209fb5" }, "string": { "color": "#85dc78" }, "string.documentation": { "color": "#179299" }, "string.regexp": { "color": "#fe640b" }, "string.escape": { "color": "#ea76cb" }, "string.special": { "color": "#ea76cb" }, "string.special.path": { "color": "#ea76cb" }, "string.special.symbol": { "color": "#dd7878" }, "string.special.url": { "color": "#dc8a78", "font_style": "italic" }, "character": { "color": "#179299" }, "character.special": { "color": "#ea76cb" }, "boolean": { "color": "#fe640b" }, "number": { "color": "#fe640b" }, "number.float": { "color": "#fe640b" }, "type": { "color": "#df8e1d" }, "type.builtin": { "color": "#8839ef", "font_style": "italic" }, "type.definition": { "color": "#df8e1d" }, "type.interface": { "color": "#df8e1d", "font_style": "italic" }, "type.super": { "color": "#df8e1d", "font_style": "italic" }, "attribute": { "color": "#fe640b" }, "property": { "color": "#1e66f5" }, "function": { "color": "#1e66f5" }, "function.builtin": { "color": "#fe640b" }, "function.call": { "color": "#1e66f5" }, "function.macro": { "color": "#179299" }, "function.method": { "color": "#1e66f5" }, "function.method.call": { "color": "#1e66f5" }, "constructor": { "color": "#dd7878" }, "operator": { "color": "#04a5e5" }, "keyword": { "color": "#8839ef" }, "keyword.modifier": { "color": "#8839ef" }, "keyword.type": { "color": "#8839ef" }, "keyword.coroutine": { "color": "#8839ef" }, "keyword.function": { "color": "#8839ef" }, "keyword.operator": { "color": "#8839ef" }, "keyword.import": { "color": "#8839ef" }, "keyword.repeat": { "color": "#8839ef" }, "keyword.return": { "color": "#8839ef" }, "keyword.debug": { "color": "#8839ef" }, "keyword.exception": { "color": "#8839ef" }, "keyword.conditional": { "color": "#8839ef" }, "keyword.conditional.ternary": { "color": "#8839ef" }, "keyword.directive": { "color": "#ea76cb" }, "keyword.directive.define": { "color": "#ea76cb" }, "keyword.export": { "color": "#04a5e5" }, "punctuation": { "color": "#7c7f93" }, "punctuation.delimiter": { "color": "#7c7f93" }, "punctuation.bracket": { "color": "#7c7f93" }, "punctuation.special": { "color": "#ea76cb" }, "punctuation.special.symbol": { "color": "#dd7878" }, "punctuation.list_marker": { "color": "#179299" }, "comment": { "color": "#7c7f93", "font_style": "italic" }, "comment.doc": { "color": "#7c7f93", "font_style": "italic" }, "comment.documentation": { "color": "#7c7f93", "font_style": "italic" }, "comment.error": { "color": "#d20f39", "font_style": "italic" }, "comment.warning": { "color": "#df8e1d", "font_style": "italic" }, "comment.hint": { "color": "#1e66f5", "font_style": "italic" }, "comment.todo": { "color": "#dd7878", "font_style": "italic" }, "comment.note": { "color": "#dc8a78", "font_style": "italic" }, "diff.plus": { "color": "#40a02b" }, "diff.minus": { "color": "#d20f39" }, "tag": { "color": "#1e66f5" }, "tag.attribute": { "color": "#df8e1d", "font_style": "italic" }, "tag.delimiter": { "color": "#179299" }, "parameter": { "color": "#e64553" }, "field": { "color": "#7287fd" }, "namespace": { "color": "#df8e1d", "font_style": "italic" }, "float": { "color": "#fe640b" }, "symbol": { "color": "#ea76cb" }, "string.regex": { "color": "#fe640b" }, "text": { "color": "#4c4f69" }, "emphasis.strong": { "color": "#e64553", "font_weight": 700 }, "emphasis": { "color": "#e64553", "font_style": "italic" }, "embedded": { "color": "#e64553" }, "text.literal": { "color": "#40a02b" }, "concept": { "color": "#209fb5" }, "enum": { "color": "#179299", "font_weight": 700 }, "function.decorator": { "color": "#fe640b" }, "type.class.definition": { "color": "#df8e1d", "font_weight": 700 }, "hint": { "color": "#acb0be", "font_style": "italic" }, "link_text": { "color": "#7287fd" }, "link_uri": { "color": "#1e66f5", "font_style": "italic" }, "parent": { "color": "#fe640b" }, "predictive": { "color": "#9ca0b0" }, "predoc": { "color": "#d20f39" }, "primary": { "color": "#e64553" }, "tag.doctype": { "color": "#8839ef" }, "string.doc": { "color": "#179299", "font_style": "italic" }, "title": { "color": "#4c4f69", "font_weight": 800 }, "variant": { "color": "#d20f39" } } } }, { "name": "Catppuccin Frappe", "mode": "dark", "colors": { "accent.background": "#3b3f4f", "accent.foreground": "#c6d0f5", "background": "#232634", "border": "#3e4255", "ring": "#f2d5cf", "foreground": "#c6d0f5", "input.border": "#51576d", "list.active.background": "#8caaee15", "list.even.background": "#303446", "list.head.background": "#2A2C3C", "muted.background": "#2c303f", "muted.foreground": "#979db5", "panel.background": "#414559", "popover.background": "#1E202B", "popover.foreground": "#c6d0f5", "primary.active.background": "#8caaee", "primary.background": "#8caaee", "primary.foreground": "#232634", "primary.hover.background": "#8caaee", "scrollbar.background": "#30344600", "scrollbar.thumb.background": "#51576d", "secondary.active.background": "#313445", "secondary.background": "#414559", "secondary.foreground": "#c6d0f5", "secondary.hover.background": "#31344599", "tab.active.background": "#232634", "tab.active.foreground": "#dce2f9", "tab.background": "#1E202B00", "tab.foreground": "#abb5d9", "tab_bar.background": "#1E202B", "title_bar.background": "#1D202B", "title_bar.border": "#30354b", "base.red": "#e78284", "base.green": "#a6d189", "base.yellow": "#e7d682", "base.blue": "#8caaee", "base.magenta": "#9882e7", "base.magenta.light": "#9882e766", "base.cyan": "#81c8be" }, "highlight": { "editor.foreground": "#c6d0f5", "editor.background": "#232634", "editor.active_line.background": "#41455977", "editor.line_number": "#979db5", "editor.active_line_number": "#c6d0f5", "editor.invisible": "#7c7f9366", "conflict": "#e78284", "created": "#a6d189", "deleted": "#3b2e2e", "error": "#3b2e2e", "hidden": "#51576d", "hint": "#8caaee", "info": "#1e2b4d", "modified": "#e7d682", "predictive": "#51576d", "syntax": { "attribute": { "color": "#e7d682" }, "boolean": { "color": "#e78284" }, "comment": { "color": "#51576d" }, "comment.doc": { "color": "#51576d" }, "constant": { "color": "#e7d682" }, "constructor": { "color": "#a6d189" }, "embedded": { "color": "#c6d0f5" }, "function": { "color": "#8caaee" }, "keyword": { "color": "#e78284" }, "link_text": { "color": "#8caaee", "font_style": "normal" }, "link_uri": { "color": "#51576d", "font_style": "italic" }, "number": { "color": "#e78284" }, "string": { "color": "#a6d189" }, "string.escape": { "color": "#a6d189" }, "string.regex": { "color": "#a6d189" }, "string.special": { "color": "#e7d682" }, "string.special.symbol": { "color": "#e7d682" }, "tag": { "color": "#8caaee" }, "text.literal": { "color": "#c6d0f5" }, "title": { "color": "#e78284", "font_weight": 600 }, "type": { "color": "#8caaee" }, "property": { "color": "#c6d0f5" }, "variable.special": { "color": "#e78284" } } } }, { "name": "Catppuccin Macchiato", "mode": "dark", "colors": { "accent.background": "#30344d", "accent.foreground": "#cad3f5", "action_bar.background": "#1E2030", "background": "#1E2030", "border": "#494d64", "ring": "#f4dbd6", "danger.background": "#ed8796", "foreground": "#cad3f5", "input.border": "#494d64", "link.active.foreground": "#8aadf4", "link.foreground": "#8aadf4", "link.hover.foreground": "#8aadf4", "list.active.background": "#8aadf420", "list.active.border": "#8aadf4", "list.even.background": "#24273a", "list.head.background": "#363a4f33", "muted.background": "#282a3a", "muted.foreground": "#b8c0e0", "popover.foreground": "#cad3f5", "primary.active.background": "#8aadf499", "primary.background": "#8aadf4", "primary.foreground": "#24273a", "primary.hover.background": "#8aadf4", "scrollbar.background": "#24273a00", "scrollbar.thumb.background": "#494d64", "secondary.active.background": "#3a3f55", "secondary.background": "#363a4f", "secondary.foreground": "#cad3f5", "secondary.hover.background": "#24273a99", "tab.background": "#17192600", "tab.foreground": "#aeb8db", "tab_bar.background": "#171926", "title_bar.background": "#171926", "title_bar.border": "#363A4F", "base.red": "#ed8796", "base.green": "#a6da95", "base.yellow": "#eed49f", "base.blue": "#8aadf4", "base.magenta": "#f5bde6", "base.magenta.light": "#f5bde666", "base.cyan": "#8bd5ca" }, "highlight": { "editor.foreground": "#cad3f5", "editor.background": "#1E2030", "editor.active_line.background": "#363a4f", "editor.line_number": "#b8c0e0", "editor.active_line_number": "#cad3f5", "editor.invisible": "#7c7f9366", "conflict": "#ed8796", "created": "#a6da95", "deleted": "#ed8796", "error": "#ed8796", "hidden": "#b8c0e0", "hint": "#8aadf4", "ignored": "#494d64", "info": "#8aadf4", "modified": "#eed49f", "predictive": "#b8c0e0", "renamed": "#f5bde6", "success": "#a6da95", "unreachable": "#b8c0e0", "warning": "#eed49f", "syntax": { "attribute": { "color": "#eed49f" }, "boolean": { "color": "#f5bde6" }, "comment": { "color": "#b8c0e0", "font_style": "italic" }, "comment.doc": { "color": "#b8c0e0", "font_style": "italic" }, "constant": { "color": "#eed49f" }, "constructor": { "color": "#8aadf4" }, "embedded": { "color": "#cad3f5" }, "function": { "color": "#8aadf4" }, "keyword": { "color": "#f5bde6" }, "link_text": { "color": "#8aadf4", "font_style": "normal" }, "link_uri": { "color": "#b8c0e0", "font_style": "italic" }, "number": { "color": "#eed49f" }, "string": { "color": "#a6da95" }, "string.escape": { "color": "#a6da95" }, "string.regex": { "color": "#a6da95" }, "string.special": { "color": "#eed49f" }, "string.special.symbol": { "color": "#eed49f" }, "tag": { "color": "#8aadf4" }, "text.literal": { "color": "#cad3f5" }, "title": { "color": "#f5bde6", "font_weight": 600 }, "type": { "color": "#8aadf4" }, "property": { "color": "#cad3f5" }, "variable.special": { "color": "#f5bde6" } } } }, { "name": "Catppuccin Mocha", "mode": "dark", "colors": { "accent.background": "#2e2e3e", "accent.foreground": "#cdd6f4", "background": "#181825", "border": "#313244", "ring": "#cba6f7", "danger.background": "#f38ba8", "danger.active.background": "#eba0ac", "danger.hover.background": "#f38ba8", "foreground": "#cdd6f4", "input.border": "#6c7086", "link.active.foreground": "#89b4fa", "link.foreground": "#89b4fa", "link.hover.foreground": "#89b4fa", "list.active.background": "#89b4fa15", "list.active.border": "#89b4fa77", "list.hover.background": "#202031", "list.even.background": "#161622", "list.head.background": "#1E1E2E", "muted.background": "#302d41", "muted.foreground": "#6c7086", "panel.background": "#302d41", "popover.background": "#1e1e2e", "popover.foreground": "#cdd6f4", "primary.active.background": "#89b4fa", "primary.background": "#89b4fa", "primary.foreground": "#1e1e2e", "primary.hover.background": "#74c7ec", "scrollbar.background": "#1e1e2e00", "scrollbar.thumb.background": "#4e4e5e", "secondary.active.background": "#45475a", "secondary.background": "#302d41", "secondary.foreground": "#cdd6f4", "secondary.hover.background": "#575268", "tab.background": "#0B0B1100", "tab.foreground": "#aeb7d5", "tab_bar.background": "#0B0B11", "title_bar.background": "#11111B", "title_bar.border": "#313244", "base.red": "#f38ba8", "base.green": "#a6e3a1", "base.yellow": "#f9e2af", "base.blue": "#89b4fa", "base.magenta": "#f5c2e7", "base.magenta.light": "#f38ba866", "base.cyan": "#94e2d5" }, "highlight": { "editor.foreground": "#cdd6f4", "editor.background": "#181825", "editor.active_line.background": "#222230AA", "editor.line_number": "#6c7086", "editor.active_line_number": "#cdd6f4", "editor.invisible": "#7c7f9366", "conflict": "#f38ba8", "created": "#a6e3a1", "deleted": "#f38ba8", "error": "#f38ba8", "hidden": "#6c7086", "hint": "#89b4fa", "ignored": "#6c7086", "info": "#89b4fa", "modified": "#f9e2af", "predictive": "#6c7086", "renamed": "#f5c2e7", "success": "#a6e3a1", "unreachable": "#6c7086", "warning": "#f9e2af", "syntax": { "attribute": { "color": "#f9e2af" }, "boolean": { "color": "#f5c2e7" }, "comment": { "color": "#6c7086", "font_style": "italic" }, "comment.doc": { "color": "#6c7086", "font_style": "italic" }, "constant": { "color": "#f9e2af" }, "constructor": { "color": "#89b4fa" }, "embedded": { "color": "#cdd6f4" }, "function": { "color": "#89b4fa" }, "keyword": { "color": "#f5c2e7" }, "link_text": { "color": "#89b4fa", "font_style": "normal" }, "link_uri": { "color": "#6c7086", "font_style": "italic" }, "number": { "color": "#f9e2af" }, "string": { "color": "#a6e3a1" }, "string.escape": { "color": "#a6e3a1" }, "string.regex": { "color": "#a6e3a1" }, "string.special": { "color": "#f9e2af" }, "string.special.symbol": { "color": "#f9e2af" }, "tag": { "color": "#89b4fa" }, "text.literal": { "color": "#cdd6f4" }, "title": { "color": "#f5c2e7", "font_weight": 600 }, "type": { "color": "#89b4fa" }, "property": { "color": "#cdd6f4" }, "variable.special": { "color": "#f5c2e7" } } } } ] } ================================================ FILE: themes/everforest.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Everforest", "author": "sainnhe", "url": "https://github.com/sainnhe/everforest", "themes": [ { "name": "Everforest Light", "mode": "light", "colors": { "accent.background": "#E7E5D4", "accent.foreground": "#5F6D75", "background": "#FEFCEE", "border": "#E7E5D4", "danger.background": "#e67e80", "danger.foreground": "#ffffff", "foreground": "#5F6D75", "input.border": "#E7E5D4", "link.active.foreground": "#7fbbb3", "link.foreground": "#7fbbb3", "link.hover.foreground": "#7fbbb3", "list.active.background": "#a7c08025", "list.active.border": "#a7c080", "list.even.background": "#F4F1E2", "list.head.background": "#F4F1E2", "muted.background": "#f1f0e2", "muted.foreground": "#959a9d", "popover.background": "#FEFCEE", "popover.foreground": "#5F6D75", "primary.background": "#e69875", "primary.foreground": "#FEFCEE", "scrollbar.background": "#e0e0e000", "scrollbar.thumb.background": "#D7D5C4", "secondary.background": "#EEEADA", "secondary.foreground": "#5F6D75", "tab.active.background": "#FEFCEE", "tab.active.foreground": "#5F6D75", "tab.background": "#F4F1E200", "tab.foreground": "#6b7b84", "tab_bar.background": "#F4F1E2", "title_bar.background": "#F9F5E4", "title_bar.border": "#E7E5D4", "base.red": "#e67e80", "base.green": "#a7c080", "base.yellow": "#dbbc7f", "base.blue": "#7fbbb3", "base.magenta": "#d699b6", "base.magenta.light": "#d699b699", "base.cyan": "#83c092" }, "highlight": { "editor.foreground": "#5F6D75", "editor.background": "#FEFCEE", "editor.active_line.background": "#E7E5D4", "editor.line_number": "#959a9d", "editor.active_line_number": "#5F6D75", "editor.invisible": "#959a9d66", "conflict": "#e67e80", "created": "#a7c080", "deleted": "#e67e80", "error": "#e67e80", "hidden": "#959a9d", "hint": "#7fbbb3", "ignored": "#959a9d", "info": "#7fbbb3", "modified": "#dbbc7f", "predictive": "#5F6D75", "renamed": "#a7c080", "success": "#a7c080", "unreachable": "#959a9d", "warning": "#dbbc7f", "syntax": { "attribute": { "color": "#dbbc7f" }, "boolean": { "color": "#e69875" }, "comment": { "color": "#959a9d", "font_style": "italic" }, "comment.doc": { "color": "#959a9d", "font_style": "italic" }, "constant": { "color": "#e69875" }, "constructor": { "color": "#a7c080" }, "embedded": { "color": "#5F6D75" }, "function": { "color": "#a7c080" }, "keyword": { "color": "#e67e80" }, "link_text": { "color": "#7fbbb3", "font_style": "normal" }, "link_uri": { "color": "#5F6D75", "font_style": "italic" }, "number": { "color": "#e69875" }, "string": { "color": "#a7c080" }, "string.escape": { "color": "#a7c080" }, "string.regex": { "color": "#a7c080" }, "string.special": { "color": "#dbbc7f" }, "string.special.symbol": { "color": "#dbbc7f" }, "tag": { "color": "#a7c080" }, "text.literal": { "color": "#dbbc7f" }, "title": { "color": "#e69875", "font_weight": 600 }, "type": { "color": "#7fbbb3" }, "property": { "color": "#5F6D75" }, "variable.special": { "color": "#e67e80" } } } }, { "name": "Everforest Dark", "mode": "dark", "colors": { "accent.background": "#3c4448", "accent.foreground": "#d3c6aa", "background": "#262E34", "border": "#40484c", "foreground": "#d3c6aa", "input.border": "#485156", "link.active.foreground": "#7fbbb3", "link.foreground": "#7fbbb3", "link.hover.foreground": "#7fbbb3", "list.active.background": "#a7c08022", "list.active.border": "#a7c08088", "list.even.background": "#2E383B", "list.head.background": "#2E383B", "muted.background": "#2E383B", "muted.foreground": "#6D7873", "panel.background": "#2E383B", "popover.background": "#262E34", "popover.foreground": "#d3c6aa", "primary.background": "#e69875", "primary.foreground": "#262E34", "scrollbar.background": "#2E383B00", "scrollbar.thumb.background": "#485156", "secondary.background": "#2E383B", "secondary.foreground": "#849087", "secondary.active.background": "#303b3e", "secondary.hover.background": "#3E474B99", "tab.active.background": "#262E34", "tab.active.foreground": "#d3c6aa", "tab.background": "#262E3400", "tab.foreground": "#849087", "tab_bar.background": "#2E383B", "title_bar.background": "#1f262b", "title_bar.border": "#40484c", "base.red": "#e67e80", "base.green": "#a7c080", "base.yellow": "#dbbc7f", "base.blue": "#7fbbb3", "base.magenta": "#844d64", "base.magenta.light": "#844d6499", "base.cyan": "#83c092" }, "highlight": { "editor.foreground": "#d3c6aa", "editor.background": "#262E34", "editor.active_line.background": "#3c4448", "editor.line_number": "#8F8F8F", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#959a9d66", "conflict": "#D2602D", "created": "#3f72e2", "hidden": "#9E9E9E", "hint": "#b283f8", "modified": "#B0A878", "predictive": "#5D5945", "syntax": { "attribute": { "color": "#be9a52" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#E1D797" }, "keyword": { "color": "#E19773" }, "link_text": { "color": "#A86D3B", "font_style": "normal" }, "link_uri": { "color": "#6F6D66", "font_style": "italic" }, "number": { "color": "#E19773" }, "string": { "color": "#76BA53" }, "string.escape": { "color": "#76BA53" }, "string.regex": { "color": "#76BA53" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#A76D3B", "font_weight": 600 }, "type": { "color": "#A86D3B" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } } ] } ================================================ FILE: themes/fahrenheit.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Fahrenheit", "author": "iTerm2-Color-Schemes", "url": "https://github.com/mbadolato/iTerm2-Color-Schemes", "themes": [ { "name": "Fahrenheit", "mode": "dark", "colors": { "accent.background": "#171717", "accent.foreground": "#FFFFCE", "background": "#000000", "border": "#252525", "danger.background": "#593002", "foreground": "#FFFFCE", "input.border": "#252525", "list.active.background": "#72020222", "list.active.border": "#720202", "list.even.background": "#090909", "list.hover.background": "#111111", "muted.background": "#20202099", "muted.foreground": "#828282", "panel.background": "#1e1e1e", "popover.background": "#090909", "popover.foreground": "#FFFFCE", "primary.background": "#720202", "primary.foreground": "#FFFFCE", "scrollbar.background": "#1e1e1e00", "scrollbar.thumb.background": "#3e3e3e", "secondary.background": "#202020", "secondary.active.background": "#202020DD", "secondary.foreground": "#979797", "secondary.hover.background": "#20202099", "tab.foreground": "#808080", "title_bar.background": "#0e0e0e", "table.background": "#1e1e1e00", "table.hover.background": "#1E1E1E", "ring": "#570101", "base.red": "#723202", "base.red.light": "#c97636", "base.green": "#027225", "base.green.light": "#39c936", "base.yellow": "#726302", "base.yellow.light": "#c9b536", "base.blue": "#022d72", "base.blue.light": "#0551cb", "base.magenta": "#3a286c", "base.magenta.light": "#58197a", "base.cyan": "#027272", "base.cyan.light": "#36c9c9" }, "highlight": { "editor.foreground": "#FFFFCE", "editor.background": "#000000", "editor.active_line.background": "#1e1e1e", "editor.line_number": "#828282", "editor.invisible": "#82828266", "warning.border": "#720202", "syntax": { "attribute": { "color": "#fecf75" }, "boolean": { "color": "#fd9f4d" }, "comment": { "color": "#828282" }, "comment.doc": { "color": "#828282" }, "constant": { "color": "#fd9f4d" }, "constructor": { "color": "#cb4a05" }, "embedded": { "color": "#dcdcdc" }, "function": { "color": "#fd9f4d" }, "keyword": { "color": "#cb4a05" }, "link_text": { "color": "#cda074", "font_style": "normal" }, "link_uri": { "color": "#9e744d", "font_style": "italic" }, "number": { "color": "#cb4a05" }, "string": { "color": "#cc734d" }, "string.escape": { "color": "#cc734d" }, "string.regex": { "color": "#cc734d" }, "string.special": { "color": "#fd9f4d" }, "string.special.symbol": { "color": "#fd9f4d" }, "tag": { "color": "#cb4a05" }, "text.literal": { "color": "#fd9f4d" }, "title": { "color": "#cda074", "font_weight": 600 }, "type": { "color": "#cda074" }, "property": { "color": "#dcdcdc" }, "variable.special": { "color": "#cb4a05" } } } } ] } ================================================ FILE: themes/flexoki.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Flexoki", "author": "kepano", "url": "https://github.com/kepano/flexoki", "themes": [ { "name": "Flexoki Light", "mode": "light", "colors": { "accent.background": "#E8E4CE", "accent.foreground": "#100F0F", "background": "#FFFCF0", "foreground": "#100F0F", "border": "#E6E4D9", "ring": "#D0A215", "selection.background": "#D0A21577", "input.border": "#E6E4D9", "list.active.background": "#D0A21520", "list.active.border": "#D0A215", "list.background": "#FFFCF0", "list.even.background": "#F2F0E5", "muted.background": "#F2F0E5", "muted.foreground": "#6F6E69", "primary.background": "#3AA99F", "primary.foreground": "#FAFAFA", "info.background": "#4385BE", "scrollbar.background": "#FAFAFA00", "scrollbar.thumb.background": "#E6E4D9", "scrollbar.thumb.hover.background": "#DAD8CE", "secondary.active.background": "#DAD8CE", "secondary.background": "#F2F0E5", "secondary.foreground": "#100F0F", "secondary.hover.background": "#E6E4D9", "tab.active.background": "#FFFCF0", "tab.active.foreground": "#100F0F", "tab.background": "#ECECED00", "tab.foreground": "#8d8986", "tab_bar.background": "#F2F0E5", "title_bar.background": "#F2F0E5", "title_bar.border": "#DAD8CE", "base.yellow": "#D0A215", "base.red": "#D14D41", "base.blue": "#4385BE", "base.green": "#879A39", "base.magenta": "#CE5D97", "base.cyan": "#3AA99F" }, "highlight": { "editor.foreground": "#100F0F", "editor.background": "#FFFCF0", "editor.active_line.background": "#F2F0E5", "editor.line_number": "#B7B5AC", "editor.active_line_number": "#24837B", "editor.invisible": "#B7B5AC66", "conflict": "#AD8301", "conflict.background": "#FAEEC6", "conflict.border": "#AD8301", "created": "#66800B", "created.background": "#EDEECF", "created.border": "#66800B", "deleted": "#AF3029", "deleted.background": "#FFE1D5", "deleted.border": "#AF3029", "error": "#AF3029", "error.background": "#FFE1D5", "error.border": "#AF3029", "hidden": "#B7B5AC", "hidden.background": "#F2F0E5", "hidden.border": "#B7B5AC", "hint": "#B7B5AC", "hint.background": "#F2F0E5", "hint.border": "#B7B5AC", "ignored": "#B7B5AC", "ignored.background": "#F2F0E5", "ignored.border": "#B7B5AC", "info": "#24837B", "info.background": "#DDF1E4", "info.border": "#24837B", "modified": "#AD8301", "modified.background": "#FAEEC6", "modified.border": "#AD8301", "predictive": "#B7B5AC", "renamed": "#24837B", "renamed.background": "#DDF1E4", "renamed.border": "#24837B", "success": "#66800B", "success.background": "#EDEECF", "success.border": "#66800B", "unreachable": "#6F6E69", "unreachable.background": "#F2F0E5", "unreachable.border": "#6F6E69", "warning": "#AD8301", "warning.background": "#FAEEC6", "warning.border": "#AD8301", "syntax": { "attribute": { "color": "#205EA6" }, "boolean": { "color": "#BC5215" }, "comment": { "color": "#B7B5AC" }, "comment.doc": { "color": "#B7B5AC" }, "constant": { "color": "#F07171" }, "constructor": { "color": "#205EA6" }, "embedded": { "color": "#5C6773" }, "function": { "color": "#BC5215" }, "keyword": { "color": "#66800B" }, "link_text": { "color": "#24837B" }, "link_uri": { "color": "#24837B" }, "number": { "color": "#5E409D" }, "string": { "color": "#24837B" }, "string.escape": { "color": "#24837B" }, "string.regex": { "color": "#24837B" }, "string.special": { "color": "#24837B" }, "string.special.symbol": { "color": "#24837B" }, "tag": { "color": "#205EA6" }, "text.literal": { "color": "#24837B" }, "title": { "color": "#24837B", "font_weight": 600 }, "type": { "color": "#AD8301" }, "property": { "color": "#BC5215" }, "variable.special": { "color": "#205EA6" } } } }, { "name": "Flexoki Dark", "mode": "dark", "colors": { "accent.background": "#242221", "accent.foreground": "#CECDC3", "background": "#100F0F", "border": "#282726", "ring": "#AD8301", "selection.background": "#D0A21577", "foreground": "#CECDC3", "input.border": "#282726", "list.active.background": "#D0A21515", "list.active.border": "#AD8301", "list.even.background": "#1C1B1A", "muted.background": "#1C1B1A", "muted.foreground": "#878580", "panel.background": "#1C1B1A", "popover.background": "#100F0F", "popover.foreground": "#CECDC3", "primary.background": "#24837B", "primary.foreground": "#FFFCF0", "info.background": "#205EA6", "scrollbar.background": "#100F0F00", "scrollbar.thumb.background": "#282726", "scrollbar.thumb.hover.background": "#343331", "secondary.background": "#232221", "secondary.active.background": "#262423", "secondary.foreground": "#CECDC3", "secondary.hover.background": "#232221", "tab.active.background": "#100F0F", "tab.active.foreground": "#CECDC3", "tab.background": "#1C1B1A", "tab.foreground": "#878580", "tab_bar.background": "#1C1B1A99", "title_bar.background": "#191818", "base.yellow": "#AD8301", "base.yellow.light": "#D0A215", "base.red": "#AF3029", "base.red.light": "#D14D41", "base.blue": "#205EA6", "base.blue.light": "#4385BE", "base.green": "#66800B", "base.green.light": "#879A39", "base.magenta": "#A02F6F", "base.magenta.light": "#CE5D97", "base.cyan": "#24837B", "base.cyan.light": "#3AA99F" }, "highlight": { "editor.foreground": "#CECDC3", "editor.background": "#100F0F", "editor.active_line.background": "#1C1B1A", "editor.line_number": "#575653", "editor.active_line_number": "#3AA99F", "editor.invisible": "#B7B5AC66", "conflict": "#D0A215", "created": "#879A39", "hidden": "#575653", "hint": "#575653", "modified": "#D0A215", "predictive": "#878580", "syntax": { "attribute": { "color": "#4385BE" }, "boolean": { "color": "#DA702C" }, "comment": { "color": "#575653" }, "comment.doc": { "color": "#575653" }, "constant": { "color": "#CE5D97" }, "constructor": { "color": "#4385BE" }, "embedded": { "color": "#878580" }, "function": { "color": "#DA702C" }, "keyword": { "color": "#879A39" }, "link_text": { "color": "#3AA99F" }, "link_uri": { "color": "#3AA99F" }, "number": { "color": "#8B7EC8" }, "string": { "color": "#3AA99F" }, "string.escape": { "color": "#3AA99F" }, "string.regex": { "color": "#3AA99F" }, "string.special": { "color": "#3AA99F" }, "string.special.symbol": { "color": "#3AA99F" }, "tag": { "color": "#4385BE" }, "text.literal": { "color": "#3AA99F" }, "title": { "color": "#D0A215", "font_weight": 600 }, "type": { "color": "#D0A215" }, "property": { "color": "#DA702C" }, "variable.special": { "color": "#4385BE" } } } } ] } ================================================ FILE: themes/gruvbox.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Gruvbox", "author": "Pavel Pertsev", "url": "https://github.com/morhetz/gruvbox", "themes": [ { "name": "Gruvbox Light", "mode": "light", "colors": { "accent.background": "#e6d19e", "accent.foreground": "#3c3836", "background": "#fbf1c7", "border": "#d5c4a1", "danger.background": "#fb4934", "danger.active.background": "#cc241d", "danger.foreground": "#fbf1c7", "foreground": "#3c3836", "input.border": "#d5c4a1", "link.active.foreground": "#458588", "link.foreground": "#458588", "link.hover.foreground": "#458588", "list.active.background": "#d7992122", "list.hover.background": "#ebdbb2", "list.active.border": "#d79921", "list.even.background": "#f9ecba", "muted.background": "#d5c4a166", "muted.foreground": "#928374", "panel.background": "#ebdbb2", "popover.background": "#fbf1c7", "popover.foreground": "#3c3836", "primary.active.background": "#b7841f", "primary.background": "#d79921", "primary.foreground": "#fbf1c7", "primary.hover.background": "#d79921ee", "ring": "#d79921", "scrollbar.background": "#fbf1c700", "scrollbar.thumb.background": "#d5c4a1", "secondary.background": "#EBDBB2", "secondary.active.background": "#d5c4a1", "secondary.foreground": "#3c3836", "secondary.hover.background": "#e8d5a6", "tab.active.background": "#fbf1c7", "tab.active.foreground": "#3c3836", "tab.background": "#ebdbb200", "tab.foreground": "#928374", "tab_bar.background": "#ebdbb2", "title_bar.background": "#ebdbb2", "title_bar.border": "#d5c4a1", "base.magenta.light": "#D3869B66", "base.red": "#fb4934", "base.green": "#67a64f", "base.yellow": "#b57614", "base.blue": "#076678", "base.magenta": "#8f3f71", "base.cyan": "#427b58" }, "highlight": { "editor.foreground": "#3c3836", "editor.background": "#fbf1c7", "editor.active_line.background": "#ebdbb2", "editor.line_number": "#928374", "editor.active_line_number": "#3c3836", "editor.invisible": "#92837466", "conflict": "#cc241d", "created": "#79740e", "deleted": "#9d0006", "error": "#cc241d", "hidden": "#928374", "hint": "#458588", "ignored": "#928374", "info": "#458588", "modified": "#b57614", "predictive": "#928374", "renamed": "#b57614", "success": "#79740e", "unreachable": "#928374", "warning": "#d79921", "syntax": { "attribute": { "color": "#b57614" }, "boolean": { "color": "#d79921" }, "comment": { "color": "#928374", "font_style": "italic" }, "comment.doc": { "color": "#928374", "font_style": "italic" }, "constant": { "color": "#d79921" }, "constructor": { "color": "#b57614" }, "embedded": { "color": "#3c3836" }, "function": { "color": "#d79921" }, "keyword": { "color": "#cc241d" }, "link_text": { "color": "#458588", "font_style": "normal" }, "link_uri": { "color": "#076678", "font_style": "italic" }, "number": { "color": "#cc241d" }, "string": { "color": "#79740e" }, "string.escape": { "color": "#79740e" }, "string.regex": { "color": "#79740e" }, "string.special": { "color": "#d79921" }, "string.special.symbol": { "color": "#d79921" }, "tag": { "color": "#b57614" }, "text.literal": { "color": "#d79921" }, "title": { "color": "#b57614", "font_weight": 600 }, "type": { "color": "#b57614" }, "property": { "color": "#3c3836" }, "variable.special": { "color": "#cc241d" } } } }, { "name": "Gruvbox Dark", "mode": "dark", "colors": { "accent.background": "#303030", "accent.foreground": "#ebdbb2", "background": "#1d2021", "border": "#3e3936", "danger.active.background": "#fb4934", "foreground": "#ebdbb2", "input.border": "#504945", "link.active.foreground": "#83a598", "link.foreground": "#83a598", "link.hover.foreground": "#83a598", "list.active.background": "#d7992111", "list.active.border": "#b17f1b", "list.even.background": "#282828", "muted.background": "#3c383655", "muted.foreground": "#928374", "panel.background": "#282828", "popover.background": "#1d2021", "popover.foreground": "#ebdbb2", "primary.background": "#d79921", "primary.foreground": "#282828", "primary.hover.background": "#fabd2f", "ring": "#b17f1b", "scrollbar.background": "#1d202100", "scrollbar.thumb.background": "#504945", "secondary.active.background": "#333332", "secondary.background": "#282828", "secondary.foreground": "#ebdbb2", "secondary.hover.background": "#33333299", "tab.active.background": "#1d2021", "tab.active.foreground": "#ebdbb2", "tab.background": "#28282800", "tab.foreground": "#928374", "tab_bar.background": "#28282899", "title_bar.background": "#252525", "title_bar.border": "#3e3936", "base.magenta.light": "#D3869B66", "base.red": "#f06555", "base.green": "#98971a", "base.yellow": "#d79921", "base.blue": "#458588", "base.magenta": "#b16286", "base.cyan": "#689d6a" }, "highlight": { "editor.foreground": "#DDDDDD", "editor.background": "#1d2021", "editor.active_line.background": "#131313", "editor.line_number": "#8F8F8F", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#92837466", "conflict": "#f06555", "created": "#3f72e2", "deleted": "#9d0006", "error": "#cc241d", "hidden": "#9E9E9E", "hint": "#b283f8", "ignored": "#928374", "info": "#458588", "modified": "#B0A878", "predictive": "#5D5945", "renamed": "#b57614", "success": "#79740e", "unreachable": "#928374", "warning": "#d79921", "syntax": { "attribute": { "color": "#be9a52" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#E1D797" }, "keyword": { "color": "#E19773" }, "link_text": { "color": "#A86D3B", "font_style": "normal" }, "link_uri": { "color": "#6F6D66", "font_style": "italic" }, "number": { "color": "#E19773" }, "string": { "color": "#76BA53" }, "string.escape": { "color": "#76BA53" }, "string.regex": { "color": "#76BA53" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#A76D3B", "font_weight": 600 }, "type": { "color": "#A86D3B" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } } ] } ================================================ FILE: themes/harper.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Harper", "author": "iTerm2-Color-Schemes", "url": "https://github.com/mbadolato/iTerm2-Color-Schemes", "themes": [ { "name": "Harper", "mode": "dark", "colors": { "accent.background": "#27222c", "accent.foreground": "#a8a49d", "background": "#010101", "border": "#333333", "foreground": "#a8a49d", "input.border": "#333333", "list.active.background": "#B196C622", "list.active.border": "#B196C699", "list.even.background": "#18151B77", "muted.background": "#171717", "muted.foreground": "#726E69", "panel.background": "#003a5b", "popover.background": "#010101", "popover.foreground": "#a8a49d", "primary.background": "#B196C6", "primary.foreground": "#000000", "scrollbar.background": "#003a5b00", "scrollbar.thumb.background": "#726E69", "secondary.background": "#1b1b1b", "secondary.active.background": "#2c2632", "secondary.foreground": "#A8A49D", "secondary.hover.background": "#23232399", "tab.active.background": "#010101", "tab.active.foreground": "#a8a49d", "tab.foreground": "#787878", "title_bar.background": "#101010", "base.red": "#ff5874", "base.red.light": "#ff587499", "base.green": "#489e48", "base.green.light": "#489e4899", "base.blue": "#7fb5e1", "base.blue.light": "#7fb5e199", "base.cyan": "#bff5e5", "base.cyan.light": "#bff5e599", "base.magenta": "#b296c6", "base.magenta.light": "#b296c699", "base.yellow": "#f8b63f", "base.yellow.light": "#f8b63f99" }, "highlight": { "editor.foreground": "#a8a49d", "editor.background": "#010101", "editor.active_line.background": "#18151B", "editor.line_number": "#726E69", "editor.active_line_number": "#a8a49d", "editor.invisible": "#726E6966", "conflict": "#ff5874", "created": "#489e48", "deleted": "#ff5874", "error": "#ff5874", "hidden": "#726E69", "hint": "#7fb5e1", "ignored": "#726E69", "info": "#7fb5e1", "modified": "#b296c6", "predictive": "#726E69", "renamed": "#b296c6", "success": "#489e48", "unreachable": "#726E69", "warning": "#f8b63f", "syntax": { "attribute": { "color": "#b296c6" }, "boolean": { "color": "#f8b63f" }, "comment": { "color": "#726E69", "font_style": "italic" }, "comment.doc": { "color": "#726E69", "font_style": "italic" }, "constant": { "color": "#f8b63f" }, "constructor": { "color": "#b296c6" }, "embedded": { "color": "#a8a49d" }, "function": { "color": "#f8b63f" }, "keyword": { "color": "#ff5874" }, "link_text": { "color": "#7fb5e1", "font_style": "normal" }, "link_uri": { "color": "#bff5e5", "font_style": "italic" }, "number": { "color": "#ff5874" }, "string": { "color": "#489e48" }, "string.escape": { "color": "#489e48" }, "string.regex": { "color": "#489e48" }, "string.special": { "color": "#f8b63f" }, "string.special.symbol": { "color": "#f8b63f" }, "tag": { "color": "#b296c6" }, "text.literal": { "color": "#f8b63f" }, "title": { "color": "#b296c6", "font_weight": 600 }, "type": { "color": "#b296c6" }, "property": { "color": "#a8a49d" }, "variable.special": { "color": "#ff5874" } } } } ] } ================================================ FILE: themes/hybrid.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Hybrid", "author": "Andrew Wong", "url": "https://github.com/w0ng/vim-hybrid", "themes": [ { "name": "Hybrid Light", "mode": "light", "shadow": false, "colors": { "accent.background": "#c8ced1", "accent.foreground": "#1c1c1c", "background": "#E4E4E4", "border": "#CACACA", "danger.background": "#ff5f5f", "danger.foreground": "#ffffff", "foreground": "#1c1c1c", "input.border": "#CACACA", "link.active.foreground": "#005f87", "link.foreground": "#005f87", "link.hover.foreground": "#005f87", "list.active.background": "#79792C20", "list.active.border": "#79792C", "list.hover.background": "#D5D5D5", "list.even.background": "#DFDFDF", "muted.background": "#d7d7d7", "muted.foreground": "#5f5f5f", "primary.background": "#005f87", "primary.foreground": "#ffffff", "scrollbar.background": "#E0E0E000", "scrollbar.thumb.background": "#8f8f8f", "secondary.background": "#D0D0D0", "secondary.active.background": "#c4c4c4", "secondary.foreground": "#1c1c1c", "secondary.hover.background": "#c4c4c499", "tab.active.background": "#E4E4E4", "tab.active.foreground": "#1c1c1c", "tab.background": "#e8e8e800", "tab.foreground": "#5f5f5f", "tab_bar.background": "#e8e8e8", "title_bar.background": "#D0D0D0", "title_bar.border": "#B3B3B3", "base.red": "#5F0000", "base.green": "#005F00", "base.yellow": "#948000", "base.blue": "#00195f", "base.magenta": "#5f1c51", "base.magenta.light": "#5f1c5111", "base.cyan": "#005a5f" }, "highlight": { "editor.foreground": "#1c1c1c", "editor.background": "#E4E4E4", "editor.active_line.background": "#D3D3D3", "editor.line_number": "#5F5F5F", "editor.active_line_number": "#1C1C1C", "editor.invisible": "#5F5F5F66", "conflict": "#D2602D", "created": "#3F72E2", "deleted": "#FF5F5F", "error": "#FF5F5F", "hidden": "#9E9E9E", "hint": "#5F87AF", "ignored": "#B3B3B3", "info": "#005F87", "modified": "#B0A878", "predictive": "#5D5945", "renamed": "#5F87AF", "success": "#005F00", "unreachable": "#9E9E9E", "warning": "#948000", "syntax": { "attribute": { "color": "#948000" }, "boolean": { "color": "#005F00" }, "comment": { "color": "#5F5F5F", "font_style": "italic" }, "comment.doc": { "color": "#5F5F5F", "font_style": "italic" }, "constant": { "color": "#005F87" }, "constructor": { "color": "#79792C" }, "embedded": { "color": "#1C1C1C" }, "function": { "color": "#005F87" }, "keyword": { "color": "#5F1C51" }, "link_text": { "color": "#005F87", "font_style": "underline" }, "link_uri": { "color": "#005F87", "font_style": "italic" }, "number": { "color": "#005F00" }, "string": { "color": "#005F00" }, "string.escape": { "color": "#005F00" }, "string.regex": { "color": "#005F00" }, "string.special": { "color": "#005F87" }, "string.special.symbol": { "color": "#005F87" }, "tag": { "color": "#79792C" }, "text.literal": { "color": "#005F87" }, "title": { "color": "#005F87", "font_weight": 600 }, "type": { "color": "#5F1C51" }, "property": { "color": "#1C1C1C" }, "variable.special": { "color": "#5F1C51" } } } }, { "name": "Hybrid Dark", "mode": "dark", "colors": { "accent.background": "#31393a", "accent.foreground": "#e8e8e8", "background": "#1D1F21", "border": "#34373c", "foreground": "#e8e8e8", "input.border": "#34373c", "link.active.foreground": "#87d7ff", "link.foreground": "#81A2BE", "link.hover.foreground": "#006f9f", "list.active.background": "#15678a10", "list.active.border": "#15678a", "list.even.background": "#282A2E", "muted.background": "#282A2E99", "muted.foreground": "#878787", "panel.background": "#1D1F21", "popover.background": "#1D1F21", "popover.foreground": "#e8e8e8", "primary.background": "#15678a", "primary.foreground": "#e8e8e8", "scrollbar.background": "#1D1F2100", "scrollbar.thumb.background": "#5f5f5f", "secondary.background": "#303336", "secondary.active.background": "#33363b", "secondary.foreground": "#e8e8e8", "secondary.hover.background": "#373b3e", "tab.active.background": "#1D1F21", "tab.active.foreground": "#d4d4d4", "tab.background": "#24262700", "tab.foreground": "#999999", "tab_bar.background": "#242627", "title_bar.background": "#242629", "title_bar.border": "#34373c", "base.red": "#8a1515", "base.red.light": "#ff5f5f", "base.green": "#7c8a15", "base.green.light": "#b3bf5a", "base.yellow": "#8a7c15", "base.yellow.light": "#e4b55e", "base.blue": "#15678a", "base.blue.light": "#6e90b0", "base.magenta": "#8a1567", "base.magenta.light": "#B294BB", "base.cyan": "#15678a", "base.cyan.light": "#7fbfb4" }, "highlight": { "editor.foreground": "#DDDDDD", "editor.background": "#000000", "editor.active_line.background": "#131313", "editor.line_number": "#8F8F8F", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#5F5F5F66", "conflict": "#D2602D", "created": "#3f72e2", "created.background": "#0C4619", "deleted.background": "#46190C", "error.background": "#46190C", "error.border": "#802207", "hidden": "#9E9E9E", "hint": "#b283f8", "hint.background": "#250c4b", "hint.border": "#3f0891", "info.background": "#0C194D", "info.border": "#082190", "modified": "#B0A878", "modified.background": "#3A310E", "predictive": "#5D5945", "success.background": "#0C4619", "warning.background": "#3A310E", "warning.border": "#7B6508", "syntax": { "attribute": { "color": "#be9a52" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#E1D797" }, "keyword": { "color": "#E19773" }, "link_text": { "color": "#A86D3B", "font_style": "normal" }, "link_uri": { "color": "#6F6D66", "font_style": "italic" }, "number": { "color": "#E19773" }, "string": { "color": "#76BA53" }, "string.escape": { "color": "#76BA53" }, "string.regex": { "color": "#76BA53" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#A76D3B", "font_weight": 600 }, "type": { "color": "#A86D3B" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } } ] } ================================================ FILE: themes/jellybeans.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Jellybeans", "author": "nanotech", "url": "https://github.com/nanotech/jellybeans.vim", "themes": [ { "name": "Jellybeans", "mode": "dark", "colors": { "accent.background": "#292929", "accent.foreground": "#e8e8e8", "background": "#151515", "border": "#2e2e2e", "danger.foreground": "#151515", "foreground": "#E8E8D3", "input.border": "#4E4847", "link.active.foreground": "#97bedc", "link.foreground": "#97bedc", "link.hover.foreground": "#97bedc", "list.active.background": "#97bedc15", "list.active.border": "#97bedc", "list.even.background": "#1C1C1C", "muted.background": "#242424", "muted.foreground": "#767676", "panel.background": "#151515", "popover.background": "#151515", "popover.foreground": "#e8e8e8", "primary.background": "#97bedc", "primary.foreground": "#151515", "scrollbar.background": "#26262600", "scrollbar.thumb.background": "#6e6e6e", "secondary.background": "#2F2F2F", "secondary.active.background": "#2f2f2f", "secondary.foreground": "#e8e8e8", "secondary.hover.background": "#252525", "tab.active.background": "#151515", "tab.active.foreground": "#e8e8e8", "tab.background": "#10101000", "tab.foreground": "#969696", "tab_bar.background": "#101010", "title_bar.background": "#101010", "title_bar.border": "#2e2e2e", "base.red": "#e27373", "base.red.light": "#ffa1a1", "base.green": "#94b979", "base.green.light": "#bddeab", "base.yellow": "#ffba7b", "base.yellow.light": "#ffdca0", "base.blue": "#97bedc", "base.blue.light": "#b1d8f6", "base.magenta": "#B294BB", "base.magenta.light": "#fbdaff", "base.cyan": "#00988e", "base.cyan.light": "#1ab2a8" }, "highlight": { "editor.foreground": "#DDDDDD", "editor.background": "#000000", "editor.active_line.background": "#131313", "editor.line_number": "#8F8F8F", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#9E9E9E66", "conflict": "#D2602D", "created": "#3f72e2", "hidden": "#9E9E9E", "hint": "#b283f8", "modified": "#B0A878", "predictive": "#5D5945", "syntax": { "attribute": { "color": "#be9a52" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#E1D797" }, "keyword": { "color": "#E19773" }, "link_text": { "color": "#A86D3B", "font_style": "normal" }, "link_uri": { "color": "#6F6D66", "font_style": "italic" }, "number": { "color": "#E19773" }, "string": { "color": "#76BA53" }, "string.escape": { "color": "#76BA53" }, "string.regex": { "color": "#76BA53" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#A76D3B", "font_weight": 600 }, "type": { "color": "#A86D3B" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } } ] } ================================================ FILE: themes/kibble.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Kibble", "author": "iTerm2-Color-Schemes", "url": "https://github.com/mbadolato/iTerm2-Color-Schemes", "themes": [ { "name": "Kibble", "mode": "dark", "colors": { "accent.background": "#252525", "accent.foreground": "#f7f7f7", "background": "#0e100a", "border": "#292a24", "danger.background": "#e07b5c", "foreground": "#f7f7f7", "input.border": "#333333", "list.active.background": "#2BCF1315", "list.active.border": "#2BCF13", "list.hover.background": "#2c292b54", "list.even.background": "#24222355", "muted.background": "#292a2455", "muted.foreground": "#777777", "panel.background": "#003a5b", "popover.background": "#0e100a", "popover.foreground": "#f7f7f7", "primary.active.background": "#6ce05c99", "primary.background": "#6ce05c", "primary.foreground": "#101010", "scrollbar.background": "#003a5b00", "scrollbar.thumb.background": "#726E69", "secondary.background": "#22231f", "secondary.active.background": "#292825", "secondary.foreground": "#f7f7f7", "secondary.hover.background": "#22231f", "success.background": "#5c6ee0", "warning.background": "#e0c85c", "info.background": "#5cd1e0", "selection.background": "#9ba787", "tab.active.background": "#0e100a", "tab.active.foreground": "#f7f7f7", "tab.foreground": "#969696", "title_bar.background": "#161712", "base.red": "#C70231", "base.red.light": "#e07b5c", "base.green": "#2BCF13", "base.green.light": "#6ce05c", "base.blue": "#3449d1", "base.blue.light": "#5c6ee0", "base.cyan": "#0798ab", "base.cyan.light": "#5cd1e0", "base.magenta": "#8400ff", "base.magenta.light": "#C495F0", "base.yellow": "#c7a302", "base.yellow.light": "#e0c85c" }, "highlight": { "editor.foreground": "#f7f7f7", "editor.background": "#0e100a", "editor.active_line.background": "#242223", "editor.line_number": "#726E69", "editor.active_line_number": "#f7f7f7", "editor.invisible": "#726E6966", "conflict": "#c70031", "created": "#2BCF13", "deleted": "#c70031", "error": "#c70031", "hidden": "#726E69", "hint": "#0798ab", "ignored": "#726E69", "info": "#0798ab", "modified": "#3449d1", "predictive": "#726E69", "renamed": "#3449d1", "success": "#2BCF13", "unreachable": "#726E69", "warning": "#c7a302", "syntax": { "attribute": { "color": "#3449d1" }, "boolean": { "color": "#c7a302" }, "comment": { "color": "#726E69", "font_style": "italic" }, "comment.doc": { "color": "#726E69", "font_style": "italic" }, "constant": { "color": "#c7a302" }, "constructor": { "color": "#3449d1" }, "embedded": { "color": "#f7f7f7" }, "function": { "color": "#c7a302" }, "keyword": { "color": "#c70031" }, "link_text": { "color": "#0798ab", "font_style": "normal" }, "link_uri": { "color": "#68f2e0", "font_style": "italic" }, "number": { "color": "#c70031" }, "string": { "color": "#2BCF13" }, "string.escape": { "color": "#2BCF13" }, "string.regex": { "color": "#2BCF13" }, "string.special": { "color": "#c7a302" }, "string.special.symbol": { "color": "#c7a302" }, "tag": { "color": "#3449d1" }, "text.literal": { "color": "#c7a302" }, "title": { "color": "#3449d1", "font_weight": 600 }, "type": { "color": "#3449d1" }, "property": { "color": "#f7f7f7" }, "variable.special": { "color": "#c70031" } } } } ] } ================================================ FILE: themes/macos-classic.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "macOS Classic", "author": "huacnlee", "url": "https://github.com/huacnlee/zed-theme-macos-classic", "themes": [ { "name": "macOS Classic Light", "mode": "light", "shadow": false, "colors": { "accent.background": "#E0E0E0", "accent.foreground": "#000000", "background": "#F9F9F9", "foreground": "#000000", "border": "#D2D2D2", "ring": "#0060de", "danger.foreground": "#FFFFFF", "list.active.background": "#0060de15", "list.active.border": "#0060de", "list.even.background": "#EFEFEF", "list.hover.background": "#E7E7E7", "muted.background": "#EAEAEA", "muted.foreground": "#707070", "popover.background": "#F5F5F5", "popover.foreground": "#000000", "primary.background": "#0060de", "primary.foreground": "#FFFFFF", "scrollbar.background": "#FFFFFF00", "scrollbar.thumb.background": "#C8C8C8", "secondary.active.background": "#D9D9D9", "secondary.background": "#E0E0E0", "secondary.foreground": "#000000", "secondary.hover.background": "#E5E5E5", "tab.active.foreground": "#000000", "tab.background": "#E9E9E9", "tab.foreground": "#606060", "tab_bar.background": "#E9E9E9", "title_bar.background": "#FEFEFE", "title_bar.border": "#DADADA", "base.yellow": "#B59A00", "base.red": "#d21f07", "base.blue": "#0060de", "base.green": "#319a00", "base.magenta": "#9A0068", "base.cyan": "#007E8A" }, "highlight": { "editor.foreground": "#000000", "editor.background": "#ffffff", "editor.active_line.background": "#F5F5F5", "editor.line_number": "#929292", "editor.active_line_number": "#000000", "editor.invisible": "#007fff66", "conflict": "#d21f07", "created": "#0060de", "hidden": "#6D6D6D", "hint": "#9A0068", "modified": "#319a00", "predictive": "#A4ABB6", "warning": "#B59A00", "syntax": { "attribute": { "color": "#957931" }, "boolean": { "color": "#C5060B" }, "comment": { "color": "#007fff" }, "comment.doc": { "color": "#007fff" }, "constant": { "color": "#C5060B" }, "constructor": { "color": "#0433ff" }, "embedded": { "color": "#333333" }, "function": { "color": "#0000A2" }, "keyword": { "color": "#0433ff" }, "link_text": { "color": "#0000A2", "font_style": "normal" }, "link_uri": { "color": "#6A7293", "font_style": "italic" }, "number": { "color": "#0433ff" }, "string": { "color": "#036A07" }, "string.escape": { "color": "#036A07" }, "string.regex": { "color": "#036A07" }, "string.special": { "color": "#d21f07" }, "string.special.symbol": { "color": "#d21f07" }, "tag": { "color": "#0433ff" }, "text.literal": { "color": "#6F42C1" }, "title": { "color": "#0433FF" }, "type": { "color": "#6f42c1" }, "property": { "color": "#333333" }, "variable": { "color": "#333333" }, "variable.special": { "color": "#C5060B" } } } }, { "name": "macOS Classic Dark", "mode": "dark", "shadow": false, "colors": { "accent.background": "#282629", "accent.foreground": "#CACCCA", "background": "#131313", "border": "#303030", "ring": "#077CFD", "foreground": "#DEDEDE", "list.even.background": "#232323", "list.active.background": "#0059D115", "list.active.border": "#077CFD", "muted.background": "#202020", "muted.foreground": "#9D9D9D", "popover.background": "#101010", "popover.foreground": "#CACCCA", "primary.background": "#077CFD", "primary.foreground": "#F2F9FF", "switch.background": "#393939", "switch.thumb.background": "#DAECFF", "scrollbar.background": "#13131300", "scrollbar.thumb.background": "#9F9F9F", "secondary.active.background": "#353535", "secondary.background": "#353535", "secondary.foreground": "#DEDEDE", "secondary.hover.background": "#35353599", "link": "#419CFF", "tab_bar.background": "#1C1C1E", "tab.active.background": "#131313", "tab.foreground": "#8F8F8F", "title_bar.background": "#1C1C1E", "selection.background": "#3F638B", "base.blue": "#419CFF", "base.cyan": "#07FDD3", "base.green": "#30D158", "base.magenta": "#A550A7", "base.red": "#FF5257", "base.yellow": "#FFC600" }, "highlight": { "editor.foreground": "#CACCCA", "editor.background": "#131313", "editor.active_line.background": "#35343666", "editor.line_number": "#8F8F8F", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#007fff66", "conflict": "#D2602D", "created": "#3f72e2", "created.background": "#0C4619", "deleted.background": "#46190C", "error.background": "#46190C", "error.border": "#802207", "hidden": "#9E9E9E", "hint": "#b283f8", "hint.background": "#250c4b", "hint.border": "#3f0891", "info.background": "#0059D1", "info.border": "#0059D1", "modified": "#B0A878", "modified.background": "#3A310E", "predictive": "#5D5945", "success.background": "#0C4619", "warning.background": "#3A310E", "warning.border": "#7B6508", "syntax": { "attribute": { "color": "#e7cb8f" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#fdd888" }, "keyword": { "color": "#c28b12" }, "link_text": { "color": "#307BF6", "font_style": "normal" }, "link_uri": { "color": "#7faef9", "font_style": "italic" }, "number": { "color": "#E1D797" }, "string": { "color": "#62BA46" }, "string.escape": { "color": "#62BA46" }, "string.regex": { "color": "#62BA46" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#fdd888", "font_weight": 600 }, "type": { "color": "#c75828" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } } ] } ================================================ FILE: themes/matrix.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Matrix", "author": "iruzo", "url": "https://github.com/iruzo/matrix-nvim", "themes": [ { "name": "Matrix", "mode": "dark", "radius": 0, "radius.lg": 0, "colors": { "accent.background": "#002d00", "accent.foreground": "#00FF00", "background": "#020D02", "border": "#12410E", "window_border": "#156B12", "ring": "#00FF00", "danger.background": "#FF0000", "danger.active.background": "#FF0000", "danger.foreground": "#0D0208", "danger.hover.background": "#FF0000", "foreground": "#88FF88", "input.border": "#003200", "link.active.foreground": "#00FF00", "link.foreground": "#00FF00", "link.hover.foreground": "#00FF00", "list.active.background": "#00FF0011", "list.active.border": "#00FF00", "list.even.background": "#00190066", "muted.background": "#001900", "muted.foreground": "#007700", "panel.background": "#001900", "popover.background": "#020D02", "popover.foreground": "#00FF00", "primary.active.background": "#00FF0099", "primary.background": "#00FF00", "primary.foreground": "#0D0208", "primary.hover.background": "#00FF00AA", "scrollbar.background": "#0D020800", "scrollbar.thumb.background": "#003d00", "secondary.active.background": "#001B00", "secondary.background": "#002900", "secondary.foreground": "#3fd43a", "secondary.hover.background": "#003300", "tab.active.foreground": "#00FF00", "tab.background": "#0D020800", "tab.foreground": "#88FF88", "title_bar.background": "#020d02", "title_bar.border": "#12410E", "base.red": "#FF0000", "base.green": "#00FF00", "base.yellow": "#ffea00", "base.blue": "#0000ff", "base.magenta": "#FF00FF", "base.cyan": "#00ffd5" }, "highlight": { "editor.foreground": "#88FF88", "editor.background": "#020d02", "editor.active_line.background": "#001900", "editor.line_number": "#007700", "editor.active_line_number": "#00FF00", "editor.invisible": "#00770066", "conflict": "#FF0000", "created": "#82d967", "deleted": "#FF0000", "error": "#FF0000", "hidden": "#007700", "hint": "#3fd43a", "ignored": "#007700", "info": "#3fd43a", "modified": "#82d967", "predictive": "#007700", "renamed": "#82d967", "success": "#82d967", "unreachable": "#007700", "warning": "#ffd700", "syntax": { "attribute": { "color": "#82d967" }, "boolean": { "color": "#ffd700" }, "comment": { "color": "#007700", "font_style": "italic" }, "comment.doc": { "color": "#007700", "font_style": "italic" }, "constant": { "color": "#ffd700" }, "constructor": { "color": "#82d967" }, "embedded": { "color": "#88FF88" }, "function": { "color": "#ffd700" }, "keyword": { "color": "#FF0000" }, "link_text": { "color": "#3fd43a", "font_style": "normal" }, "link_uri": { "color": "#00FF00", "font_style": "italic" }, "number": { "color": "#FF0000" }, "string": { "color": "#82d967" }, "string.escape": { "color": "#82d967" }, "string.regex": { "color": "#82d967" }, "string.special": { "color": "#ffd700" }, "string.special.symbol": { "color": "#ffd700" }, "tag": { "color": "#82d967" }, "text.literal": { "color": "#ffd700" }, "title": { "color": "#82d967", "font_weight": 600 }, "type": { "color": "#82d967" }, "property": { "color": "#88FF88" }, "variable.special": { "color": "#FF0000" } } } } ] } ================================================ FILE: themes/mellifluous.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Mellifluous", "author": "Ramojus Lapinskas", "url": "https://github.com/ramojus/mellifluous.nvim", "themes": [ { "name": "Mellifluous Light", "mode": "light", "shadow": false, "colors": { "accent.background": "#d4d4d4", "accent.foreground": "#383a42", "background": "#E7E7E7", "border": "#CACACA", "danger.active.background": "#e06c75", "danger.hover.background": "#be5046", "foreground": "#383a42", "link.active.foreground": "#5A6599", "link.foreground": "#5A6599", "link.hover.foreground": "#5A6599", "list.hover.background": "#d5d5d5", "list.active.background": "#5A659912", "list.active.border": "#5A6599", "list.even.background": "#F0F0F0", "muted.background": "#E0E0E0", "muted.foreground": "#828997", "panel.background": "#fafafa", "popover.background": "#E4E4E4", "popover.foreground": "#383a42", "primary.active.background": "#5A6599AA", "primary.background": "#5A6599", "primary.foreground": "#F0F0F0", "primary.hover.background": "#626ca2", "scrollbar.background": "#fafafa00", "scrollbar.thumb.background": "#c0c0c0", "secondary.background": "#d0d0d0", "secondary.active.background": "#cccccc", "secondary.foreground": "#383a42", "secondary.hover.background": "#d7d7d7", "tab.active.background": "#E7E7E7", "tab.active.foreground": "#383a42", "tab.background": "#F0F0F000", "tab.foreground": "#727272", "tab_bar.background": "#E7E7E7", "title_bar.background": "#dfdfdf", "title_bar.border": "#CACACA", "base.red": "#C95954", "base.green": "#828040", "base.yellow": "#c98f54", "base.blue": "#a8a1be", "base.magenta": "#b39fb0", "base.magenta.light": "#9C699588", "base.cyan": "#54c981" }, "highlight": { "editor.foreground": "#383a42", "editor.background": "#E7E7E7", "editor.active_line.background": "#d5d5d5", "editor.line_number": "#727272", "editor.active_line_number": "#383a42", "editor.invisible": "#A0A0A066", "conflict": "#e06c75", "created": "#828040", "deleted": "#be5046", "error": "#e06c75", "hidden": "#727272", "hint": "#5A6599", "ignored": "#727272", "info": "#5A6599", "modified": "#727272", "predictive": "#727272", "renamed": "#727272", "success": "#828040", "unreachable": "#727272", "warning": "#cbaa89", "syntax": { "attribute": { "color": "#727272" }, "boolean": { "color": "#cbaa89" }, "comment": { "color": "#A0A0A0", "font_style": "italic" }, "comment.doc": { "color": "#A0A0A0", "font_style": "italic" }, "constant": { "color": "#cbaa89" }, "constructor": { "color": "#727272" }, "embedded": { "color": "#383a42" }, "function": { "color": "#cbaa89" }, "keyword": { "color": "#e06c75" }, "link_text": { "color": "#5A6599", "font_style": "normal" }, "link_uri": { "color": "#5A6599", "font_style": "italic" }, "number": { "color": "#e06c75" }, "string": { "color": "#828040" }, "string.escape": { "color": "#828040" }, "string.regex": { "color": "#828040" }, "string.special": { "color": "#cbaa89" }, "string.special.symbol": { "color": "#cbaa89" }, "tag": { "color": "#727272" }, "text.literal": { "color": "#cbaa89" }, "title": { "color": "#727272", "font_weight": 600 }, "type": { "color": "#727272" }, "property": { "color": "#383a42" }, "variable.special": { "color": "#e06c75" } } } }, { "name": "Mellifluous Dark", "mode": "dark", "shadow": false, "colors": { "accent.background": "#2f2f2f", "accent.foreground": "#abb2bf", "background": "#1A1A1A", "border": "#444444", "foreground": "#abb2bf", "input.border": "#444444", "link.active.foreground": "#5A6599", "link.foreground": "#5A6599", "link.hover.foreground": "#5A6599", "list.active.background": "#5A659922", "list.active.border": "#5A6599", "list.even.background": "#29292988", "muted.background": "#44444455", "muted.foreground": "#828997", "panel.background": "#282c34", "popover.background": "#1A1A1A", "popover.foreground": "#abb2bf", "primary.active.background": "#5A6599AA", "primary.background": "#5A6599", "primary.foreground": "#ecedf4", "primary.hover.background": "#626ca2", "scrollbar.background": "#282c3400", "scrollbar.thumb.background": "#444444", "secondary.background": "#292929", "secondary.active.background": "#323232", "secondary.foreground": "#abb2bf", "secondary.hover.background": "#292929", "tab.active.background": "#1A1A1A", "tab.active.foreground": "#abb2bf", "tab.background": "#1A1A1A00", "tab.foreground": "#abb2bf", "title_bar.background": "#171717", "title_bar.border": "#444444", "base.red": "#C95954", "base.green": "#828040", "base.yellow": "#c98d54", "base.blue": "#5481c9", "base.magenta": "#9C6995", "base.magenta.light": "#9C699577", "base.cyan": "#54c9bd" }, "highlight": { "editor.foreground": "#abb2bf", "editor.background": "#1A1A1A", "editor.active_line.background": "#29292977", "editor.line_number": "#828997", "editor.active_line_number": "#abb2bf", "editor.invisible": "#A0A0A066", "conflict": "#e06c75", "conflict.background": "#1A1A1A", "conflict.border": "#be5046", "created": "#828040", "created.background": "#292929", "created.border": "#929292", "deleted": "#be5046", "deleted.background": "#1A1A1A", "deleted.border": "#e06c75", "error": "#e06c75", "error.background": "#1A1A1A", "error.border": "#be5046", "hidden": "#828997", "hidden.background": "#292929", "hidden.border": "#444444", "hint": "#5A6599", "hint.background": "#292929", "hint.border": "#626ca2", "ignored": "#828997", "ignored.background": "#292929", "ignored.border": "#444444", "info": "#5A6599", "info.background": "#292929", "info.border": "#626ca2", "modified": "#929292", "modified.background": "#292929", "modified.border": "#444444", "predictive": "#828997", "predictive.background": "#292929", "predictive.border": "#444444", "renamed": "#929292", "renamed.background": "#292929", "renamed.border": "#444444", "success": "#828040", "success.background": "#292929", "success.border": "#929292", "unreachable": "#828997", "unreachable.background": "#292929", "unreachable.border": "#444444", "warning": "#d19a66", "warning.background": "#292929", "warning.border": "#929292", "syntax": { "attribute": { "color": "#929292" }, "boolean": { "color": "#d19a66" }, "comment": { "color": "#828997", "font_style": "italic" }, "comment.doc": { "color": "#828997", "font_style": "italic" }, "constant": { "color": "#d19a66" }, "constructor": { "color": "#929292" }, "embedded": { "color": "#abb2bf" }, "function": { "color": "#d19a66" }, "keyword": { "color": "#e06c75" }, "link_text": { "color": "#5A6599", "font_style": "normal" }, "link_uri": { "color": "#5A6599", "font_style": "italic" }, "number": { "color": "#e06c75" }, "string": { "color": "#828040" }, "string.escape": { "color": "#828040" }, "string.regex": { "color": "#828040" }, "string.special": { "color": "#d19a66" }, "string.special.symbol": { "color": "#d19a66" }, "tag": { "color": "#929292" }, "text.literal": { "color": "#d19a66" }, "title": { "color": "#929292", "font_weight": 600 }, "type": { "color": "#929292" }, "property": { "color": "#abb2bf" }, "variable.special": { "color": "#e06c75" } } } } ] } ================================================ FILE: themes/molokai.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Molokai", "author": "Tomas Restrepo", "url": "https://github.com/tomasr/molokai", "themes": [ { "name": "Molokai Light", "mode": "light", "shadow": false, "colors": { "accent.background": "#EBE4E1", "accent.foreground": "#060606", "background": "#FEFAF9", "border": "#E4DEDA", "window_border": "#d6d6d6", "ring": "#a0a0ab", "foreground": "#0a0a0a", "input.border": "#E4DEDA", "list.active.background": "#E1477411", "list.active.border": "#E14774AA", "list.even.background": "#E4DEDA33", "muted.background": "#f1eded", "muted.foreground": "#767676", "panel.background": "#FEFAF9", "popover.background": "#FEFAF9", "popover.foreground": "#0a0a0a", "primary.background": "#E14774", "primary.foreground": "#fafafa", "scrollbar.background": "#FEFAF900", "scrollbar.thumb.background": "#ADADB0", "secondary.background": "#E4DEDA", "secondary.foreground": "#060606", "secondary.hover.background": "#E4DEDA99", "title_bar.background": "#e8e3e0", "title_bar.border": "#d5ccc6", "base.red": "#e17047", "base.green": "#82cc0a", "base.yellow": "#ccac0a", "base.blue": "#e16032", "base.blue.light": "#e16032", "base.magenta": "#7058be", "base.cyan": "#0acc78" }, "highlight": { "editor.foreground": "#0a0a0a", "editor.background": "#FEFAF9", "editor.active_line.background": "#E4DEDA", "editor.line_number": "#767676", "editor.active_line_number": "#0a0a0a", "editor.invisible": "#76767666", "conflict": "#e14775", "created": "#269D69", "deleted": "#e14775", "error": "#e14775", "hidden": "#767676", "hint": "#7058be", "ignored": "#767676", "info": "#1c8ca8", "modified": "#cc7a0a", "predictive": "#ADADB0", "renamed": "#E14774", "success": "#269D69", "unreachable": "#767676", "warning": "#cc7a0a", "syntax": { "attribute": { "color": "#e14775" }, "boolean": { "color": "#cc7a0a" }, "comment": { "color": "#767676", "font_style": "italic" }, "comment.doc": { "color": "#767676", "font_style": "italic" }, "constant": { "color": "#7058be" }, "constructor": { "color": "#269D69" }, "embedded": { "color": "#1c8ca8" }, "function": { "color": "#e14775" }, "keyword": { "color": "#e14775", "font_style": "normal" }, "link_text": { "color": "#cc7a0a", "font_style": "underline" }, "link_uri": { "color": "#7058be", "font_style": "italic" }, "number": { "color": "#cc7a0a" }, "string": { "color": "#269D69" }, "string.escape": { "color": "#7058be" }, "string.regex": { "color": "#269D69" }, "string.special": { "color": "#e14775" }, "string.special.symbol": { "color": "#e14775" }, "tag": { "color": "#e14775" }, "text.literal": { "color": "#7058be" }, "title": { "color": "#cc7a0a", "font_weight": 600 }, "type": { "color": "#1c8ca8" }, "property": { "color": "#7058be" }, "variable.special": { "color": "#e14775" } } } }, { "name": "Molokai Dark", "mode": "dark", "colors": { "accent.background": "#2b2f30", "accent.foreground": "#f8f8f2", "background": "#1b1d1e", "border": "#393939", "foreground": "#f8f8f2", "input.border": "#3b3b3b", "list.active.background": "#f9267211", "list.active.border": "#f9267288", "list.even.background": "#292c2e", "muted.background": "#232526", "muted.foreground": "#5b5a54", "panel.background": "#1b1d1e", "popover.background": "#1b1d1e", "popover.foreground": "#f8f8f2", "primary.background": "#dc1860", "primary.foreground": "#f8f8f2", "scrollbar.background": "#1b1d1e00", "scrollbar.thumb.background": "#4b4b4b", "secondary.background": "#292c2e", "secondary.active.background": "#303436", "secondary.foreground": "#f5f5f4", "secondary.hover.background": "#30343699", "table.row.border": "#3b3b3b70", "title_bar.background": "#202223", "title_bar.border": "#3b3b3b", "base.red": "#dc1860", "base.green": "#82cc0a", "base.yellow": "#807607", "base.blue": "#075880", "base.magenta": "#800780", "base.cyan": "#07807e" }, "highlight": { "editor.foreground": "#DDDDDD", "editor.background": "#000000", "editor.active_line.background": "#131313", "editor.line_number": "#8F8F8F", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#76767666", "conflict": "#D2602D", "created": "#3f72e2", "created.background": "#0C4619", "deleted.background": "#46190C", "error.background": "#46190C", "error.border": "#802207", "hidden": "#9E9E9E", "hint": "#b283f8", "hint.background": "#250c4b", "hint.border": "#3f0891", "info.background": "#0C194D", "info.border": "#082190", "modified": "#B0A878", "modified.background": "#3A310E", "predictive": "#5D5945", "success.background": "#0C4619", "warning.background": "#3A310E", "warning.border": "#7B6508", "syntax": { "attribute": { "color": "#be9a52" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#E1D797" }, "keyword": { "color": "#E19773" }, "link_text": { "color": "#A86D3B", "font_style": "normal" }, "link_uri": { "color": "#6F6D66", "font_style": "italic" }, "number": { "color": "#E19773" }, "string": { "color": "#76BA53" }, "string.escape": { "color": "#76BA53" }, "string.regex": { "color": "#76BA53" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#A76D3B", "font_weight": 600 }, "type": { "color": "#A86D3B" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } } ] } ================================================ FILE: themes/solarized.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Solarized", "author": "Ethan Schoonover", "url": "https://ethanschoonover.com/solarized", "themes": [ { "name": "Solarized Light", "mode": "light", "colors": { "accent.background": "#ebe4ce", "accent.foreground": "#073642", "background": "#FDF6E3", "border": "#DCD4BC", "foreground": "#586E75", "input.border": "#DCD4BC", "link.foreground": "#587573", "list.active.background": "#586E75", "list.active.border": "#586E75", "list.even.background": "#EEE8D599", "muted.background": "#eee8d5", "muted.foreground": "#93a1a1", "panel.background": "#fdf6e3", "popover.background": "#fdf6e3", "popover.foreground": "#073642", "primary.background": "#586E75", "primary.foreground": "#fdf6e3", "scrollbar.background": "#EEE8D500", "scrollbar.thumb.background": "#d5cbaf", "secondary.background": "#EEE8D5", "secondary.active.background": "#DCD4BC", "secondary.foreground": "#073642", "secondary.hover.background": "#DCD4BC88", "tab.active.background": "#fdf6e3", "tab.active.foreground": "#073642", "tab.background": "#fdf6e300", "tab.foreground": "#657b83", "tab_bar.background": "#eee8d5", "title_bar.background": "#f4ecd7", "title_bar.border": "#dcd4bc", "chart.grid": "#ebe5d4", "base.red": "#755858", "base.green": "#717558", "base.yellow": "#756e58", "base.blue": "#586975", "base.magenta": "#755866", "base.cyan": "#587573" }, "highlight": { "editor.foreground": "#586E75", "editor.background": "#FDF6E3", "editor.active_line.background": "#EEE8D5", "editor.line_number": "#93A1A1", "editor.active_line_number": "#073642", "editor.invisible": "#93A1A166", "conflict": "#DC322F", "created": "#859900", "deleted": "#DC322F", "error": "#DC322F", "hidden": "#93A1A1", "hint": "#2AA198", "ignored": "#93A1A1", "info": "#268BD2", "modified": "#B58900", "predictive": "#586E75", "renamed": "#2AA198", "success": "#859900", "unreachable": "#DC322F", "warning": "#B58900", "syntax": { "attribute": { "color": "#B58900" }, "boolean": { "color": "#CB4B16" }, "comment": { "color": "#93A1A1", "font_style": "italic" }, "comment.doc": { "color": "#93A1A1", "font_style": "italic" }, "constant": { "color": "#268BD2" }, "constructor": { "color": "#859900" }, "embedded": { "color": "#2AA198" }, "function": { "color": "#268BD2" }, "keyword": { "color": "#DC322F" }, "link_text": { "color": "#268BD2", "font_style": "underline" }, "link_uri": { "color": "#2AA198", "font_style": "italic" }, "number": { "color": "#D33682" }, "string": { "color": "#859900" }, "string.escape": { "color": "#2AA198" }, "string.regex": { "color": "#D33682" }, "string.special": { "color": "#B58900" }, "string.special.symbol": { "color": "#B58900" }, "tag": { "color": "#268BD2" }, "text.literal": { "color": "#586E75" }, "title": { "color": "#073642", "font_weight": 600 }, "type": { "color": "#2AA198" }, "property": { "color": "#586E75" }, "variable.special": { "color": "#DC322F" } } } }, { "name": "Solarized Dark", "author": "Ethan Schoonover", "url": "https://ethanschoonover.com/solarized", "mode": "dark", "colors": { "accent.background": "#073945", "accent.foreground": "#fdf6e3", "background": "#002B36", "border": "#103a44", "foreground": "#fdf6e3", "input.border": "#083e4c", "list.active.background": "#0d667d25", "list.active.border": "#0d667d", "list.even.background": "#15333A99", "muted.background": "#073642", "muted.foreground": "#839496", "panel.background": "#002b36", "popover.background": "#002b36", "popover.foreground": "#fdf6e3", "primary.background": "#0d667d", "primary.foreground": "#fdf6e3", "scrollbar.background": "#002b3600", "scrollbar.thumb.background": "#586e75", "secondary.background": "#073642", "secondary.active.background": "#083e4b", "secondary.foreground": "#fdf6e3", "secondary.hover.background": "#073945", "tab.active.background": "#002b36", "tab.active.foreground": "#fdf6e3", "tab.foreground": "#93a1a1", "tab_bar.background": "#07364266", "title_bar.background": "#09323d", "title_bar.border": "#16404b", "base.red": "#7d2f0d", "base.green": "#0d7d3f", "base.yellow": "#7d650d", "base.blue": "#0d507d", "base.magenta": "#7d0d43", "base.cyan": "#0d7d70" }, "highlight": { "editor.foreground": "#DDDDDD", "editor.background": "#002B36", "editor.active_line.background": "#073642", "editor.line_number": "#8F8F8F", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#93A1A166", "conflict": "#D2602D", "created": "#3f72e2", "created.background": "#0C4619", "deleted.background": "#46190C", "error.background": "#46190C", "error.border": "#802207", "hidden": "#9E9E9E", "hint": "#b283f8", "hint.background": "#250c4b", "hint.border": "#3f0891", "info.background": "#0C194D", "info.border": "#082190", "modified": "#B0A878", "modified.background": "#3A310E", "predictive": "#5D5945", "success.background": "#0C4619", "warning.background": "#3A310E", "warning.border": "#7B6508", "syntax": { "attribute": { "color": "#be9a52" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#E1D797" }, "keyword": { "color": "#E19773" }, "link_text": { "color": "#A86D3B", "font_style": "normal" }, "link_uri": { "color": "#6F6D66", "font_style": "italic" }, "number": { "color": "#E19773" }, "string": { "color": "#76BA53" }, "string.escape": { "color": "#76BA53" }, "string.regex": { "color": "#76BA53" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#A76D3B", "font_weight": 600 }, "type": { "color": "#A86D3B" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } } ] } ================================================ FILE: themes/spaceduck.json ================================================ { "name": "Spaceduck", "author": "Guillermo Rodriguez", "url": "https://github.com/pineapplegiant/spaceduck", "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "themes": [ { "name": "Spaceduck", "mode": "dark", "colors": { "accent.background": "#212440", "accent.foreground": "#a3a49d", "background": "#0F111B", "border": "#272C42", "foreground": "#a3a49d", "input.border": "#272C42", "link.active.foreground": "#089CC5", "link.foreground": "#089CC5", "link.hover.foreground": "#089CC5", "list.active.background": "#089CC511", "list.active.border": "#089CC5", "list.even.background": "#161928", "muted.background": "#272C4255", "muted.foreground": "#4b6479", "panel.background": "#003a5b", "popover.background": "#0F111B", "popover.foreground": "#a3a49d", "primary.background": "#089CC5", "primary.foreground": "#ecf0c1", "scrollbar.background": "#003a5b00", "scrollbar.thumb.background": "#24303a", "secondary.background": "#242547", "secondary.active.background": "#292b51", "secondary.foreground": "#a3a49d", "secondary.hover.background": "#242547", "tab.active.background": "#0F111B", "tab.foreground": "#838182", "title_bar.background": "#141724", "title_bar.border": "#272C42", "base.red": "#e33400", "base.green": "#5ccc96", "base.blue": "#00a3cc", "base.cyan": "#089CC5", "base.magenta": "#C86D8C", "base.magenta.light": "#C86D8C33", "base.yellow": "#b89c00" }, "highlight": { "editor.foreground": "#ecf0c1", "editor.background": "#0F111B", "editor.active_line.background": "#1c1d39", "editor.line_number": "#4b6479", "editor.active_line_number": "#ecf0c1", "editor.invisible": "#4b647966", "conflict": "#e33400", "created": "#5ccc96", "deleted": "#e33400", "error": "#e33400", "hidden": "#4b6479", "hint": "#C86D8C", "ignored": "#4b6479", "info": "#089CC5", "modified": "#f2ce00", "predictive": "#5D5945", "renamed": "#089CC5", "success": "#5ccc96", "unreachable": "#4b6479", "warning": "#f2ce00", "syntax": { "attribute": { "color": "#f2ce00" }, "boolean": { "color": "#f2ce00" }, "comment": { "color": "#4b6479", "font_style": "italic" }, "comment.doc": { "color": "#4b6479", "font_style": "italic" }, "constant": { "color": "#e33400" }, "constructor": { "color": "#5ccc96" }, "embedded": { "color": "#ecf0c1" }, "function": { "color": "#089CC5" }, "keyword": { "color": "#C86D8C" }, "link_text": { "color": "#089CC5", "font_style": "underline" }, "link_uri": { "color": "#089CC5", "font_style": "italic" }, "number": { "color": "#f2ce00" }, "string": { "color": "#5ccc96" }, "string.escape": { "color": "#5ccc96" }, "string.regex": { "color": "#5ccc96" }, "string.special": { "color": "#f2ce00" }, "string.special.symbol": { "color": "#f2ce00" }, "tag": { "color": "#C86D8C" }, "text.literal": { "color": "#ecf0c1" }, "title": { "color": "#f2ce00", "font_weight": 600 }, "type": { "color": "#089CC5" }, "property": { "color": "#ecf0c1" }, "variable.special": { "color": "#C86D8C" } } } } ] } ================================================ FILE: themes/tokyonight.json ================================================ { "name": "Tokyo", "author": "Folke Lemaitre", "url": "https://github.com/folke/tokyonight.nvim", "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "themes": [ { "name": "Tokyo Night", "mode": "dark", "colors": { "accent.background": "#2C3045", "accent.foreground": "#c0caf5", "background": "#1a1b26", "border": "#292e42", "window_border": "#3d4462", "ring": "#7aa2f7", "foreground": "#c0caf5", "link.active.foreground": "#7aa2f722", "link.foreground": "#7aa2f7", "link.hover.foreground": "#7aa2f7", "list.active.background": "#7aa2f722", "list.active.border": "#7aa2f7", "list.even.background": "#2C304599", "muted.background": "#2C304544", "muted.foreground": "#565f89", "panel.background": "#292e42", "popover.background": "#1a1b26", "popover.foreground": "#c0caf5", "primary.background": "#7aa2f7", "primary.foreground": "#1a1b26", "scrollbar.background": "#1a1b2600", "scrollbar.thumb.background": "#414868", "secondary.active.background": "#292e42", "secondary.background": "#292e42", "secondary.foreground": "#C0CAF5", "secondary.hover.background": "#2d3349", "title_bar.background": "#161720", "title_bar.border": "#292e42", "chart.grid": "#222332", "base.red": "#f7768e", "base.green": "#9ece6a", "base.yellow": "#e0af68", "base.blue": "#7aa2f7", "base.magenta": "#565f89", "base.cyan": "#7dcfff" }, "highlight": { "editor.foreground": "#c0caf5", "editor.background": "#1a1b26", "editor.active_line.background": "#292e42", "editor.line_number": "#565f89", "editor.active_line_number": "#c0caf5", "editor.invisible": "#565f8966", "conflict": "#f7768e", "created": "#9ece6a", "deleted": "#f7768e", "error": "#f7768e", "hidden": "#565f89", "hint": "#7dcfff", "ignored": "#565f89", "info": "#7aa2f7", "modified": "#e0af68", "predictive": "#565f89", "renamed": "#7aa2f7", "success": "#9ece6a", "unreachable": "#565f89", "warning": "#e0af68", "syntax": { "attribute": { "color": "#e0af68" }, "boolean": { "color": "#9ece6a" }, "comment": { "color": "#565f89", "font_style": "italic" }, "comment.doc": { "color": "#565f89", "font_style": "italic" }, "constant": { "color": "#7aa2f7" }, "constructor": { "color": "#e0af68" }, "embedded": { "color": "#c0caf5" }, "function": { "color": "#7aa2f7" }, "keyword": { "color": "#f7768e" }, "link_text": { "color": "#7dcfff", "font_style": "underline" }, "link_uri": { "color": "#7aa2f7", "font_style": "italic" }, "number": { "color": "#e0af68" }, "string": { "color": "#9ece6a" }, "string.escape": { "color": "#7dcfff" }, "string.regex": { "color": "#9ece6a" }, "string.special": { "color": "#e0af68" }, "string.special.symbol": { "color": "#e0af68" }, "tag": { "color": "#f7768e" }, "text.literal": { "color": "#c0caf5" }, "title": { "color": "#7aa2f7", "font_weight": 600 }, "type": { "color": "#7dcfff" }, "property": { "color": "#c0caf5" }, "variable.special": { "color": "#f7768e" } } } }, { "name": "Tokyo Storm", "mode": "dark", "colors": { "accent.background": "#414868", "accent.foreground": "#c0caf5", "background": "#24283b", "border": "#3d4462", "window_border": "#3d4462", "ring": "#7aa2f7", "foreground": "#c0caf5", "input.border": "#414868", "link.active.foreground": "#7aa2f7", "link.foreground": "#7aa2f7", "link.hover.foreground": "#7aa2f7", "list.active.background": "#7aa2f711", "list.active.border": "#7aa2f7", "list.even.background": "#41486844", "muted.background": "#292e42", "muted.foreground": "#565f89", "panel.background": "#292e42", "popover.background": "#24283b", "popover.foreground": "#c0caf5", "primary.background": "#7aa2f7", "primary.foreground": "#24283b", "scrollbar.background": "#24283b00", "scrollbar.thumb.background": "#414868", "secondary.active.background": "#323750", "secondary.background": "#292e42", "secondary.foreground": "#c0caf5", "secondary.hover.background": "#32375099", "title_bar.background": "#202435", "title_bar.border": "#3d4462", "base.red": "#f7768e", "base.green": "#9ece6a", "base.yellow": "#e0af68", "base.blue": "#7aa2f7", "base.magenta": "#b283f8", "base.cyan": "#7dcfff" }, "highlight": { "editor.foreground": "#B0B9E2", "editor.background": "#24283B", "editor.active_line.background": "#292E42", "editor.line_number": "#363C58", "editor.active_line_number": "#B0B9E2", "editor.invisible": "#565f8966", "conflict": "#D2602D", "created": "#3f72e2", "deleted": "#f7768e", "error": "#f7768e", "hidden": "#9E9E9E", "hint": "#b283f8", "modified": "#B0A878", "predictive": "#5D5945", "success": "#9ece6a", "warning": "#e0af68", "syntax": { "attribute": { "color": "#be9a52" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#E1D797" }, "keyword": { "color": "#E19773" }, "link_text": { "color": "#A86D3B", "font_style": "normal" }, "link_uri": { "color": "#6F6D66", "font_style": "italic" }, "number": { "color": "#E19773" }, "string": { "color": "#76BA53" }, "string.escape": { "color": "#76BA53" }, "string.regex": { "color": "#76BA53" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#A76D3B", "font_weight": 600 }, "type": { "color": "#A86D3B" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } }, { "name": "Tokyo Moon", "mode": "dark", "colors": { "accent.background": "#32364E", "accent.foreground": "#c0caf5", "background": "#222436", "border": "#323752", "window_border": "#404668", "ring": "#82aaff", "foreground": "#c0caf5", "input.border": "#444b6a", "link.active.foreground": "#82aaff", "link.foreground": "#82aaff", "link.hover.foreground": "#82aaff", "list.active.background": "#82aaff22", "list.active.border": "#82aaff", "list.background": "#222436", "list.even.background": "#282a40", "muted.background": "#2d3149", "muted.foreground": "#6e738d", "panel.background": "#2d3149", "popover.background": "#222436", "popover.foreground": "#c0caf5", "primary.background": "#82aaff", "primary.foreground": "#222436", "scrollbar.background": "#22243600", "scrollbar.thumb.background": "#444b6a", "secondary.active.background": "#2f334c", "secondary.background": "#2d3149", "secondary.foreground": "#8c94b5", "secondary.hover.background": "#31354f", "table.row.border": "#2d3149", "title_bar.background": "#1e2030", "title_bar.border": "#323752", "base.red": "#ff757f", "base.green": "#c3e88d", "base.yellow": "#ffc777", "base.blue": "#82aaff", "base.magenta": "#6e738d", "base.cyan": "#86e1fc" }, "highlight": { "editor.foreground": "#c0caf5", "editor.background": "#222436", "editor.active_line.background": "#2d3149", "editor.line_number": "#6e738d", "editor.active_line_number": "#c0caf5", "editor.invisible": "#565f8966", "conflict": "#ed8796", "created": "#c3e88d", "deleted": "#ed8796", "error": "#ed8796", "hidden": "#6e738d", "hint": "#86e1fc", "ignored": "#6e738d", "info": "#82aaff", "modified": "#ffc777", "predictive": "#6e738d", "renamed": "#82aaff", "success": "#c3e88d", "unreachable": "#6e738d", "warning": "#ffc777", "syntax": { "attribute": { "color": "#ffc777" }, "boolean": { "color": "#c3e88d" }, "comment": { "color": "#6e738d", "font_style": "italic" }, "comment.doc": { "color": "#6e738d", "font_style": "italic" }, "constant": { "color": "#82aaff" }, "constructor": { "color": "#ffc777" }, "embedded": { "color": "#c0caf5" }, "function": { "color": "#82aaff" }, "keyword": { "color": "#ed8796" }, "link_text": { "color": "#86e1fc", "font_style": "underline" }, "link_uri": { "color": "#82aaff", "font_style": "italic" }, "number": { "color": "#ffc777" }, "string": { "color": "#c3e88d" }, "string.escape": { "color": "#86e1fc" }, "string.regex": { "color": "#c3e88d" }, "string.special": { "color": "#ffc777" }, "string.special.symbol": { "color": "#ffc777" }, "tag": { "color": "#ed8796" }, "text.literal": { "color": "#c0caf5" }, "title": { "color": "#82aaff", "font_weight": 600 }, "type": { "color": "#86e1fc" }, "property": { "color": "#c0caf5" }, "variable.special": { "color": "#ed8796" } } } } ] } ================================================ FILE: themes/twilight.json ================================================ { "$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json", "name": "Twilight", "author": "MacroMates", "url": "https://macromates.com", "themes": [ { "name": "Twilight", "mode": "dark", "colors": { "accent.background": "#2b2b2b", "accent.foreground": "#dcdcdc", "background": "#141414", "border": "#343434", "foreground": "#dcdcdc", "input.border": "#3e3e3e", "list.active.background": "#CDA86911", "list.active.border": "#CDA86988", "list.even.background": "#1E1E1E99", "muted.background": "#2e2e2e88", "muted.foreground": "#828282", "primary.background": "#CDA869", "primary.foreground": "#1e1e1e", "scrollbar.background": "#1e1e1e00", "scrollbar.thumb.background": "#3e3e3e", "secondary.background": "#262626", "secondary.active.background": "#2a2a2a", "secondary.foreground": "#dcdcdc", "secondary.hover.background": "#272727", "tab.active.background": "#141414", "tab.active.foreground": "#dcdcdc", "tab.background": "#1E1E1E00", "tab.foreground": "#A9A9A9", "tab_bar.background": "#1E1E1E", "table.background": "#1e1e1e00", "title_bar.background": "#1e1e1e", "title_bar.border": "#343434", "base.red": "#c06d44", "base.red.light": "#de7c4c", "base.green": "#afb97a", "base.green.light": "#ccd88c", "base.yellow": "#c2a86c", "base.yellow.light": "#e2c47e", "base.blue": "#44474a", "base.blue.light": "#5a5e62", "base.magenta": "#b4be7c", "base.magenta.light": "#d0dc8e99", "base.cyan": "#778385", "base.cyan.light": "#8a989b" }, "highlight": { "editor.foreground": "#DDDDDD", "editor.background": "#000000", "editor.active_line.background": "#131313", "editor.line_number": "#8F8F8F", "editor.active_line_number": "#DDDDDD", "editor.invisible": "#9E9E9E66", "conflict": "#D2602D", "created": "#3f72e2", "hidden": "#9E9E9E", "hint": "#b283f8", "modified": "#B0A878", "predictive": "#5D5945", "syntax": { "attribute": { "color": "#be9a52" }, "boolean": { "color": "#E1D797" }, "comment": { "color": "#9E9E9E" }, "comment.doc": { "color": "#9E9E9E" }, "constant": { "color": "#E1D797" }, "constructor": { "color": "#b5af9a" }, "embedded": { "color": "#CACCCA" }, "function": { "color": "#E1D797" }, "keyword": { "color": "#E19773" }, "link_text": { "color": "#A86D3B", "font_style": "normal" }, "link_uri": { "color": "#6F6D66", "font_style": "italic" }, "number": { "color": "#E19773" }, "string": { "color": "#76BA53" }, "string.escape": { "color": "#76BA53" }, "string.regex": { "color": "#76BA53" }, "string.special": { "color": "#E1D797" }, "string.special.symbol": { "color": "#E1D797" }, "tag": { "color": "#b5af9a" }, "text.literal": { "color": "#E1D797" }, "title": { "color": "#A76D3B", "font_weight": 600 }, "type": { "color": "#A86D3B" }, "property": { "color": "#CACCCA" }, "variable.special": { "color": "#E19773" } } } } ] }