├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── DEVELOPMENT.md ├── LICENSE-APACHE ├── README.md ├── assets ├── .gitkeep └── icons │ ├── a-large-small.svg │ ├── arrow-down.svg │ ├── arrow-left.svg │ ├── arrow-right.svg │ ├── arrow-up.svg │ ├── asterisk.svg │ ├── bell.svg │ ├── book-open.svg │ ├── bot.svg │ ├── calendar.svg │ ├── chart-pie.svg │ ├── check.svg │ ├── chevron-down.svg │ ├── chevron-left.svg │ ├── chevron-right.svg │ ├── chevron-up.svg │ ├── chevrons-up-down.svg │ ├── circle-check.svg │ ├── circle-user.svg │ ├── circle-x.svg │ ├── close.svg │ ├── copy.svg │ ├── dash.svg │ ├── delete.svg │ ├── ellipsis-vertical.svg │ ├── ellipsis.svg │ ├── external-link.svg │ ├── eye-off.svg │ ├── eye.svg │ ├── frame.svg │ ├── gallery-vertical-end.svg │ ├── github.svg │ ├── globe.svg │ ├── heart-off.svg │ ├── heart.svg │ ├── inbox.svg │ ├── info.svg │ ├── inspector.svg │ ├── layout-dashboard.svg │ ├── loader-circle.svg │ ├── loader.svg │ ├── map.svg │ ├── maximize.svg │ ├── menu.svg │ ├── minimize.svg │ ├── minus.svg │ ├── moon.svg │ ├── palette.svg │ ├── panel-bottom-open.svg │ ├── panel-bottom.svg │ ├── panel-left-close.svg │ ├── panel-left-open.svg │ ├── panel-left.svg │ ├── panel-right-close.svg │ ├── panel-right-open.svg │ ├── panel-right.svg │ ├── plus.svg │ ├── resize-corner.svg │ ├── search.svg │ ├── settings-2.svg │ ├── settings.svg │ ├── sort-ascending.svg │ ├── sort-descending.svg │ ├── square-terminal.svg │ ├── star-off.svg │ ├── star.svg │ ├── sun.svg │ ├── thumbs-down.svg │ ├── thumbs-up.svg │ ├── triangle-alert.svg │ ├── window-close.svg │ ├── window-maximize.svg │ ├── window-minimize.svg │ └── window-restore.svg ├── crates ├── macros │ ├── Cargo.toml │ └── src │ │ ├── derive_into_plot.rs │ │ └── lib.rs ├── story │ ├── Cargo.toml │ ├── examples │ │ ├── accordion.rs │ │ ├── alert.rs │ │ ├── button.rs │ │ ├── code-editor.rs │ │ ├── description_list.rs │ │ ├── dock.rs │ │ ├── fixtures │ │ │ ├── test.c │ │ │ ├── test.go │ │ │ ├── test.js │ │ │ ├── test.py │ │ │ ├── test.rb │ │ │ ├── test.rs │ │ │ ├── test.sql │ │ │ └── test.zig │ │ ├── form.rs │ │ ├── html.html │ │ ├── html.rs │ │ ├── input.rs │ │ ├── list.rs │ │ ├── markdown.md │ │ ├── markdown.rs │ │ ├── modal.rs │ │ ├── popover.rs │ │ ├── scrollable.rs │ │ ├── sidebar.rs │ │ ├── switch.rs │ │ ├── table.rs │ │ ├── tabs.rs │ │ ├── text.rs │ │ ├── tiles.rs │ │ ├── toggle.rs │ │ ├── tooltip.rs │ │ └── webview.rs │ └── src │ │ ├── accordion_story.rs │ │ ├── alert_story.rs │ │ ├── assets.rs │ │ ├── badge_story.rs │ │ ├── button_story.rs │ │ ├── calendar_story.rs │ │ ├── chart_story.rs │ │ ├── checkbox_story.rs │ │ ├── clipboard_story.rs │ │ ├── color_picker_story.rs │ │ ├── date_picker_story.rs │ │ ├── drawer_story.rs │ │ ├── dropdown_story.rs │ │ ├── fixtures │ │ ├── color-wheel.svg │ │ ├── google.svg │ │ └── mod.rs │ │ ├── form_story.rs │ │ ├── icon_story.rs │ │ ├── image_story.rs │ │ ├── input_story.rs │ │ ├── kbd_story.rs │ │ ├── label_story.rs │ │ ├── lib.rs │ │ ├── list_story.rs │ │ ├── main.rs │ │ ├── menu_story.rs │ │ ├── modal_story.rs │ │ ├── notification_story.rs │ │ ├── number_input_story.rs │ │ ├── otp_input_story.rs │ │ ├── popover_story.rs │ │ ├── progress_story.rs │ │ ├── radio_story.rs │ │ ├── resizable_story.rs │ │ ├── scrollable_story.rs │ │ ├── sidebar_story.rs │ │ ├── slider_story.rs │ │ ├── switch_story.rs │ │ ├── table_story.rs │ │ ├── tabs_story.rs │ │ ├── tag_story.rs │ │ ├── textarea_story.rs │ │ ├── title_bar.rs │ │ ├── toggle_story.rs │ │ ├── tooltip_story.rs │ │ ├── webview_story.rs │ │ └── welcome_story.rs └── ui │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── default-colors.json │ ├── locales │ └── ui.yml │ ├── src │ ├── accordion.rs │ ├── actions.rs │ ├── alert.rs │ ├── animation.rs │ ├── badge.rs │ ├── breadcrumb.rs │ ├── button │ │ ├── button.rs │ │ ├── button_group.rs │ │ ├── dropdown_button.rs │ │ ├── mod.rs │ │ └── toggle.rs │ ├── chart │ │ ├── area_chart.rs │ │ ├── bar_chart.rs │ │ ├── line_chart.rs │ │ ├── mod.rs │ │ └── pie_chart.rs │ ├── checkbox.rs │ ├── clipboard.rs │ ├── color_picker.rs │ ├── colors.rs │ ├── description_list.rs │ ├── divider.rs │ ├── dock │ │ ├── dock.rs │ │ ├── invalid_panel.rs │ │ ├── mod.rs │ │ ├── panel.rs │ │ ├── stack_panel.rs │ │ ├── state.rs │ │ ├── tab_panel.rs │ │ └── tiles.rs │ ├── drawer.rs │ ├── dropdown.rs │ ├── event.rs │ ├── focusable.rs │ ├── form.rs │ ├── highlighter │ │ ├── highlighter.rs │ │ ├── languages.rs │ │ ├── languages │ │ │ ├── html │ │ │ │ ├── highlights.scm │ │ │ │ └── injections.scm │ │ │ ├── markdown │ │ │ │ ├── highlights.scm │ │ │ │ └── injections.scm │ │ │ ├── markdown_inline │ │ │ │ └── highlights.scm │ │ │ └── rust │ │ │ │ ├── README.md │ │ │ │ ├── highlights.scm │ │ │ │ └── injections.scm │ │ ├── mod.rs │ │ ├── registry.rs │ │ └── themes │ │ │ ├── README.md │ │ │ ├── dark.json │ │ │ └── light.json │ ├── history.rs │ ├── icon.rs │ ├── indicator.rs │ ├── input │ │ ├── blink_cursor.rs │ │ ├── change.rs │ │ ├── clear_button.rs │ │ ├── element.rs │ │ ├── marker.rs │ │ ├── mask_pattern.rs │ │ ├── mod.rs │ │ ├── mode.rs │ │ ├── number_input.rs │ │ ├── otp_input.rs │ │ ├── state.rs │ │ ├── text_input.rs │ │ └── text_wrapper.rs │ ├── inspector.rs │ ├── kbd.rs │ ├── label.rs │ ├── lib.rs │ ├── link.rs │ ├── list │ │ ├── list.rs │ │ ├── list_item.rs │ │ ├── loading.rs │ │ └── mod.rs │ ├── menu │ │ ├── context_menu.rs │ │ ├── mod.rs │ │ └── popup_menu.rs │ ├── modal.rs │ ├── notification.rs │ ├── plot │ │ ├── axis.rs │ │ ├── grid.rs │ │ ├── label.rs │ │ ├── mod.rs │ │ ├── scale.rs │ │ ├── scale │ │ │ ├── band.rs │ │ │ ├── linear.rs │ │ │ ├── point.rs │ │ │ └── sealed.rs │ │ ├── shape.rs │ │ ├── shape │ │ │ ├── arc.rs │ │ │ ├── area.rs │ │ │ ├── bar.rs │ │ │ ├── line.rs │ │ │ └── pie.rs │ │ └── tooltip.rs │ ├── popover.rs │ ├── progress.rs │ ├── radio.rs │ ├── resizable │ │ ├── mod.rs │ │ ├── panel.rs │ │ └── resize_handle.rs │ ├── root.rs │ ├── scroll │ │ ├── mod.rs │ │ ├── scrollable.rs │ │ ├── scrollable_mask.rs │ │ └── scrollbar.rs │ ├── sidebar │ │ ├── footer.rs │ │ ├── group.rs │ │ ├── header.rs │ │ ├── menu.rs │ │ └── mod.rs │ ├── skeleton.rs │ ├── slider.rs │ ├── styled.rs │ ├── svg_img.rs │ ├── switch.rs │ ├── tab.rs │ ├── tab │ │ ├── tab.rs │ │ └── tab_bar.rs │ ├── table.rs │ ├── table │ │ └── loading.rs │ ├── tag.rs │ ├── text │ │ ├── element.rs │ │ ├── html.rs │ │ ├── markdown.rs │ │ ├── mod.rs │ │ ├── text_view.rs │ │ └── utils.rs │ ├── theme.rs │ ├── time │ │ ├── calendar.rs │ │ ├── date_picker.rs │ │ ├── mod.rs │ │ └── utils.rs │ ├── title_bar.rs │ ├── tooltip.rs │ ├── virtual_list.rs │ ├── webview.rs │ └── window_border.rs │ └── tests │ └── fixtures │ └── layout.json ├── flake.lock ├── flake.nix └── script ├── bootstrap └── install-linux-deps /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = ["-C", "link-arg=/STACK:8000000"] 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "weekly" 7 | allow: 8 | - dependency-name: gpui 9 | - dependency-name: wry 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - "*" 7 | tags: 8 | - "*" 9 | 10 | jobs: 11 | test: 12 | name: Test 13 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - target: aarch64-apple-darwin 19 | run_on: macos-latest 20 | - target: x86_64-linux-gnu 21 | run_on: ubuntu-latest 22 | - target: x86_64-windows-msvc 23 | run_on: windows-latest 24 | runs-on: ${{ matrix.run_on }} 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Install system dependencies 28 | if: ${{ matrix.run_on != 'windows-latest' }} 29 | run: script/bootstrap 30 | - name: Machete 31 | if: ${{ matrix.run_on == 'macos-latest' }} 32 | uses: bnjbvr/cargo-machete@main 33 | - name: Setup | Cache Cargo 34 | uses: actions/cache@v4 35 | with: 36 | path: | 37 | ~/.cargo/bin/ 38 | ~/.cargo/registry/index/ 39 | ~/.cargo/registry/cache/ 40 | ~/.cargo/git/db/ 41 | target/ 42 | key: test-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} 43 | - name: Typo check 44 | if: ${{ matrix.run_on == 'macos-latest' }} 45 | run: | 46 | cargo install typos-cli || echo "typos-cli already installed" 47 | typos 48 | - name: Lint 49 | if: ${{ matrix.run_on == 'macos-latest' }} 50 | run: | 51 | cargo clippy -- --deny warnings 52 | - name: Build test 53 | run: | 54 | cargo test --all 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | /docks.json 4 | .vscode 5 | index.scip 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/macros", "crates/story", "crates/ui"] 3 | 4 | default-members = ["crates/story"] 5 | resolver = "2" 6 | 7 | [workspace.dependencies] 8 | gpui = { git = "https://github.com/zed-industries/zed.git" } 9 | gpui-component = { path = "crates/ui" } 10 | gpui-component-macros = { path = "crates/macros" } 11 | story = { path = "crates/story" } 12 | 13 | anyhow = "1" 14 | log = "0.4" 15 | serde = "1.0.203" 16 | serde_json = "1" 17 | smallvec = "1" 18 | 19 | [workspace.dependencies.windows] 20 | features = ["Wdk", "Wdk_System", "Wdk_System_SystemServices"] 21 | version = "0.58.0" 22 | 23 | [workspace.lints.clippy] 24 | almost_complete_range = "allow" 25 | arc_with_non_send_sync = "allow" 26 | borrowed_box = "allow" 27 | dbg_macro = "deny" 28 | let_underscore_future = "allow" 29 | map_entry = "allow" 30 | module_inception = "allow" 31 | non_canonical_partial_ord_impl = "allow" 32 | reversed_empty_ranges = "allow" 33 | single_range_in_vec_init = "allow" 34 | style = { level = "allow", priority = -1 } 35 | todo = "deny" 36 | type_complexity = "allow" 37 | 38 | [profile.dev] 39 | codegen-units = 16 40 | debug = "limited" 41 | split-debuginfo = "unpacked" 42 | 43 | [profile.dev.package] 44 | resvg = { opt-level = 3 } 45 | rustybuzz = { opt-level = 3 } 46 | taffy = { opt-level = 3 } 47 | ttf-parser = { opt-level = 3 } 48 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | ## Contributing Guide 2 | 3 | Contributing is welcome, if you find some bugs or have some ideas, please open an issue or submit a pull request. 4 | 5 | Please ensure that you are using clean code, follows the coding style or code organization in exists codes, and pass all tests. 6 | 7 | Please submit one PR for do one thing, this is important, that for help us to review your code more easily and push to merge fast. 8 | 9 | ## Development and Testing 10 | 11 | There have a lot of UI test cases in `crates/story` folder, if you change the exists features you can run the tests is working. 12 | 13 | ### Run story 14 | 15 | Use `cargo run` to run a complete story examples. 16 | 17 | ```bash 18 | cargo run 19 | ``` 20 | 21 | ### Run single example 22 | 23 | There also available some split examples, run `cargo run --example` to see the available examples. 24 | 25 | ```bash 26 | cargo run --example table 27 | ``` 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPUI Component 2 | 3 | UI components for building fantastic desktop applications using [GPUI](https://gpui.rs). 4 | 5 | ## Features 6 | 7 | - **Richness**: 40+ cross-platform desktop UI components. 8 | - **Native**: Inspired by macOS and Windows controls, combined with shadcn/ui design for a modern experience. 9 | - **Ease of Use**: Stateless `RenderOnce` components, simple and user-friendly. 10 | - **Customizable**: Built-in `Theme` and `ThemeColor`, supporting multi-theme and variable-based configurations. 11 | - **Versatile**: Supports sizes like `xs`, `sm`, `md`, and `lg`. 12 | - **Flexible Layout**: Dock layout for panel arrangements, resizing, and freeform (Tiles) layouts. 13 | - **High Performance**: Virtualized Table and List components for smooth large-data rendering. 14 | - **Content Rendering**: Native support for Markdown and simple HTML. 15 | - **Charting**: Built-in charts for visualization your data. 16 | - **Code Highlighting**: Code Editor and Syntax highlighting. 17 | 18 | ## Showcase 19 | 20 | Here is the first application: [Longbridge Pro](https://longbridge.com/desktop), built using GPUI Component. 21 | 22 | Image 23 | 24 | We built multi-theme support in the application. This feature is not included in GPUI Component itself, but is based on the `Theme` feature, so it's easy to implement. 25 | 26 | ## Usage 27 | 28 | GPUI and GPUI Component are still in development, so you need to add dependencies by git. 29 | 30 | ```toml 31 | gpui = { git = "https://github.com/zed-industries/zed.git" } 32 | gpui-component = { git = "https://github.com/longbridge/gpui-component.git" } 33 | ``` 34 | 35 | ### WebView 36 | 37 | > Still early and experimental; there are a lot of limitations. 38 | 39 | GPUI Component has a `WebView` element based on [Wry](https://github.com/tauri-apps/wry). This is an optional feature, which you can enable with a feature flag. 40 | 41 | ```toml 42 | gpui-component = { git = "https://github.com/longbridge/gpui-component.git", features = ["webview"] } 43 | ``` 44 | 45 | More usage examples can be found in the [story](https://github.com/longbridge/gpui-component/tree/main/crates/story) directory. 46 | 47 | ### Icons 48 | 49 | GPUI Component has an `Icon` element, but it does not include SVG files by default. 50 | 51 | The example uses [Lucide](https://lucide.dev) icons, but you can use any icons you like. Just name the SVG files as defined in [IconName](https://github.com/longbridge/gpui-component/blob/main/crates/ui/src/icon.rs#L86). You can add any icons you need to your project. 52 | 53 | ## Development 54 | 55 | We have a gallery of applications built with GPUI Component. 56 | 57 | ```bash 58 | cargo run 59 | ``` 60 | 61 | More examples can be found in the `examples` directory. You can run them with `cargo run --example `. 62 | 63 | Check out [DEVELOPMENT.md](DEVELOPMENT.md) for more details. 64 | 65 | ## License 66 | 67 | Apache-2.0 68 | 69 | - UI design based on [shadcn/ui](https://ui.shadcn.com). 70 | - Icons from [Lucide](https://lucide.dev). 71 | -------------------------------------------------------------------------------- /assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longbridge/gpui-component/01197769dfe6b2dd9406e90679d986a00a84a297/assets/.gitkeep -------------------------------------------------------------------------------- /assets/icons/a-large-small.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/asterisk.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/book-open.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/bot.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/chart-pie.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/chevrons-up-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/circle-check.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/circle-user.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/circle-x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/dash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/ellipsis-vertical.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/ellipsis.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/external-link.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/eye-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/frame.svg: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /assets/icons/gallery-vertical-end.svg: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /assets/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/heart-off.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/inbox.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/inspector.svg: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /assets/icons/layout-dashboard.svg: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /assets/icons/loader-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/icons/map.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/icons/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/moon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/palette.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/panel-bottom-open.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/panel-bottom.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/panel-left-close.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/panel-left-open.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/panel-left.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/panel-right-close.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/panel-right-open.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/panel-right.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/resize-corner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/settings-2.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /assets/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /assets/icons/sort-ascending.svg: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/icons/sort-descending.svg: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/icons/square-terminal.svg: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /assets/icons/star-off.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/star.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/sun.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/thumbs-down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/thumbs-up.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/triangle-alert.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/window-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/icons/window-maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/icons/window-minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/icons/window-restore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /crates/macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2024" 3 | name = "gpui-component-macros" 4 | version = "0.1.0" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [lints] 10 | workspace = true 11 | 12 | [dependencies] 13 | proc-macro2 = "1.0" 14 | quote = "1.0" 15 | syn = "2.0" 16 | 17 | [package.metadata.cargo-machete] 18 | ignored = ["proc-macro2"] 19 | -------------------------------------------------------------------------------- /crates/macros/src/derive_into_plot.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{DeriveInput, parse_macro_input}; 4 | 5 | pub fn derive_into_plot(input: TokenStream) -> TokenStream { 6 | let ast = parse_macro_input!(input as DeriveInput); 7 | let type_name = &ast.ident; 8 | let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); 9 | 10 | let expanded = quote! { 11 | impl #impl_generics gpui::IntoElement for #type_name #type_generics #where_clause { 12 | type Element = Self; 13 | 14 | fn into_element(self) -> Self::Element { 15 | self 16 | } 17 | } 18 | 19 | impl #impl_generics gpui::Element for #type_name #type_generics #where_clause { 20 | type RequestLayoutState = (); 21 | type PrepaintState = (); 22 | 23 | fn id(&self) -> Option { 24 | None 25 | } 26 | 27 | fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { 28 | None 29 | } 30 | 31 | fn request_layout( 32 | &mut self, 33 | _: Option<&gpui::GlobalElementId>, 34 | _: Option<&gpui::InspectorElementId>, 35 | window: &mut gpui::Window, 36 | cx: &mut gpui::App, 37 | ) -> (gpui::LayoutId, Self::RequestLayoutState) { 38 | let style = gpui::Style { 39 | size: gpui::Size::full(), 40 | ..Default::default() 41 | }; 42 | 43 | (window.request_layout(style, None, cx), ()) 44 | } 45 | 46 | fn prepaint( 47 | &mut self, 48 | _: Option<&gpui::GlobalElementId>, 49 | _: Option<&gpui::InspectorElementId>, 50 | _: gpui::Bounds, 51 | _: &mut Self::RequestLayoutState, 52 | _: &mut gpui::Window, 53 | _: &mut gpui::App, 54 | ) -> Self::PrepaintState { 55 | } 56 | 57 | fn paint( 58 | &mut self, 59 | _: Option<&gpui::GlobalElementId>, 60 | _: Option<&gpui::InspectorElementId>, 61 | bounds: gpui::Bounds, 62 | _: &mut Self::RequestLayoutState, 63 | _: &mut Self::PrepaintState, 64 | window: &mut gpui::Window, 65 | cx: &mut gpui::App, 66 | ) { 67 | ::paint(self, bounds, window, cx) 68 | } 69 | } 70 | }; 71 | 72 | TokenStream::from(expanded) 73 | } 74 | -------------------------------------------------------------------------------- /crates/macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | mod derive_into_plot; 4 | 5 | #[proc_macro_derive(IntoPlot)] 6 | pub fn derive_into_plot(input: TokenStream) -> TokenStream { 7 | derive_into_plot::derive_into_plot(input) 8 | } 9 | -------------------------------------------------------------------------------- /crates/story/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "story" 4 | publish = false 5 | version = "0.1.0" 6 | 7 | [dependencies] 8 | anyhow.workspace = true 9 | gpui.workspace = true 10 | gpui-component = { workspace = true, features = ["webview"] } 11 | 12 | chrono = "0.4" 13 | fake = { version = "2.10.0", features = ["dummy"] } 14 | rand = "0.8" 15 | raw-window-handle = { version = "0.6", features = ["std"] } 16 | regex = "1" 17 | reqwest_client = { git = "https://github.com/zed-industries/zed.git" } 18 | rust-embed = "8.5.0" 19 | serde = "1" 20 | serde_json = "1" 21 | unindent = "0.2.3" 22 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 23 | 24 | [target.'cfg(target_os = "linux")'.dependencies] 25 | gtk = { version = "0.18" } 26 | 27 | [lints] 28 | workspace = true 29 | -------------------------------------------------------------------------------- /crates/story/examples/accordion.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{AccordionStory, Assets}; 3 | 4 | pub struct Example { 5 | root: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let root = AccordionStory::view(window, cx); 11 | 12 | Self { root } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.root.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Accordion Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/alert.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | 3 | use story::{AlertStory, Assets}; 4 | 5 | pub struct Example { 6 | story: Entity, 7 | } 8 | 9 | impl Example { 10 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 11 | Self { 12 | story: AlertStory::view(window, cx), 13 | } 14 | } 15 | 16 | fn view(window: &mut Window, cx: &mut App) -> Entity { 17 | cx.new(|cx| Self::new(window, cx)) 18 | } 19 | } 20 | 21 | impl Render for Example { 22 | fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { 23 | self.story.clone() 24 | } 25 | } 26 | 27 | fn main() { 28 | let app = Application::new().with_assets(Assets); 29 | 30 | app.run(move |cx| { 31 | story::init(cx); 32 | cx.activate(true); 33 | 34 | story::create_new_window("Alert Example", Example::view, cx); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /crates/story/examples/button.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, ButtonStory}; 3 | 4 | pub struct Example { 5 | root: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let root = ButtonStory::view(window, cx); 11 | 12 | Self { root } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.root.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Button Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/fixtures/test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define MAX_NAME_LENGTH 100 7 | #define BUFFER_SIZE 1024 8 | 9 | 10 | /* Constants for configuration limits */ 11 | #define MIN_TIMEOUT 1000 12 | #define MAX_TIMEOUT 10000 13 | #define MAX_RETRIES 5 14 | 15 | /** 16 | * HelloWorld structure represents a greeter object with configuration 17 | * Contains: 18 | * - name: String identifier for the greeter (max 100 chars) 19 | * - created_at: Timestamp when instance was created 20 | * - timeout: Milliseconds to wait between greetings (1000-10000) 21 | * - retries: Number of retry attempts (0-5) 22 | */ 23 | typedef struct { 24 | char name[MAX_NAME_LENGTH]; 25 | time_t created_at; 26 | int timeout; 27 | int retries; 28 | } HelloWorld; 29 | 30 | HelloWorld* hello_world_create(const char* name) { 31 | HelloWorld* hw = (HelloWorld*)malloc(sizeof(HelloWorld)); 32 | if (!hw) return NULL; 33 | 34 | strncpy(hw->name, name, MAX_NAME_LENGTH - 1); 35 | hw->name[MAX_NAME_LENGTH - 1] = '\0'; 36 | hw->created_at = time(NULL); 37 | hw->timeout = 5000; 38 | hw->retries = 3; 39 | 40 | return hw; 41 | } 42 | 43 | void hello_world_destroy(HelloWorld* hw) { 44 | if (hw) { 45 | free(hw); 46 | } 47 | } 48 | 49 | void hello_world_greet(HelloWorld* hw, const char** names, int count) { 50 | for (int i = 0; i < count; i++) { 51 | printf("Hello, %s from %s!\n", names[i], hw->name); 52 | } 53 | } 54 | 55 | void hello_world_configure(HelloWorld* hw, int timeout, int retries) { 56 | hw->timeout = timeout; 57 | hw->retries = retries; 58 | } 59 | 60 | char* hello_world_generate_report(const HelloWorld* hw) { 61 | char* report = (char*)malloc(BUFFER_SIZE); 62 | char time_str[26]; 63 | ctime_r(&hw->created_at, time_str); 64 | time_str[24] = '\0'; 65 | 66 | snprintf(report, BUFFER_SIZE, 67 | "HelloWorld Report\n" 68 | "================\n" 69 | "Name: %s\n" 70 | "Created: %s\n" 71 | "Timeout: %d\n" 72 | "Retries: %d\n", 73 | hw->name, time_str, hw->timeout, hw->retries); 74 | 75 | return report; 76 | } 77 | 78 | int main() { 79 | HelloWorld* greeter = hello_world_create("C Example"); 80 | 81 | const char* names[] = {"Alice", "Bob"}; 82 | int names_count = sizeof(names) / sizeof(names[0]); 83 | 84 | hello_world_configure(greeter, 1000, 5); 85 | hello_world_greet(greeter, names, names_count); 86 | 87 | char* report = hello_world_generate_report(greeter); 88 | printf("%s\n", report); 89 | free(report); 90 | 91 | hello_world_destroy(greeter); 92 | return 0; 93 | } 94 | -------------------------------------------------------------------------------- /crates/story/examples/fixtures/test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // Default timeout duration for operations 12 | const timeout = 5 * time.Second 13 | 14 | var ( 15 | instanceCount int 16 | mu sync.RWMutex 17 | ) 18 | 19 | /** 20 | * HelloWorld represents a greeter with configuration options 21 | * Contains: 22 | * - name: String identifier for the greeter 23 | * - createdAt: Timestamp when instance was created 24 | * - options: Map of configuration options 25 | */ 26 | type HelloWorld struct { 27 | name string 28 | createdAt time.Time 29 | options map[string]interface{} 30 | } 31 | 32 | type Config struct { 33 | Timeout time.Duration `json:"timeout"` 34 | Retries int `json:"retries"` 35 | Debug bool `json:"debug"` 36 | } 37 | 38 | func NewHelloWorld(name string) *HelloWorld { 39 | mu.Lock() 40 | instanceCount++ 41 | mu.Unlock() 42 | return &HelloWorld{ 43 | name: name, 44 | createdAt: time.Now(), 45 | options: make(map[string]interface{}), 46 | } 47 | } 48 | 49 | func (h *HelloWorld) Greet(ctx context.Context, names ...string) error { 50 | for _, name := range names { 51 | select { 52 | case <-ctx.Done(): 53 | return ctx.Err() 54 | default: 55 | fmt.Printf("Hello, %s!\n", name) 56 | } 57 | } 58 | return nil 59 | } 60 | 61 | func (h *HelloWorld) Configure(cfg Config) { 62 | h.options["timeout"] = cfg.Timeout 63 | h.options["retries"] = cfg.Retries 64 | h.options["debug"] = cfg.Debug 65 | } 66 | 67 | func (h *HelloWorld) generateReport() string { 68 | data, _ := json.MarshalIndent(h.options, "", " ") 69 | return fmt.Sprintf(` 70 | HelloWorld Report 71 | ================ 72 | Name: %s 73 | Created: %s 74 | Options: %s 75 | `, h.name, h.createdAt.Format(time.RFC3339), string(data)) 76 | } 77 | 78 | func main() { 79 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 80 | defer cancel() 81 | 82 | greeter := NewHelloWorld("Go") 83 | greeter.Configure(Config{ 84 | Timeout: timeout, 85 | Retries: 3, 86 | Debug: true, 87 | }) 88 | 89 | if err := greeter.Greet(ctx, "Alice", "Bob"); err != nil { 90 | fmt.Printf("Error greeting: %v\n", err) 91 | } 92 | fmt.Println(greeter.generateReport()) 93 | } 94 | -------------------------------------------------------------------------------- /crates/story/examples/fixtures/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class representing a HelloWorld greeter with various utility methods 3 | * @class HelloWorld 4 | */ 5 | class HelloWorld { 6 | // Version number of the HelloWorld class 7 | static VERSION = '1.0.0'; 8 | // Counter to track number of class instances 9 | static #instanceCount = 0; 10 | 11 | // Private instance fields 12 | #name; 13 | #options; 14 | #createdAt; 15 | 16 | /** 17 | * Creates a new HelloWorld instance 18 | * @param {string} name - The name to use for greetings 19 | * @param {Object} options - Configuration options 20 | */ 21 | constructor(name = 'World', options = {}) { 22 | this.#name = name; 23 | this.#options = options; 24 | this.#createdAt = new Date(); 25 | HelloWorld.#instanceCount++; 26 | } 27 | 28 | static getInstanceCount() { 29 | return HelloWorld.#instanceCount; 30 | } 31 | 32 | get name() { 33 | return this.#name; 34 | } 35 | 36 | set name(value) { 37 | this.#name = value; 38 | } 39 | 40 | async greet(...names) { 41 | try { 42 | for (const name of names) { 43 | await new Promise(resolve => setTimeout(resolve, 100)); 44 | console.log(`Hello, ${name}!`); 45 | } 46 | } catch (error) { 47 | console.error(`Error: ${error.message}`); 48 | } 49 | } 50 | 51 | configure(options = {}) { 52 | Object.assign(this.#options, options); 53 | } 54 | 55 | *generateSequence(start = 0, end = 10) { 56 | for (let i = start; i <= end; i++) yield i; 57 | } 58 | 59 | processNames(names) { 60 | return names 61 | .filter(name => name.length > 0) 62 | .map(name => name.toUpperCase()) 63 | .sort(); 64 | } 65 | } 66 | 67 | const greeter = new HelloWorld('JavaScript'); 68 | 69 | (async () => { 70 | const uniqueNames = new Set(['Alice', 'Bob']); 71 | await greeter.greet(...uniqueNames); 72 | 73 | for (const num of greeter.generateSequence(0, 5)) { 74 | console.log(num); 75 | } 76 | })(); 77 | -------------------------------------------------------------------------------- /crates/story/examples/fixtures/test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Optional, List, Dict, Any 3 | from dataclasses import dataclass 4 | from datetime import datetime 5 | import json 6 | import asyncio 7 | 8 | # Data class for configuration settings 9 | @dataclass 10 | class Config: 11 | timeout: int = 5000 # Default timeout in milliseconds 12 | retries: int = 3 # Number of retry attempts 13 | debug: bool = False # Debug mode flag 14 | 15 | """ 16 | HelloWorld class provides greeting functionality with configuration options. 17 | 18 | Features: 19 | - Async greetings with customizable names 20 | - Configuration management 21 | - Instance tracking 22 | - Report generation 23 | 24 | Example: 25 | greeter = HelloWorld("Python") 26 | await greeter.greet("Alice", "Bob") 27 | """ 28 | class HelloWorld: 29 | VERSION: str = "1.0.0" 30 | _instance_count: int = 0 31 | 32 | def __init__(self, name: str = "World", options: Optional[Dict[str, Any]] = None): 33 | self._name = name 34 | self._options = options or {} 35 | self._created_at = datetime.now() 36 | self._config = Config() 37 | HelloWorld._instance_count += 1 38 | 39 | @property 40 | def name(self) -> str: 41 | return self._name 42 | 43 | @name.setter 44 | def name(self, value: str) -> None: 45 | if not value: 46 | raise ValueError("Name cannot be empty") 47 | self._name = value 48 | 49 | @classmethod 50 | def get_instance_count(cls) -> int: 51 | return cls._instance_count 52 | 53 | async def greet(self, *names: str) -> None: 54 | try: 55 | for name in names: 56 | await asyncio.sleep(0.1) 57 | print(f"Hello, {name}!") 58 | except Exception as e: 59 | print(f"Error: {str(e)}") 60 | 61 | def process_names(self, names: List[str] = None) -> List[str]: 62 | if names is None: 63 | names = [] 64 | return sorted([name.upper() for name in names if name]) 65 | 66 | def _generate_report(self) -> str: 67 | return f""" 68 | HelloWorld Report 69 | ================ 70 | Name: {self._name} 71 | Created: {self._created_at.isoformat()} 72 | Options: {json.dumps(self._options, indent=2)} 73 | """ 74 | 75 | def __str__(self) -> str: 76 | return f"HelloWorld(name={self._name})" 77 | 78 | async def main(): 79 | greeter = HelloWorld("Python") 80 | await greeter.greet("Alice", "Bob", "Charlie") 81 | print(greeter._generate_report()) 82 | 83 | if __name__ == "__main__": 84 | asyncio.run(main()) 85 | -------------------------------------------------------------------------------- /crates/story/examples/fixtures/test.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'date' 3 | 4 | # Module for logging functionality 5 | module Logging 6 | LOG_LEVELS = %i[debug info warn error].freeze 7 | end 8 | 9 | # HelloWorld class provides greeting functionality with configuration options 10 | # @author Example Author 11 | # @version 1.0.0 12 | # Features: 13 | # - Configurable greetings with multiple names 14 | # - Instance tracking 15 | # - Report generation 16 | # - Logging capabilities 17 | class HelloWorld < Object 18 | include Logging 19 | 20 | @@instances = 0 21 | VERSION = '1.0.0' 22 | 23 | attr_accessor :name 24 | attr_reader :created_at 25 | 26 | def initialize(name: 'World', options: {}) 27 | @name = name 28 | @created_at = Time.now 29 | @options = options 30 | @@instances += 1 31 | yield self if block_given? 32 | end 33 | 34 | def self.instance_count(format: :short) 35 | case format 36 | when :short then @@instances.to_s 37 | when :long then "Total instances: #{@@instances}" 38 | end 39 | end 40 | 41 | def greet(*names) 42 | names.each { |n| puts "Hello, #{n}!" } 43 | rescue => e 44 | puts "Error: #{e.message}" 45 | end 46 | 47 | def configure(timeout: 5000, retries: 3) 48 | @options.merge!(timeout: timeout, retries: retries) 49 | end 50 | 51 | def configured? 52 | !@options.empty? 53 | end 54 | 55 | def process_names(names) 56 | names.map(&:upcase).select(&:present?) 57 | end 58 | 59 | private 60 | 61 | def generate_report 62 | <<~REPORT 63 | HelloWorld Report 64 | ================ 65 | Name: #{@name} 66 | Created: #{@created_at} 67 | Options: #{@options.to_json} 68 | REPORT 69 | end 70 | end 71 | 72 | # Create new greeter instance with configuration block 73 | greeter = HelloWorld.new(name: 'Ruby') { |g| g.configure(timeout: 1000) } 74 | 75 | # Process array and handle errors 76 | numbers = [1, 2, 3, 4, 5] 77 | doubled = numbers.map { |n| n * 2 } 78 | 79 | # Email validation 80 | EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i 81 | validator = ->(email) { email.match?(EMAIL_REGEX) } 82 | 83 | begin 84 | greeter.greet('Alice', 'Bob') 85 | rescue StandardError => e 86 | puts "Error occurred: #{e.message}" 87 | ensure 88 | puts "Execution completed at #{Time.now}" 89 | end 90 | -------------------------------------------------------------------------------- /crates/story/examples/fixtures/test.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM users 3 | WHERE email ilike '%test%' 4 | AND deleted_at IS NOT NULL 5 | LIMIT 1; 6 | -------------------------------------------------------------------------------- /crates/story/examples/fixtures/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const json = std.json; 3 | const time = std.time; 4 | const HashMap = std.HashMap; 5 | 6 | pub const VERSION = "1.0.0"; 7 | 8 | pub const HelloError = error{ 9 | InvalidName, 10 | Timeout, 11 | }; 12 | 13 | pub const HelloWorld = struct { 14 | name: []const u8, 15 | options: HashMap([]const u8, json.Value), 16 | created_at: i64, 17 | 18 | pub fn init(allocator: *std.mem.Allocator, name: []const u8) !HelloWorld { 19 | return HelloWorld{ 20 | .name = name, 21 | .options = HashMap([]const u8, json.Value).init(allocator), 22 | .created_at = time.timestamp(), 23 | }; 24 | } 25 | 26 | pub fn deinit(self: *HelloWorld) void { 27 | self.options.deinit(); 28 | } 29 | 30 | pub fn greet(self: *const HelloWorld, names: []const []const u8) !void { 31 | for (names) |name| { 32 | time.sleep(100 * time.millisecond); 33 | std.debug.print("Hello, {s}!\n", .{name}); 34 | } 35 | } 36 | 37 | pub fn configure(self: *HelloWorld, options: HashMap([]const u8, json.Value)) void { 38 | var it = options.iterator(); 39 | while (it.next()) |entry| { 40 | self.options.put(entry.key, entry.value) catch {}; 41 | } 42 | } 43 | 44 | pub fn generateReport(self: *const HelloWorld) ![]const u8 { 45 | var report = std.ArrayList(u8).init(std.heap.page_allocator); 46 | defer report.deinit(); 47 | 48 | try report.writer().print( 49 | \\HelloWorld Report 50 | \\================ 51 | \\Name: {s} 52 | \\Created: {} 53 | \\Options: {} 54 | \\ 55 | , .{ 56 | self.name, 57 | self.created_at, 58 | self.options, 59 | }); 60 | 61 | return report.toOwnedSlice(); 62 | } 63 | }; 64 | 65 | pub fn main() !void { 66 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 67 | defer _ = gpa.deinit(); 68 | const allocator = &gpa.allocator; 69 | 70 | var greeter = try HelloWorld.init(allocator, "Zig"); 71 | defer greeter.deinit(); 72 | 73 | var config = HashMap([]const u8, json.Value).init(allocator); 74 | try config.put("timeout", json.Value{ .Integer = 5000 }); 75 | try config.put("retries", json.Value{ .Integer = 3 }); 76 | 77 | greeter.configure(config); 78 | 79 | const names = [_][]const u8{ "Alice", "Bob" }; 80 | try greeter.greet(&names); 81 | 82 | const report = try greeter.generateReport(); 83 | std.debug.print("{s}\n", .{report}); 84 | } 85 | -------------------------------------------------------------------------------- /crates/story/examples/form.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, FormStory}; 3 | 4 | pub struct Example { 5 | story: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let story = FormStory::view(window, cx); 11 | 12 | Self { story } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.story.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Form Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/html.html: -------------------------------------------------------------------------------- 1 |
2 |

A simple HTML document

3 |
4 | Here is a test in div. 5 |

6 | This is a paragraph inside a div element, have 7 | @Mention Tag 8 | Link with: Bold italic, bold, 9 | italic, and 10 | code text. 11 |

12 | 15 |

This is a text before blockquote.

16 |
17 | This is before paragraph in blockquote. 18 |

This is a second blockquote paragraph.

19 |

This is after paragraph in blockquote.

20 |
21 | 26 |

27 | 28 |

29 |
30 |

This is second paragraph.

31 |
32 | A text after div. 33 |

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

38 |
39 | 40 |
41 |

List

42 | Example for Bulleted and Numbered List: 43 | 44 |

Numbered List

45 | Text before the Numbered List. 46 |
    47 |
  1. 48 | Numbered item 1 49 |
      50 |
    1. Sub item 1
    2. 51 |
    3. Sub item 2
    4. 52 |
    53 |
  2. 54 |
  3. Numbered item 2
  4. 55 |
  5. Numbered item 3
  6. 56 |
57 | Text after the Numbered List. 58 |

Bulleted List

59 | Text before the Bulleted List. 60 |
    61 |
  • 62 | Bullet 1 63 |
      64 |
    1. Sub Numbered 1
    2. 65 |
    3. Sub Numbered 2
    4. 66 |
    67 |
  • 68 |
  • Bullet 2
  • 69 |
70 | Text after the Bulleted List. 71 |
72 | Text before the section. 73 |
74 |

Table

75 | Text before the table. 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
Head 1Head 2
This Cell have 2 span
Cell 3Cell 4
93 | Text after the table. 94 |
95 | Text after the section. 96 |
97 |

Images

98 | 99 |

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

103 | 104 | Text before the image. 105 | 106 | Text after the image. 107 | 108 |
109 |
-------------------------------------------------------------------------------- /crates/story/examples/html.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use gpui_component::{ 3 | highlighter::Language, 4 | input::{InputState, TabSize, TextInput}, 5 | resizable::{h_resizable, resizable_panel, ResizableState}, 6 | text::TextView, 7 | }; 8 | use story::Assets; 9 | 10 | pub struct Example { 11 | input_state: Entity, 12 | resizable_state: Entity, 13 | _subscribe: Subscription, 14 | } 15 | 16 | const EXAMPLE: &str = include_str!("./html.html"); 17 | 18 | impl Example { 19 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 20 | let input_state = cx.new(|cx| { 21 | InputState::new(window, cx) 22 | .code_editor(Language::Html) 23 | .tab_size(TabSize { 24 | tab_size: 4, 25 | hard_tabs: false, 26 | }) 27 | .default_value(EXAMPLE) 28 | .placeholder("Enter your HTML here...") 29 | }); 30 | 31 | let resizable_state = ResizableState::new(cx); 32 | 33 | let _subscribe = cx.subscribe( 34 | &input_state, 35 | |_, _, _: &gpui_component::input::InputEvent, cx| { 36 | cx.notify(); 37 | }, 38 | ); 39 | 40 | Self { 41 | input_state, 42 | resizable_state, 43 | _subscribe, 44 | } 45 | } 46 | 47 | fn view(window: &mut Window, cx: &mut App) -> Entity { 48 | cx.new(|cx| Self::new(window, cx)) 49 | } 50 | } 51 | 52 | impl Render for Example { 53 | fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { 54 | h_resizable("container", self.resizable_state.clone()) 55 | .child( 56 | resizable_panel().child( 57 | div() 58 | .id("source") 59 | .size_full() 60 | .font_family("Menlo") 61 | .text_size(px(13.)) 62 | .child(TextInput::new(&self.input_state).h_full().appearance(false)), 63 | ), 64 | ) 65 | .child( 66 | resizable_panel().child( 67 | div() 68 | .id("preview") 69 | .size_full() 70 | .p_5() 71 | .overflow_y_scroll() 72 | .child(TextView::html("preview", self.input_state.read(cx).value())), 73 | ), 74 | ) 75 | } 76 | } 77 | 78 | fn main() { 79 | let app = Application::new().with_assets(Assets); 80 | 81 | app.run(move |cx| { 82 | story::init(cx); 83 | cx.activate(true); 84 | 85 | story::create_new_window("HTML Example", Example::view, cx); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /crates/story/examples/input.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, InputStory}; 3 | 4 | pub struct Example { 5 | root: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let root = InputStory::view(window, cx); 11 | 12 | Self { root } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.root.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Input Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/list.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, ListStory}; 3 | 4 | pub struct Example { 5 | root: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let root = ListStory::view(window, cx); 11 | 12 | Self { root } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.root.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("List Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/markdown.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use gpui::*; 4 | use gpui_component::{ 5 | highlighter::{HighlightTheme, Language}, 6 | input::{InputEvent, InputState, TabSize, TextInput}, 7 | resizable::{h_resizable, resizable_panel, ResizableState}, 8 | text::{TextView, TextViewStyle}, 9 | ActiveTheme as _, 10 | }; 11 | use story::Assets; 12 | 13 | pub struct Example { 14 | input_state: Entity, 15 | resizable_state: Entity, 16 | } 17 | 18 | const EXAMPLE: &str = include_str!("./markdown.md"); 19 | 20 | impl Example { 21 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 22 | let input_state = cx.new(|cx| { 23 | InputState::new(window, cx) 24 | .code_editor(Language::Markdown) 25 | .line_number(true) 26 | .tab_size(TabSize { 27 | tab_size: 2, 28 | ..Default::default() 29 | }) 30 | .placeholder("Enter your Markdown here...") 31 | .default_value(EXAMPLE) 32 | }); 33 | let resizable_state = ResizableState::new(cx); 34 | 35 | let _subscribe = cx.subscribe(&input_state, |_, _, _: &InputEvent, cx| { 36 | cx.notify(); 37 | }); 38 | 39 | Self { 40 | resizable_state, 41 | input_state, 42 | } 43 | } 44 | 45 | fn view(window: &mut Window, cx: &mut App) -> Entity { 46 | cx.new(|cx| Self::new(window, cx)) 47 | } 48 | } 49 | 50 | impl Render for Example { 51 | fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { 52 | let theme = if cx.theme().mode.is_dark() { 53 | HighlightTheme::default_dark() 54 | } else { 55 | HighlightTheme::default_light() 56 | }; 57 | 58 | let is_dark = cx.theme().mode.is_dark(); 59 | 60 | h_resizable("container", self.resizable_state.clone()) 61 | .child( 62 | resizable_panel().child( 63 | div() 64 | .id("source") 65 | .size_full() 66 | .font_family("Monaco") 67 | .text_size(px(12.)) 68 | .child(TextInput::new(&self.input_state).h_full().appearance(false)), 69 | ), 70 | ) 71 | .child( 72 | resizable_panel().child( 73 | div() 74 | .id("preview") 75 | .size_full() 76 | .p_5() 77 | .overflow_y_scroll() 78 | .child( 79 | TextView::markdown("preview", self.input_state.read(cx).value()).style( 80 | TextViewStyle { 81 | highlight_theme: Rc::new(theme.clone()), 82 | is_dark, 83 | ..Default::default() 84 | }, 85 | ), 86 | ), 87 | ), 88 | ) 89 | } 90 | } 91 | 92 | fn main() { 93 | let app = Application::new().with_assets(Assets); 94 | 95 | app.run(move |cx| { 96 | story::init(cx); 97 | cx.activate(true); 98 | 99 | story::create_new_window("Markdown Example", Example::view, cx); 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /crates/story/examples/modal.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, ModalStory}; 3 | 4 | pub struct Example { 5 | root: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let root = ModalStory::view(window, cx); 11 | 12 | Self { root } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.root.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Modal Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/popover.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, PopoverStory}; 3 | 4 | pub struct Example { 5 | story: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let story = PopoverStory::view(window, cx); 11 | 12 | Self { story } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.story.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Popover Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/scrollable.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, ScrollableStory}; 3 | 4 | pub struct Example { 5 | story: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let story = ScrollableStory::view(window, cx); 11 | 12 | Self { story } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.story.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Scrollable Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/sidebar.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, SidebarStory}; 3 | 4 | pub struct Example { 5 | root: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let root = SidebarStory::view(window, cx); 11 | 12 | Self { root } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().mt(-px(1.)).size_full().child(self.root.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Sidebar Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/switch.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, SwitchStory}; 3 | 4 | pub struct Example { 5 | story: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let story = SwitchStory::view(window, cx); 11 | 12 | Self { story } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.story.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Switch Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/table.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, TableStory}; 3 | 4 | pub struct Example { 5 | table: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let table = TableStory::view(window, cx); 11 | 12 | Self { table } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.table.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Table Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/tabs.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, TabsStory}; 3 | 4 | pub struct Example { 5 | root: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let root = TabsStory::view(window, cx); 11 | 12 | Self { root } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.root.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Tabs Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/text.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, LabelStory}; 3 | 4 | pub struct Example { 5 | root: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let root = LabelStory::view(window, cx); 11 | 12 | Self { root } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div() 23 | .p_4() 24 | .id("example") 25 | .overflow_y_scroll() 26 | .size_full() 27 | .child(self.root.clone()) 28 | } 29 | } 30 | 31 | fn main() { 32 | let app = Application::new().with_assets(Assets); 33 | 34 | app.run(move |cx| { 35 | story::init(cx); 36 | cx.activate(true); 37 | 38 | story::create_new_window("List Example", Example::view, cx); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /crates/story/examples/toggle.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, ToggleStory}; 3 | 4 | pub struct Example { 5 | root: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let root = ToggleStory::view(window, cx); 11 | 12 | Self { root } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.root.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("Toggle Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/examples/tooltip.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, TooltipStory}; 3 | 4 | pub struct Example { 5 | root: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let root = TooltipStory::view(window, cx); 11 | 12 | Self { root } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div() 23 | .p_4() 24 | .id("example") 25 | .overflow_y_scroll() 26 | .size_full() 27 | .child(self.root.clone()) 28 | } 29 | } 30 | 31 | fn main() { 32 | let app = Application::new().with_assets(Assets); 33 | 34 | app.run(move |cx| { 35 | story::init(cx); 36 | cx.activate(true); 37 | 38 | story::create_new_window("Tooltip Example", Example::view, cx); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /crates/story/examples/webview.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | use story::{Assets, WebViewStory}; 3 | 4 | pub struct Example { 5 | root: Entity, 6 | } 7 | 8 | impl Example { 9 | pub fn new(window: &mut Window, cx: &mut Context) -> Self { 10 | let root = WebViewStory::view(window, cx); 11 | 12 | Self { root } 13 | } 14 | 15 | fn view(window: &mut Window, cx: &mut App) -> Entity { 16 | cx.new(|cx| Self::new(window, cx)) 17 | } 18 | } 19 | 20 | impl Render for Example { 21 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 22 | div().p_4().size_full().child(self.root.clone()) 23 | } 24 | } 25 | 26 | fn main() { 27 | let app = Application::new().with_assets(Assets); 28 | 29 | app.run(move |cx| { 30 | story::init(cx); 31 | cx.activate(true); 32 | 33 | story::create_new_window("WebView Example", Example::view, cx); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /crates/story/src/assets.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | 3 | use gpui::AssetSource; 4 | use rust_embed::RustEmbed; 5 | 6 | #[derive(RustEmbed)] 7 | #[folder = "../../assets"] 8 | #[include = "icons/**/*"] 9 | #[exclude = "*.DS_Store"] 10 | pub struct Assets; 11 | 12 | impl AssetSource for Assets { 13 | fn load(&self, path: &str) -> gpui::Result>> { 14 | Self::get(path) 15 | .map(|f| Some(f.data)) 16 | .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) 17 | } 18 | 19 | fn list(&self, path: &str) -> gpui::Result> { 20 | Ok(Self::iter() 21 | .filter_map(|p| { 22 | if p.starts_with(path) { 23 | Some(p.into()) 24 | } else { 25 | None 26 | } 27 | }) 28 | .collect()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/story/src/badge_story.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | img, App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, 3 | Render, Styled, Window, 4 | }; 5 | use gpui_component::{ 6 | badge::Badge, dock::PanelControl, v_flex, ActiveTheme, Icon, IconName, Sizable as _, 7 | }; 8 | 9 | use crate::section; 10 | 11 | pub struct BadgeStory { 12 | focus_handle: gpui::FocusHandle, 13 | } 14 | 15 | impl BadgeStory { 16 | fn new(_: &mut Window, cx: &mut Context) -> Self { 17 | Self { 18 | focus_handle: cx.focus_handle(), 19 | } 20 | } 21 | 22 | pub fn view(window: &mut Window, cx: &mut App) -> Entity { 23 | cx.new(|cx| Self::new(window, cx)) 24 | } 25 | } 26 | 27 | impl super::Story for BadgeStory { 28 | fn title() -> &'static str { 29 | "Badge" 30 | } 31 | 32 | fn description() -> &'static str { 33 | "A red dot that indicates the number of unread messages." 34 | } 35 | 36 | fn new_view(window: &mut Window, cx: &mut App) -> Entity { 37 | Self::view(window, cx) 38 | } 39 | 40 | fn zoomable() -> Option { 41 | None 42 | } 43 | } 44 | 45 | impl Focusable for BadgeStory { 46 | fn focus_handle(&self, _: &App) -> FocusHandle { 47 | self.focus_handle.clone() 48 | } 49 | } 50 | 51 | impl Render for BadgeStory { 52 | fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { 53 | v_flex() 54 | .gap_4() 55 | .child( 56 | section("Badge on Icon") 57 | .max_w_md() 58 | .child( 59 | Badge::new() 60 | .count(3) 61 | .child(Icon::new(IconName::Bell).large()), 62 | ) 63 | .child( 64 | Badge::new() 65 | .count(103) 66 | .child(Icon::new(IconName::Inbox).large()), 67 | ), 68 | ) 69 | .child( 70 | section("Badge on Avatar") 71 | .max_w_md() 72 | .child( 73 | Badge::new().count(3).child( 74 | img("https://avatars.githubusercontent.com/u/5518?v=4") 75 | .size_10() 76 | .border_1() 77 | .border_color(cx.theme().border) 78 | .rounded_full(), 79 | ), 80 | ) 81 | .child( 82 | Badge::new().count(103).child( 83 | img("https://avatars.githubusercontent.com/u/28998859?v=4") 84 | .size_10() 85 | .border_1() 86 | .border_color(cx.theme().border) 87 | .rounded_full(), 88 | ), 89 | ), 90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/story/src/calendar_story.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement as _, 3 | Render, Styled as _, Window, 4 | }; 5 | use gpui_component::{ 6 | calendar::{Calendar, CalendarState}, 7 | v_flex, 8 | }; 9 | 10 | use crate::section; 11 | 12 | pub struct CalendarStory { 13 | focus_handle: FocusHandle, 14 | calendar: Entity, 15 | calendar_wide: Entity, 16 | } 17 | 18 | impl super::Story for CalendarStory { 19 | fn title() -> &'static str { 20 | "Calendar" 21 | } 22 | 23 | fn description() -> &'static str { 24 | "A calendar to select a date or date range." 25 | } 26 | 27 | fn new_view(window: &mut Window, cx: &mut App) -> Entity { 28 | Self::view(window, cx) 29 | } 30 | } 31 | 32 | impl CalendarStory { 33 | pub fn view(window: &mut Window, cx: &mut App) -> Entity { 34 | cx.new(|cx| Self::new(window, cx)) 35 | } 36 | 37 | fn new(window: &mut Window, cx: &mut Context) -> Self { 38 | let calendar = cx.new(|cx| CalendarState::new(window, cx)); 39 | let calendar_wide = cx.new(|cx| CalendarState::new(window, cx)); 40 | 41 | Self { 42 | calendar, 43 | calendar_wide, 44 | focus_handle: cx.focus_handle(), 45 | } 46 | } 47 | } 48 | 49 | impl Focusable for CalendarStory { 50 | fn focus_handle(&self, _: &App) -> FocusHandle { 51 | self.focus_handle.clone() 52 | } 53 | } 54 | 55 | impl Render for CalendarStory { 56 | fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { 57 | v_flex() 58 | .gap_3() 59 | .child( 60 | section("Normal") 61 | .max_w_md() 62 | .child(Calendar::new(&self.calendar)), 63 | ) 64 | .child( 65 | section("With 3 Months") 66 | .max_w_md() 67 | .child(Calendar::new(&self.calendar_wide).number_of_months(3)), 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/story/src/clipboard_story.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement, Render, SharedString, 3 | Styled, Window, 4 | }; 5 | 6 | use gpui_component::{clipboard::Clipboard, label::Label, link::Link, v_flex, ContextModal}; 7 | 8 | use crate::section; 9 | 10 | pub struct ClipboardStory { 11 | focus_handle: gpui::FocusHandle, 12 | masked: bool, 13 | } 14 | 15 | impl super::Story for ClipboardStory { 16 | fn title() -> &'static str { 17 | "Clipboard" 18 | } 19 | 20 | fn description() -> &'static str { 21 | "A button that helps you copy text or other content to your clipboard." 22 | } 23 | 24 | fn new_view(window: &mut Window, cx: &mut App) -> Entity { 25 | Self::view(window, cx) 26 | } 27 | } 28 | 29 | impl ClipboardStory { 30 | pub(crate) fn new(_: &mut Window, cx: &mut App) -> Self { 31 | Self { 32 | focus_handle: cx.focus_handle(), 33 | masked: false, 34 | } 35 | } 36 | 37 | pub fn view(window: &mut Window, cx: &mut App) -> Entity { 38 | cx.new(|cx| Self::new(window, cx)) 39 | } 40 | } 41 | impl Focusable for ClipboardStory { 42 | fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { 43 | self.focus_handle.clone() 44 | } 45 | } 46 | impl Render for ClipboardStory { 47 | fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { 48 | v_flex().gap_6().child( 49 | section("Copy to Clipboard") 50 | .max_w_md() 51 | .child( 52 | Clipboard::new("clipboard1") 53 | .content(|_, _| Label::new("Click icon to copy")) 54 | .value_fn({ 55 | let view = cx.entity().clone(); 56 | move |_, cx| { 57 | SharedString::from(format!("masked :{}", view.read(cx).masked)) 58 | } 59 | }) 60 | .on_copied(|value, window, cx| { 61 | window.push_notification(format!("Copied value: {}", value), cx) 62 | }), 63 | ) 64 | .child( 65 | Clipboard::new("clipboard2") 66 | .content(|_, _| { 67 | Link::new("link1") 68 | .href("https://github.com") 69 | .child("GitHub") 70 | }) 71 | .value("https://github.com") 72 | .on_copied(|value, window, cx| { 73 | window.push_notification(format!("Copied value: {}", value), cx) 74 | }), 75 | ), 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/story/src/color_picker_story.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | prelude::FluentBuilder as _, App, AppContext, Context, Entity, Focusable, Hsla, IntoElement, 3 | ParentElement as _, Render, Styled as _, Subscription, Window, 4 | }; 5 | use gpui_component::{ 6 | blue_500, 7 | color_picker::{ColorPicker, ColorPickerEvent, ColorPickerState}, 8 | green_500, red_500, v_flex, yellow_500, Colorize, 9 | }; 10 | 11 | use crate::section; 12 | 13 | pub struct ColorPickerStory { 14 | color: Entity, 15 | selected_color: Option, 16 | 17 | _subscriptions: Vec, 18 | } 19 | 20 | impl super::Story for ColorPickerStory { 21 | fn title() -> &'static str { 22 | "ColorPicker" 23 | } 24 | 25 | fn description() -> &'static str { 26 | "A color picker to select color." 27 | } 28 | 29 | fn new_view(window: &mut Window, cx: &mut App) -> Entity { 30 | Self::view(window, cx) 31 | } 32 | } 33 | 34 | impl ColorPickerStory { 35 | pub fn view(window: &mut Window, cx: &mut App) -> Entity { 36 | cx.new(|cx| Self::new(window, cx)) 37 | } 38 | 39 | fn new(window: &mut Window, cx: &mut Context) -> Self { 40 | let color = cx.new(|cx| ColorPickerState::new(window, cx).default_value(red_500())); 41 | 42 | let _subscriptions = vec![cx.subscribe(&color, |this, _, ev, _| match ev { 43 | ColorPickerEvent::Change(color) => { 44 | this.selected_color = *color; 45 | println!("Color changed to: {:?}", color); 46 | } 47 | })]; 48 | 49 | Self { 50 | color, 51 | selected_color: Some(red_500()), 52 | _subscriptions, 53 | } 54 | } 55 | } 56 | 57 | impl Focusable for ColorPickerStory { 58 | fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { 59 | self.color.read(cx).focus_handle(cx) 60 | } 61 | } 62 | 63 | impl Render for ColorPickerStory { 64 | fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { 65 | v_flex().gap_3().child( 66 | section("Normal") 67 | .max_w_md() 68 | .child(ColorPicker::new(&self.color).featured_colors(vec![ 69 | red_500(), 70 | blue_500(), 71 | green_500(), 72 | yellow_500(), 73 | ])) 74 | .when_some(self.selected_color, |this, color| { 75 | this.child(color.to_hex()) 76 | }), 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /crates/story/src/fixtures/color-wheel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 17 | 23 | 29 | 35 | 41 | 47 | 52 | 53 | -------------------------------------------------------------------------------- /crates/story/src/fixtures/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/story/src/icon_story.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, 3 | Styled, Window, 4 | }; 5 | use gpui_component::{ 6 | button::{Button, ButtonVariant, ButtonVariants}, 7 | dock::PanelControl, 8 | gray_500, green_500, h_flex, red_500, v_flex, Icon, IconName, 9 | }; 10 | 11 | use crate::section; 12 | 13 | pub struct IconStory { 14 | focus_handle: gpui::FocusHandle, 15 | } 16 | 17 | impl IconStory { 18 | fn new(_: &mut Window, cx: &mut Context) -> Self { 19 | Self { 20 | focus_handle: cx.focus_handle(), 21 | } 22 | } 23 | 24 | pub fn view(window: &mut Window, cx: &mut App) -> Entity { 25 | cx.new(|cx| Self::new(window, cx)) 26 | } 27 | } 28 | 29 | impl super::Story for IconStory { 30 | fn title() -> &'static str { 31 | "Icon" 32 | } 33 | 34 | fn description() -> &'static str { 35 | "SVG Icons based on Lucide.dev" 36 | } 37 | 38 | fn new_view(window: &mut Window, cx: &mut App) -> Entity { 39 | Self::view(window, cx) 40 | } 41 | 42 | fn zoomable() -> Option { 43 | None 44 | } 45 | } 46 | 47 | impl Focusable for IconStory { 48 | fn focus_handle(&self, _: &App) -> FocusHandle { 49 | self.focus_handle.clone() 50 | } 51 | } 52 | 53 | impl Render for IconStory { 54 | fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { 55 | v_flex() 56 | .gap_4() 57 | .child( 58 | section("Icon") 59 | .text_lg() 60 | .child(IconName::Info) 61 | .child(IconName::Map) 62 | .child(IconName::Bot) 63 | .child(IconName::GitHub) 64 | .child(IconName::Calendar) 65 | .child(IconName::Globe) 66 | .child(IconName::Heart), 67 | ) 68 | .child( 69 | section("Color Icon") 70 | .child( 71 | Icon::new(IconName::Maximize) 72 | .size_6() 73 | .text_color(green_500()), 74 | ) 75 | .child(Icon::new(IconName::Minimize).size_6().text_color(red_500())), 76 | ) 77 | .child( 78 | section("Icon Button").child( 79 | h_flex() 80 | .gap_4() 81 | .child( 82 | Button::new("like1") 83 | .icon(Icon::new(IconName::Heart).text_color(gray_500()).size_6()) 84 | .with_variant(ButtonVariant::Ghost), 85 | ) 86 | .child( 87 | Button::new("like2") 88 | .icon(Icon::new(IconName::HeartOff).text_color(red_500()).size_6()) 89 | .with_variant(ButtonVariant::Ghost), 90 | ) 91 | .child( 92 | Button::new("like3") 93 | .icon(Icon::new(IconName::Heart).text_color(green_500()).size_6()) 94 | .with_variant(ButtonVariant::Ghost), 95 | ), 96 | ), 97 | ) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /crates/story/src/image_story.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | img, App, AppContext, ClickEvent, ElementId, Entity, FocusHandle, Focusable, 3 | ParentElement as _, Render, Styled, Window, 4 | }; 5 | use gpui_component::{button::Button, dock::PanelControl, v_flex, SvgImg}; 6 | 7 | use crate::section; 8 | 9 | const SVG_ITEMS: &[&str] = &[ 10 | include_str!("./fixtures/google.svg"), 11 | include_str!("./fixtures/color-wheel.svg"), 12 | ]; 13 | 14 | pub struct ImageStory { 15 | svg_index: usize, 16 | focus_handle: gpui::FocusHandle, 17 | } 18 | 19 | impl super::Story for ImageStory { 20 | fn title() -> &'static str { 21 | "Image" 22 | } 23 | 24 | fn description() -> &'static str { 25 | "Image and SVG image supported." 26 | } 27 | 28 | fn new_view(window: &mut Window, cx: &mut App) -> Entity { 29 | Self::view(window, cx) 30 | } 31 | 32 | fn zoomable() -> Option { 33 | Some(PanelControl::Toolbar) 34 | } 35 | } 36 | 37 | impl ImageStory { 38 | pub fn new(_: &mut Window, cx: &mut App) -> Self { 39 | Self { 40 | svg_index: 0, 41 | focus_handle: cx.focus_handle(), 42 | } 43 | } 44 | 45 | pub fn view(window: &mut Window, cx: &mut App) -> Entity { 46 | cx.new(|cx| Self::new(window, cx)) 47 | } 48 | 49 | fn svg_img(&self, id: impl Into) -> SvgImg { 50 | SvgImg::new(id, SVG_ITEMS[self.svg_index].as_bytes()) 51 | } 52 | } 53 | 54 | impl Focusable for ImageStory { 55 | fn focus_handle(&self, _: &App) -> FocusHandle { 56 | self.focus_handle.clone() 57 | } 58 | } 59 | 60 | impl Render for ImageStory { 61 | fn render( 62 | &mut self, 63 | _window: &mut gpui::Window, 64 | cx: &mut gpui::Context, 65 | ) -> impl gpui::IntoElement { 66 | v_flex() 67 | .gap_4() 68 | .size_full() 69 | .child( 70 | Button::new("switch") 71 | .label("Switch SVG") 72 | .on_click(cx.listener(|this, _: &ClickEvent, _, cx| { 73 | this.svg_index += 1; 74 | if this.svg_index >= SVG_ITEMS.len() { 75 | this.svg_index = 0; 76 | } 77 | cx.notify(); 78 | })), 79 | ) 80 | .child(section("SVG 160px").child(self.svg_img("logo1").size_40().flex_grow())) 81 | .child(section("SVG 80px").child(self.svg_img("logo3").size_20().flex_grow())) 82 | .child(section("SVG 48px").child(self.svg_img("logo4").size_12().flex_grow())) 83 | .child( 84 | section("SVG from img 40px").child( 85 | img("https://pub.lbkrs.com/files/202503/vEnnmgUM6bo362ya/sdk.svg").h_24(), 86 | ), 87 | ) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/story/src/kbd_story.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | App, AppContext, Context, Entity, Focusable, IntoElement, Keystroke, ParentElement, Render, 3 | Styled, Window, 4 | }; 5 | 6 | use gpui_component::{h_flex, v_flex, Kbd}; 7 | 8 | use crate::section; 9 | 10 | pub struct KbdStory { 11 | focus_handle: gpui::FocusHandle, 12 | } 13 | 14 | impl super::Story for KbdStory { 15 | fn title() -> &'static str { 16 | "Kbd" 17 | } 18 | 19 | fn description() -> &'static str { 20 | "A tag style to display keyboard shortcuts" 21 | } 22 | 23 | fn new_view(window: &mut Window, cx: &mut App) -> Entity { 24 | Self::view(window, cx) 25 | } 26 | } 27 | 28 | impl KbdStory { 29 | pub(crate) fn new(_: &mut Window, cx: &mut App) -> Self { 30 | Self { 31 | focus_handle: cx.focus_handle(), 32 | } 33 | } 34 | 35 | pub fn view(window: &mut Window, cx: &mut App) -> Entity { 36 | cx.new(|cx| Self::new(window, cx)) 37 | } 38 | } 39 | impl Focusable for KbdStory { 40 | fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { 41 | self.focus_handle.clone() 42 | } 43 | } 44 | impl Render for KbdStory { 45 | fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { 46 | v_flex().gap_6().child( 47 | section("Kbd").child( 48 | h_flex() 49 | .gap_2() 50 | .child(Kbd::new(Keystroke::parse("cmd-shift-p").unwrap())) 51 | .child(Kbd::new(Keystroke::parse("cmd-ctrl-t").unwrap())) 52 | .child(Kbd::new(Keystroke::parse("escape").unwrap())) 53 | .child(Kbd::new(Keystroke::parse("backspace").unwrap())) 54 | .child(Kbd::new(Keystroke::parse("/").unwrap())) 55 | .child(Kbd::new(Keystroke::parse("enter").unwrap())), 56 | ), 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/story/src/welcome_story.rs: -------------------------------------------------------------------------------- 1 | use gpui::{App, AppContext, Context, Entity, Focusable, ParentElement, Render, Styled, Window}; 2 | 3 | use gpui_component::{dock::PanelControl, text::TextView, v_flex}; 4 | 5 | use crate::Story; 6 | 7 | pub struct WelcomeStory { 8 | focus_handle: gpui::FocusHandle, 9 | } 10 | 11 | impl WelcomeStory { 12 | pub fn view(window: &mut Window, cx: &mut App) -> Entity { 13 | cx.new(|cx| Self::new(window, cx)) 14 | } 15 | 16 | fn new(_: &mut Window, cx: &mut Context) -> Self { 17 | Self { 18 | focus_handle: cx.focus_handle(), 19 | } 20 | } 21 | } 22 | 23 | impl Story for WelcomeStory { 24 | fn title() -> &'static str { 25 | "Introduction" 26 | } 27 | 28 | fn description() -> &'static str { 29 | "UI components for building fantastic desktop application by using GPUI." 30 | } 31 | 32 | fn new_view(window: &mut Window, cx: &mut App) -> Entity { 33 | Self::view(window, cx) 34 | } 35 | 36 | fn zoomable() -> Option { 37 | None 38 | } 39 | } 40 | 41 | impl Focusable for WelcomeStory { 42 | fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { 43 | self.focus_handle.clone() 44 | } 45 | } 46 | 47 | impl Render for WelcomeStory { 48 | fn render( 49 | &mut self, 50 | _: &mut gpui::Window, 51 | _cx: &mut gpui::Context, 52 | ) -> impl gpui::IntoElement { 53 | v_flex().p_4().gap_5().child(TextView::markdown( 54 | "intro", 55 | include_str!("../../../README.md"), 56 | )) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "UI components for building fantastic desktop application by using GPUI." 3 | edition = "2021" 4 | homepage = "https://github.com/longbridge/gpui-component" 5 | keywords = ["GPUI", "application", "desktop", "ui"] 6 | license-file = "LICENSE-APACHE" 7 | name = "gpui-component" 8 | publish = true 9 | version = "0.1.0" 10 | 11 | [lib] 12 | doctest = false 13 | 14 | [features] 15 | decimal = ["dep:rust_decimal"] 16 | inspector = [] 17 | webview = ["dep:wry"] 18 | 19 | [dependencies] 20 | gpui.workspace = true 21 | gpui-component-macros.workspace = true 22 | 23 | anyhow = "1" 24 | enum-iterator = "2.1.0" 25 | futures-util = "0.3.31" 26 | image = "0.25.1" 27 | itertools = "0.13.0" 28 | lyon = "1.0" 29 | once_cell = "1.19.0" 30 | paste = "1" 31 | regex = "1" 32 | resvg = { version = "0.45.0", default-features = false, features = ["text"] } 33 | rust-i18n = "3" 34 | schemars = "0.8.22" 35 | serde = "1.0.203" 36 | serde_json = "1" 37 | smallvec = "1.13.2" 38 | smol = "1" 39 | tracing = "0.1.41" 40 | unicode-segmentation = "1.12.0" 41 | usvg = { version = "0.45.0", default-features = false, features = ["text"] } 42 | uuid = "1.10" 43 | wry = { version = "0.48.0", optional = true } 44 | 45 | # Chart 46 | num-traits = "0.2" 47 | rust_decimal = { version = "1.37.0", optional = true } 48 | 49 | # Markdown Parser 50 | markdown = "1.0.0-alpha.22" 51 | 52 | # HTML Parser 53 | html5ever = "0.27" 54 | markup5ever_rcdom = "0.3.0" 55 | minify-html = "0.15.0" 56 | 57 | # Calendar 58 | chrono = "0.4.38" 59 | 60 | # Code Editor 61 | tree-sitter = "0.25.4" 62 | tree-sitter-bash = "0.23.3" 63 | tree-sitter-c = "0.24.1" 64 | tree-sitter-c-sharp = "0.23.1" 65 | tree-sitter-cmake = "0.7.1" 66 | tree-sitter-cpp = "0.23.4" 67 | tree-sitter-css = "0.23.2" 68 | tree-sitter-diff = "0.1.0" 69 | tree-sitter-elixir = "0.3" 70 | tree-sitter-embedded-template = "0.23.0" 71 | tree-sitter-go = "0.23.4" 72 | tree-sitter-graphql = "0.1.0" 73 | tree-sitter-highlight = "0.25.4" 74 | tree-sitter-html = "0.23.2" 75 | tree-sitter-java = "0.23.5" 76 | tree-sitter-javascript = "0.23.1" 77 | tree-sitter-json = "0.24.8" 78 | tree-sitter-make = "1.1.1" 79 | tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", tag = "v0.5.0" } 80 | tree-sitter-proto = "0.2.0" 81 | tree-sitter-python = "0.23.6" 82 | tree-sitter-ruby = "0.23.1" 83 | tree-sitter-rust = "0.24.0" 84 | tree-sitter-scala = "0.23.4" 85 | tree-sitter-sequel = "0.3.8" 86 | tree-sitter-swift = "0.7.0" 87 | tree-sitter-toml-ng = "0.7.0" 88 | tree-sitter-typescript = "0.23.2" 89 | tree-sitter-yaml = "0.7.1" 90 | tree-sitter-zig = "1.1.2" 91 | 92 | [dev-dependencies] 93 | indoc = "2" 94 | 95 | [lints] 96 | workspace = true 97 | -------------------------------------------------------------------------------- /crates/ui/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../../LICENSE-APACHE -------------------------------------------------------------------------------- /crates/ui/locales/ui.yml: -------------------------------------------------------------------------------- 1 | _version: 2 2 | Calendar: 3 | week.0: 4 | en: Su 5 | zh-CN: 日 6 | zh-HK: 日 7 | it: Do 8 | week.1: 9 | en: Mo 10 | zh-CN: 一 11 | zh-HK: 一 12 | it: Lu 13 | week.2: 14 | en: Tu 15 | zh-CN: 二 16 | zh-HK: 二 17 | it: Ma 18 | week.3: 19 | en: We 20 | zh-CN: 三 21 | zh-HK: 三 22 | it: Me 23 | week.4: 24 | en: Th 25 | zh-CN: 四 26 | zh-HK: 四 27 | it: Gi 28 | week.5: 29 | en: Fr 30 | zh-CN: 五 31 | zh-HK: 五 32 | it: Ve 33 | week.6: 34 | en: Sa 35 | zh-CN: 六 36 | zh-HK: 六 37 | it: Sa 38 | month.January: 39 | en: January 40 | zh-CN: 一月 41 | zh-HK: 一月 42 | it: Gennaio 43 | month.February: 44 | en: February 45 | zh-CN: 二月 46 | zh-HK: 二月 47 | it: Febbraio 48 | month.March: 49 | en: March 50 | zh-CN: 三月 51 | zh-HK: 三月 52 | it: Marzo 53 | month.April: 54 | en: April 55 | zh-CN: 四月 56 | zh-HK: 四月 57 | it: Aprile 58 | month.May: 59 | en: May 60 | zh-CN: 五月 61 | zh-HK: 五月 62 | it: Maggio 63 | month.June: 64 | en: June 65 | zh-CN: 六月 66 | zh-HK: 六月 67 | it: Giugno 68 | month.July: 69 | en: July 70 | zh-CN: 七月 71 | zh-HK: 七月 72 | it: Luglio 73 | month.August: 74 | en: August 75 | zh-CN: 八月 76 | zh-HK: 八月 77 | it: Agosto 78 | month.September: 79 | en: September 80 | zh-CN: 九月 81 | zh-HK: 九月 82 | it: Settembre 83 | month.October: 84 | en: October 85 | zh-CN: 十月 86 | zh-HK: 十月 87 | it: Ottobre 88 | month.November: 89 | en: November 90 | zh-CN: 十一月 91 | zh-HK: 十一月 92 | it: Novembre 93 | month.December: 94 | en: December 95 | zh-CN: 十二月 96 | zh-HK: 十二月 97 | it: Dicembre 98 | DatePicker: 99 | placeholder: 100 | en: "Select date" 101 | zh-CN: 选择日期 102 | zh-HK: 選擇日期 103 | it: "Seleziona data" 104 | Dropdown: 105 | placeholder: 106 | en: "Please select" 107 | zh-CN: "请选择" 108 | zh-HK: "請選擇" 109 | it: Seleziona 110 | Dock: 111 | Unnamed: 112 | en: Unnamed 113 | zh-CN: 未命名 114 | zh-HK: 未命名 115 | it: "Senza nome" 116 | Close: 117 | en: Close 118 | zh-CN: 关闭 119 | zh-HK: 關閉 120 | it: Chiudi 121 | Zoom In: 122 | en: Zoom In 123 | zh-CN: 放大 124 | zh-HK: 放大 125 | it: Zoom In 126 | Zoom Out: 127 | en: Zoom Out 128 | zh-CN: 缩小 129 | zh-HK: 縮小 130 | it: Zoom Out 131 | Collapse: 132 | en: Collapse 133 | zh-CN: 隐藏 134 | zh-HK: 隱藏 135 | it: Nascondi 136 | Expand: 137 | en: Expand 138 | zh-CN: 展开 139 | zh-HK: 展開 140 | it: Espandi 141 | Modal: 142 | ok: 143 | en: OK 144 | zh-CN: 确定 145 | zh-HK: 確定 146 | it: OK 147 | cancel: 148 | en: Cancel 149 | zh-CN: 取消 150 | zh-HK: 取消 151 | it: Annulla 152 | List: 153 | search_placeholder: 154 | en: Search... 155 | zh-CN: 搜索... 156 | zh-HK: 搜索... 157 | it: Ricerca... 158 | -------------------------------------------------------------------------------- /crates/ui/src/actions.rs: -------------------------------------------------------------------------------- 1 | use gpui::{actions, impl_internal_actions}; 2 | use serde::Deserialize; 3 | 4 | #[derive(Clone, PartialEq, Eq, Deserialize)] 5 | pub struct Confirm { 6 | /// Is confirm with secondary. 7 | pub secondary: bool, 8 | } 9 | 10 | actions!(list, [Cancel, SelectPrev, SelectNext]); 11 | impl_internal_actions!(list, [Confirm]); 12 | -------------------------------------------------------------------------------- /crates/ui/src/animation.rs: -------------------------------------------------------------------------------- 1 | /// A cubic bezier function like CSS `cubic-bezier`. 2 | /// 3 | /// Builder: 4 | /// 5 | /// https://cubic-bezier.com 6 | pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> impl Fn(f32) -> f32 { 7 | move |t: f32| { 8 | let one_t = 1.0 - t; 9 | let one_t2 = one_t * one_t; 10 | let t2 = t * t; 11 | let t3 = t2 * t; 12 | 13 | // The Bezier curve function for x and y, where x0 = 0, y0 = 0, x3 = 1, y3 = 1 14 | let _x = 3.0 * x1 * one_t2 * t + 3.0 * x2 * one_t * t2 + t3; 15 | let y = 3.0 * y1 * one_t2 * t + 3.0 * y2 * one_t * t2 + t3; 16 | 17 | y 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/ui/src/badge.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | div, prelude::FluentBuilder, px, relative, AnyElement, App, Div, IntoElement, ParentElement, 3 | RenderOnce, Styled, Window, 4 | }; 5 | 6 | use crate::{h_flex, red_500, white}; 7 | 8 | #[derive(Default)] 9 | enum BadgeStyle { 10 | Dot, 11 | #[default] 12 | Number, 13 | } 14 | 15 | #[derive(IntoElement)] 16 | pub struct Badge { 17 | base: Div, 18 | count: usize, 19 | max: usize, 20 | style: BadgeStyle, 21 | } 22 | 23 | impl Badge { 24 | pub fn new() -> Self { 25 | Self { 26 | base: div(), 27 | count: 0, 28 | max: 99, 29 | style: Default::default(), 30 | } 31 | } 32 | 33 | pub fn dot(mut self) -> Self { 34 | self.style = BadgeStyle::Dot; 35 | self 36 | } 37 | 38 | pub fn count(mut self, count: usize) -> Self { 39 | self.count = count; 40 | self 41 | } 42 | 43 | pub fn max(mut self, max: usize) -> Self { 44 | self.max = max; 45 | self 46 | } 47 | } 48 | 49 | impl ParentElement for Badge { 50 | fn extend(&mut self, elements: impl IntoIterator) { 51 | self.base.extend(elements); 52 | } 53 | } 54 | 55 | impl RenderOnce for Badge { 56 | fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { 57 | self.base.relative().when(self.count > 0, |this| { 58 | this.child( 59 | h_flex() 60 | .absolute() 61 | .justify_center() 62 | .rounded_full() 63 | .bg(red_500()) 64 | .map(|this| match self.style { 65 | BadgeStyle::Dot => this.top(px(0.)).right(px(0.)).size(px(6.)), 66 | BadgeStyle::Number => { 67 | let count = if self.count > self.max { 68 | format!("{}+", self.max) 69 | } else { 70 | self.count.to_string() 71 | }; 72 | 73 | this.top(px(-3.)) 74 | .right(-px(3. * count.len() as f32)) 75 | .py_0p5() 76 | .px_0p5() 77 | .min_w_3p5() 78 | .text_color(white()) 79 | .text_size(px(10.)) 80 | .line_height(relative(1.)) 81 | .child(count) 82 | } 83 | }), 84 | ) 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/ui/src/breadcrumb.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use gpui::{ 4 | div, prelude::FluentBuilder as _, App, ClickEvent, ElementId, InteractiveElement as _, 5 | IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, 6 | Window, 7 | }; 8 | 9 | use crate::{h_flex, ActiveTheme, Icon, IconName}; 10 | 11 | #[derive(IntoElement)] 12 | pub struct Breadcrumb { 13 | items: Vec, 14 | } 15 | 16 | #[derive(IntoElement)] 17 | pub struct BreadcrumbItem { 18 | id: ElementId, 19 | text: SharedString, 20 | on_click: Option>, 21 | disabled: bool, 22 | is_last: bool, 23 | } 24 | 25 | impl BreadcrumbItem { 26 | pub fn new(id: impl Into, text: impl Into) -> Self { 27 | Self { 28 | id: id.into(), 29 | text: text.into(), 30 | on_click: None, 31 | disabled: false, 32 | is_last: false, 33 | } 34 | } 35 | 36 | pub fn disabled(mut self, disabled: bool) -> Self { 37 | self.disabled = disabled; 38 | self 39 | } 40 | 41 | pub fn on_click( 42 | mut self, 43 | on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, 44 | ) -> Self { 45 | self.on_click = Some(Rc::new(on_click)); 46 | self 47 | } 48 | 49 | /// For internal use only. 50 | fn is_last(mut self, is_last: bool) -> Self { 51 | self.is_last = is_last; 52 | self 53 | } 54 | } 55 | 56 | impl RenderOnce for BreadcrumbItem { 57 | fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { 58 | div() 59 | .id(self.id) 60 | .child(self.text) 61 | .text_color(cx.theme().muted_foreground) 62 | .when(self.is_last, |this| this.text_color(cx.theme().foreground)) 63 | .when(self.disabled, |this| { 64 | this.text_color(cx.theme().muted_foreground) 65 | }) 66 | .when(!self.disabled, |this| { 67 | this.when_some(self.on_click, |this, on_click| { 68 | this.cursor_pointer().on_click(move |event, window, cx| { 69 | on_click(event, window, cx); 70 | }) 71 | }) 72 | }) 73 | } 74 | } 75 | 76 | impl Breadcrumb { 77 | pub fn new() -> Self { 78 | Self { items: Vec::new() } 79 | } 80 | 81 | /// Add an item to the breadcrumb. 82 | pub fn item(mut self, item: BreadcrumbItem) -> Self { 83 | self.items.push(item); 84 | self 85 | } 86 | } 87 | 88 | #[derive(IntoElement)] 89 | struct BreadcrumbSeparator; 90 | impl RenderOnce for BreadcrumbSeparator { 91 | fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { 92 | Icon::new(IconName::ChevronRight) 93 | .text_color(cx.theme().muted_foreground) 94 | .size_3p5() 95 | .into_any_element() 96 | } 97 | } 98 | 99 | impl RenderOnce for Breadcrumb { 100 | fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { 101 | let items_count = self.items.len(); 102 | 103 | let mut children = vec![]; 104 | for (ix, item) in self.items.into_iter().enumerate() { 105 | let is_last = ix == items_count - 1; 106 | 107 | children.push(item.is_last(is_last).into_any_element()); 108 | if !is_last { 109 | children.push(BreadcrumbSeparator.into_any_element()); 110 | } 111 | } 112 | 113 | h_flex() 114 | .gap_1p5() 115 | .text_sm() 116 | .text_color(cx.theme().muted_foreground) 117 | .children(children) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /crates/ui/src/button/mod.rs: -------------------------------------------------------------------------------- 1 | mod button; 2 | mod button_group; 3 | mod dropdown_button; 4 | mod toggle; 5 | 6 | pub use button::*; 7 | pub use button_group::*; 8 | pub use dropdown_button::*; 9 | pub use toggle::*; 10 | -------------------------------------------------------------------------------- /crates/ui/src/chart/mod.rs: -------------------------------------------------------------------------------- 1 | mod area_chart; 2 | mod bar_chart; 3 | mod line_chart; 4 | mod pie_chart; 5 | 6 | pub use area_chart::AreaChart; 7 | pub use bar_chart::BarChart; 8 | pub use line_chart::LineChart; 9 | pub use pie_chart::PieChart; 10 | -------------------------------------------------------------------------------- /crates/ui/src/chart/pie_chart.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use gpui::{App, Bounds, Hsla, Pixels, Window}; 4 | use gpui_component_macros::IntoPlot; 5 | use num_traits::Zero; 6 | 7 | use crate::{ 8 | plot::{ 9 | shape::{Arc, Pie}, 10 | Plot, 11 | }, 12 | ActiveTheme, 13 | }; 14 | 15 | #[derive(IntoPlot)] 16 | pub struct PieChart { 17 | data: Vec, 18 | inner_radius: f64, 19 | outer_radius: f64, 20 | pad_angle: f64, 21 | value: Option f64>>, 22 | color: Option Hsla>>, 23 | } 24 | 25 | impl PieChart { 26 | pub fn new(data: I) -> Self 27 | where 28 | I: IntoIterator, 29 | { 30 | Self { 31 | data: data.into_iter().collect(), 32 | inner_radius: 0., 33 | outer_radius: 0., 34 | pad_angle: 0., 35 | value: None, 36 | color: None, 37 | } 38 | } 39 | 40 | pub fn inner_radius(mut self, inner_radius: f64) -> Self { 41 | self.inner_radius = inner_radius; 42 | self 43 | } 44 | 45 | pub fn outer_radius(mut self, outer_radius: f64) -> Self { 46 | self.outer_radius = outer_radius; 47 | self 48 | } 49 | 50 | pub fn pad_angle(mut self, pad_angle: f64) -> Self { 51 | self.pad_angle = pad_angle; 52 | self 53 | } 54 | 55 | pub fn value(mut self, value: impl Fn(&T) -> f64 + 'static) -> Self { 56 | self.value = Some(Rc::new(value)); 57 | self 58 | } 59 | 60 | pub fn color(mut self, color: impl Fn(&T) -> H + 'static) -> Self 61 | where 62 | H: Into + 'static, 63 | { 64 | self.color = Some(Rc::new(move |t| color(t).into())); 65 | self 66 | } 67 | } 68 | 69 | impl Plot for PieChart { 70 | fn paint(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { 71 | let Some(value_fn) = self.value.as_ref() else { 72 | return; 73 | }; 74 | 75 | let outer_radius = if self.outer_radius.is_zero() { 76 | bounds.size.height.to_f64() * 0.4 77 | } else { 78 | self.outer_radius 79 | }; 80 | 81 | let arc = Arc::new() 82 | .inner_radius(self.inner_radius) 83 | .outer_radius(outer_radius); 84 | let value_fn = value_fn.clone(); 85 | let mut pie = Pie::::new().value(move |d| Some(value_fn(d))); 86 | pie = pie.pad_angle(self.pad_angle); 87 | let arcs = pie.arcs(&self.data); 88 | 89 | for a in &arcs { 90 | arc.paint( 91 | a, 92 | if let Some(color_fn) = self.color.as_ref() { 93 | color_fn(a.data) 94 | } else { 95 | cx.theme().chart_2 96 | }, 97 | &bounds, 98 | window, 99 | ); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/ui/src/divider.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | div, prelude::FluentBuilder as _, px, Axis, Div, Hsla, IntoElement, ParentElement, RenderOnce, 3 | SharedString, Styled, 4 | }; 5 | 6 | use crate::ActiveTheme; 7 | 8 | /// A divider that can be either vertical or horizontal. 9 | #[derive(IntoElement)] 10 | pub struct Divider { 11 | base: Div, 12 | label: Option, 13 | axis: Axis, 14 | color: Option, 15 | } 16 | 17 | impl Divider { 18 | pub fn vertical() -> Self { 19 | Self { 20 | base: div().h_full(), 21 | axis: Axis::Vertical, 22 | label: None, 23 | color: None, 24 | } 25 | } 26 | 27 | pub fn horizontal() -> Self { 28 | Self { 29 | base: div().w_full(), 30 | axis: Axis::Horizontal, 31 | label: None, 32 | color: None, 33 | } 34 | } 35 | 36 | pub fn label(mut self, label: impl Into) -> Self { 37 | self.label = Some(label.into()); 38 | self 39 | } 40 | 41 | pub fn color(mut self, color: impl Into) -> Self { 42 | self.color = Some(color.into()); 43 | self 44 | } 45 | } 46 | 47 | impl Styled for Divider { 48 | fn style(&mut self) -> &mut gpui::StyleRefinement { 49 | self.base.style() 50 | } 51 | } 52 | 53 | impl RenderOnce for Divider { 54 | fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement { 55 | let theme = cx.theme(); 56 | 57 | self.base 58 | .flex() 59 | .flex_shrink_0() 60 | .items_center() 61 | .justify_center() 62 | .child( 63 | div() 64 | .absolute() 65 | .map(|this| match self.axis { 66 | Axis::Vertical => this.w(px(1.)).h_full(), 67 | Axis::Horizontal => this.h(px(1.)).w_full(), 68 | }) 69 | .bg(self.color.unwrap_or(cx.theme().border)), 70 | ) 71 | .when_some(self.label, |this, label| { 72 | this.child( 73 | div() 74 | .px_2() 75 | .py_1() 76 | .mx_auto() 77 | .text_xs() 78 | .bg(cx.theme().background) 79 | .text_color(theme.muted_foreground) 80 | .child(label), 81 | ) 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/ui/src/dock/invalid_panel.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | App, EventEmitter, FocusHandle, Focusable, ParentElement as _, Render, SharedString, 3 | Styled as _, Window, 4 | }; 5 | 6 | use crate::ActiveTheme as _; 7 | 8 | use super::{Panel, PanelEvent, PanelState}; 9 | 10 | pub(crate) struct InvalidPanel { 11 | name: SharedString, 12 | focus_handle: FocusHandle, 13 | old_state: PanelState, 14 | } 15 | 16 | impl InvalidPanel { 17 | pub(crate) fn new(name: &str, state: PanelState, _: &mut Window, cx: &mut App) -> Self { 18 | Self { 19 | focus_handle: cx.focus_handle(), 20 | name: SharedString::from(name.to_owned()), 21 | old_state: state, 22 | } 23 | } 24 | } 25 | impl Panel for InvalidPanel { 26 | fn panel_name(&self) -> &'static str { 27 | "InvalidPanel" 28 | } 29 | 30 | fn dump(&self, _cx: &App) -> super::PanelState { 31 | self.old_state.clone() 32 | } 33 | } 34 | impl EventEmitter for InvalidPanel {} 35 | impl Focusable for InvalidPanel { 36 | fn focus_handle(&self, _: &App) -> FocusHandle { 37 | self.focus_handle.clone() 38 | } 39 | } 40 | impl Render for InvalidPanel { 41 | fn render( 42 | &mut self, 43 | _: &mut gpui::Window, 44 | cx: &mut gpui::Context, 45 | ) -> impl gpui::IntoElement { 46 | gpui::div() 47 | .size_full() 48 | .my_6() 49 | .flex() 50 | .flex_col() 51 | .items_center() 52 | .justify_center() 53 | .text_color(cx.theme().muted_foreground) 54 | .child(format!( 55 | "The `{}` panel type is not registered in PanelRegistry.", 56 | self.name.clone() 57 | )) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/ui/src/event.rs: -------------------------------------------------------------------------------- 1 | use gpui::{App, ClickEvent, FocusableWrapper, InteractiveElement, Stateful, Window}; 2 | 3 | pub trait InteractiveElementExt: InteractiveElement { 4 | /// Set the listener for a double click event. 5 | fn on_double_click( 6 | mut self, 7 | listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, 8 | ) -> Self 9 | where 10 | Self: Sized, 11 | { 12 | self.interactivity().on_click(move |event, window, cx| { 13 | if event.up.click_count == 2 { 14 | listener(event, window, cx); 15 | } 16 | }); 17 | self 18 | } 19 | } 20 | 21 | impl InteractiveElementExt for FocusableWrapper {} 22 | impl InteractiveElementExt for Stateful {} 23 | -------------------------------------------------------------------------------- /crates/ui/src/focusable.rs: -------------------------------------------------------------------------------- 1 | use gpui::{App, FocusHandle, Window}; 2 | 3 | /// A trait for views that can cycle focus between its children. 4 | /// 5 | /// This will provide a default implementation for the `cycle_focus` method that will cycle focus. 6 | /// 7 | /// You should implement the `cycle_focus_handles` method to return a list of focus handles that 8 | /// should be cycled, and the cycle will follow the order of the list. 9 | pub trait FocusableCycle { 10 | /// Returns a list of focus handles that should be cycled. 11 | fn cycle_focus_handles(&self, window: &mut Window, cx: &mut App) -> Vec 12 | where 13 | Self: Sized; 14 | 15 | /// Cycles focus between the focus handles returned by `cycle_focus_handles`. 16 | /// If `is_next` is `true`, it will cycle to the next focus handle, otherwise it will cycle to prev. 17 | fn cycle_focus(&self, is_next: bool, window: &mut Window, cx: &mut App) 18 | where 19 | Self: Sized, 20 | { 21 | let focused_handle = window.focused(cx); 22 | let handles = self.cycle_focus_handles(window, cx); 23 | let handles = if is_next { 24 | handles 25 | } else { 26 | handles.into_iter().rev().collect() 27 | }; 28 | 29 | let fallback_handle = handles[0].clone(); 30 | let target_focus_handle = handles 31 | .into_iter() 32 | .skip_while(|handle| Some(handle) != focused_handle.as_ref()) 33 | .skip(1) 34 | .next() 35 | .unwrap_or(fallback_handle); 36 | 37 | target_focus_handle.focus(window); 38 | cx.stop_propagation(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/ui/src/highlighter/languages/html/highlights.scm: -------------------------------------------------------------------------------- 1 | (tag_name) @tag 2 | (erroneous_end_tag_name) @tag.error 3 | (doctype) @constant 4 | (attribute_name) @attribute 5 | (attribute_value) @string 6 | (comment) @comment 7 | 8 | [ 9 | "<" 10 | ">" 11 | "" 13 | ] @punctuation.bracket -------------------------------------------------------------------------------- /crates/ui/src/highlighter/languages/html/injections.scm: -------------------------------------------------------------------------------- 1 | ((script_element 2 | (raw_text) @injection.content) 3 | (#set! injection.language "javascript")) 4 | 5 | ((style_element 6 | (raw_text) @injection.content) 7 | (#set! injection.language "css")) -------------------------------------------------------------------------------- /crates/ui/src/highlighter/languages/markdown/highlights.scm: -------------------------------------------------------------------------------- 1 | ;From nvim-treesitter/nvim-treesitter 2 | (atx_heading (inline) @title) 3 | (setext_heading (paragraph) @title) 4 | 5 | [ 6 | (atx_h1_marker) 7 | (atx_h2_marker) 8 | (atx_h3_marker) 9 | (atx_h4_marker) 10 | (atx_h5_marker) 11 | (atx_h6_marker) 12 | (setext_h1_underline) 13 | (setext_h2_underline) 14 | ] @punctuation.special 15 | 16 | [ 17 | (link_title) 18 | (indented_code_block) 19 | (fenced_code_block) 20 | ] @text.literal 21 | 22 | [ 23 | (fenced_code_block_delimiter) 24 | ] @punctuation.delimiter 25 | 26 | (code_fence_content) @none 27 | 28 | [ 29 | (link_destination) 30 | ] @text.uri 31 | 32 | [ 33 | (link_label) 34 | ] @text.reference 35 | 36 | [ 37 | (list_marker_plus) 38 | (list_marker_minus) 39 | (list_marker_star) 40 | (list_marker_dot) 41 | (list_marker_parenthesis) 42 | (thematic_break) 43 | ] @punctuation.list_marker 44 | 45 | [ 46 | (block_continuation) 47 | (block_quote_marker) 48 | ] @punctuation.special 49 | 50 | [ 51 | (backslash_escape) 52 | ] @string.escape -------------------------------------------------------------------------------- /crates/ui/src/highlighter/languages/markdown/injections.scm: -------------------------------------------------------------------------------- 1 | (fenced_code_block 2 | (info_string 3 | (language) @injection.language) 4 | (code_fence_content) @injection.content) 5 | 6 | ((html_block) @injection.content (#set! injection.language "html")) 7 | 8 | (document . (section . (thematic_break) (_) @injection.content (thematic_break)) (#set! injection.language "yaml")) 9 | 10 | ((minus_metadata) @injection.content (#set! injection.language "yaml")) 11 | 12 | ((plus_metadata) @injection.content (#set! injection.language "toml")) 13 | 14 | ((inline) @injection.content (#set! injection.language "markdown_inline")) -------------------------------------------------------------------------------- /crates/ui/src/highlighter/languages/markdown_inline/highlights.scm: -------------------------------------------------------------------------------- 1 | [ 2 | (emphasis_delimiter) 3 | (code_span_delimiter) 4 | ] @punctuation.delimiter 5 | 6 | (emphasis) @emphasis 7 | 8 | (strong_emphasis) @emphasis.strong 9 | 10 | [ 11 | (link_destination) 12 | (uri_autolink) 13 | ] @link_uri 14 | 15 | [ 16 | (link_label) 17 | (link_text) 18 | (image_description) 19 | ] @link_text 20 | -------------------------------------------------------------------------------- /crates/ui/src/highlighter/languages/rust/README.md: -------------------------------------------------------------------------------- 1 | https://github.com/tree-sitter/tree-sitter-rust/tree/master/queries -------------------------------------------------------------------------------- /crates/ui/src/highlighter/languages/rust/injections.scm: -------------------------------------------------------------------------------- 1 | ((macro_invocation 2 | (token_tree) @injection.content) 3 | (#set! injection.language "rust") 4 | (#set! injection.include-children)) 5 | 6 | ((macro_rule 7 | (token_tree) @injection.content) 8 | (#set! injection.language "rust") 9 | (#set! injection.include-children)) -------------------------------------------------------------------------------- /crates/ui/src/highlighter/mod.rs: -------------------------------------------------------------------------------- 1 | mod highlighter; 2 | mod languages; 3 | mod registry; 4 | 5 | pub use highlighter::*; 6 | pub use languages::*; 7 | pub use registry::*; 8 | 9 | use gpui::App; 10 | 11 | pub fn init(cx: &mut App) { 12 | registry::init(cx); 13 | } 14 | -------------------------------------------------------------------------------- /crates/ui/src/highlighter/themes/README.md: -------------------------------------------------------------------------------- 1 | The default theme based on [zed-theme-macos-classic](https://github.com/huacnlee/zed-theme-macos-classic). -------------------------------------------------------------------------------- /crates/ui/src/indicator.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::{Icon, IconName, Sizable, Size}; 4 | use gpui::{ 5 | div, ease_in_out, percentage, prelude::FluentBuilder as _, Animation, AnimationExt as _, App, 6 | Hsla, IntoElement, ParentElement, RenderOnce, Styled as _, Transformation, Window, 7 | }; 8 | 9 | #[derive(IntoElement)] 10 | pub struct Indicator { 11 | size: Size, 12 | icon: Icon, 13 | speed: Duration, 14 | color: Option, 15 | } 16 | 17 | impl Indicator { 18 | pub fn new() -> Self { 19 | Self { 20 | size: Size::Medium, 21 | speed: Duration::from_secs_f64(0.8), 22 | icon: Icon::new(IconName::Loader), 23 | color: None, 24 | } 25 | } 26 | 27 | pub fn icon(mut self, icon: impl Into) -> Self { 28 | self.icon = icon.into(); 29 | self 30 | } 31 | 32 | pub fn color(mut self, color: Hsla) -> Self { 33 | self.color = Some(color); 34 | self 35 | } 36 | } 37 | 38 | impl Sizable for Indicator { 39 | fn with_size(mut self, size: impl Into) -> Self { 40 | self.size = size.into(); 41 | self 42 | } 43 | } 44 | 45 | impl RenderOnce for Indicator { 46 | fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { 47 | div() 48 | .child( 49 | self.icon 50 | .with_size(self.size) 51 | .when_some(self.color, |this, color| this.text_color(color)) 52 | .with_animation( 53 | "circle", 54 | Animation::new(self.speed).repeat().with_easing(ease_in_out), 55 | |this, delta| this.transform(Transformation::rotate(percentage(delta))), 56 | ), 57 | ) 58 | .into_element() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/ui/src/input/blink_cursor.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use gpui::{Context, Timer}; 4 | 5 | static INTERVAL: Duration = Duration::from_millis(500); 6 | static PAUSE_DELAY: Duration = Duration::from_millis(300); 7 | 8 | /// To manage the Input cursor blinking. 9 | /// 10 | /// It will start blinking with a interval of 500ms. 11 | /// Every loop will notify the view to update the `visible`, and Input will observe this update to touch repaint. 12 | /// 13 | /// The input painter will check if this in visible state, then it will draw the cursor. 14 | pub(crate) struct BlinkCursor { 15 | visible: bool, 16 | paused: bool, 17 | epoch: usize, 18 | } 19 | 20 | impl BlinkCursor { 21 | pub fn new() -> Self { 22 | Self { 23 | visible: false, 24 | paused: false, 25 | epoch: 0, 26 | } 27 | } 28 | 29 | /// Start the blinking 30 | pub fn start(&mut self, cx: &mut Context) { 31 | self.blink(self.epoch, cx); 32 | } 33 | 34 | pub fn stop(&mut self, cx: &mut Context) { 35 | self.epoch = 0; 36 | cx.notify(); 37 | } 38 | 39 | fn next_epoch(&mut self) -> usize { 40 | self.epoch += 1; 41 | self.epoch 42 | } 43 | 44 | fn blink(&mut self, epoch: usize, cx: &mut Context) { 45 | if self.paused || epoch != self.epoch { 46 | return; 47 | } 48 | 49 | self.visible = !self.visible; 50 | cx.notify(); 51 | 52 | // Schedule the next blink 53 | let epoch = self.next_epoch(); 54 | cx.spawn(async move |this, cx| { 55 | Timer::after(INTERVAL).await; 56 | if let Some(this) = this.upgrade() { 57 | this.update(cx, |this, cx| this.blink(epoch, cx)).ok(); 58 | } 59 | }) 60 | .detach(); 61 | } 62 | 63 | pub fn visible(&self) -> bool { 64 | // Keep showing the cursor if paused 65 | self.paused || self.visible 66 | } 67 | 68 | /// Pause the blinking, and delay 500ms to resume the blinking. 69 | pub fn pause(&mut self, cx: &mut Context) { 70 | self.paused = true; 71 | cx.notify(); 72 | 73 | // delay 500ms to start the blinking 74 | let epoch = self.next_epoch(); 75 | cx.spawn(async move |this, cx| { 76 | Timer::after(PAUSE_DELAY).await; 77 | 78 | if let Some(this) = this.upgrade() { 79 | this.update(cx, |this, cx| { 80 | this.paused = false; 81 | this.blink(epoch, cx); 82 | }) 83 | .ok(); 84 | } 85 | }) 86 | .detach(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /crates/ui/src/input/change.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, ops::Range}; 2 | 3 | use crate::history::HistoryItem; 4 | 5 | #[derive(Debug, PartialEq, Clone)] 6 | pub struct Change { 7 | pub(crate) old_range: Range, 8 | pub(crate) old_text: String, 9 | pub(crate) new_range: Range, 10 | pub(crate) new_text: String, 11 | version: usize, 12 | } 13 | 14 | impl Change { 15 | pub fn new( 16 | old_range: Range, 17 | old_text: &str, 18 | new_range: Range, 19 | new_text: &str, 20 | ) -> Self { 21 | Self { 22 | old_range, 23 | old_text: old_text.to_string(), 24 | new_range, 25 | new_text: new_text.to_string(), 26 | version: 0, 27 | } 28 | } 29 | } 30 | 31 | impl HistoryItem for Change { 32 | fn version(&self) -> usize { 33 | self.version 34 | } 35 | 36 | fn set_version(&mut self, version: usize) { 37 | self.version = version; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/ui/src/input/clear_button.rs: -------------------------------------------------------------------------------- 1 | use gpui::{App, Styled}; 2 | 3 | use crate::{ 4 | button::{Button, ButtonVariants as _}, 5 | ActiveTheme as _, Icon, IconName, Sizable as _, 6 | }; 7 | 8 | #[inline] 9 | pub(crate) fn clear_button(cx: &App) -> Button { 10 | Button::new("clean") 11 | .icon(Icon::new(IconName::CircleX)) 12 | .ghost() 13 | .xsmall() 14 | .text_color(cx.theme().muted_foreground) 15 | } 16 | -------------------------------------------------------------------------------- /crates/ui/src/input/mod.rs: -------------------------------------------------------------------------------- 1 | mod blink_cursor; 2 | mod change; 3 | mod clear_button; 4 | mod element; 5 | mod marker; 6 | mod mask_pattern; 7 | mod mode; 8 | mod number_input; 9 | mod otp_input; 10 | mod state; 11 | mod text_input; 12 | mod text_wrapper; 13 | 14 | pub(crate) use clear_button::*; 15 | pub use marker::*; 16 | pub use mask_pattern::MaskPattern; 17 | pub use mode::TabSize; 18 | pub use number_input::{NumberInput, NumberInputEvent, StepAction}; 19 | pub use otp_input::*; 20 | pub use state::*; 21 | pub use text_input::*; 22 | -------------------------------------------------------------------------------- /crates/ui/src/input/text_wrapper.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use gpui::{App, Font, LineFragment, Pixels, SharedString}; 4 | 5 | #[allow(unused)] 6 | pub(super) struct LineWrap { 7 | /// The number of soft wrapped lines of this line (Not include first line.) 8 | pub(super) wrap_lines: usize, 9 | /// The range of the line text in the entire text. 10 | pub(super) range: Range, 11 | } 12 | 13 | impl LineWrap { 14 | pub(super) fn height(&self, line_height: Pixels) -> Pixels { 15 | line_height * (self.wrap_lines + 1) 16 | } 17 | } 18 | 19 | /// Used to prepare the text with soft_wrap to be get lines to displayed in the TextArea 20 | /// 21 | /// After use lines to calculate the scroll size of the TextArea 22 | pub(super) struct TextWrapper { 23 | pub(super) text: SharedString, 24 | /// The wrapped lines, value is start and end index of the line. 25 | pub(super) wrapped_lines: Vec>, 26 | /// The lines by split \n 27 | pub(super) lines: Vec, 28 | pub(super) font: Font, 29 | pub(super) font_size: Pixels, 30 | /// If is none, it means the text is not wrapped 31 | pub(super) wrap_width: Option, 32 | } 33 | 34 | #[allow(unused)] 35 | impl TextWrapper { 36 | pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option) -> Self { 37 | Self { 38 | text: SharedString::default(), 39 | font, 40 | font_size, 41 | wrap_width, 42 | wrapped_lines: Vec::new(), 43 | lines: Vec::new(), 44 | } 45 | } 46 | 47 | pub(super) fn set_wrap_width(&mut self, wrap_width: Option, cx: &mut App) { 48 | self.wrap_width = wrap_width; 49 | self.update(self.text.clone(), true, cx); 50 | } 51 | 52 | pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) { 53 | self.font = font; 54 | self.font_size = font_size; 55 | self.update(self.text.clone(), true, cx); 56 | } 57 | 58 | /// Update the text wrapper and recalculate the wrapped lines. 59 | /// 60 | /// If the `text` is the same as the current text, do nothing. 61 | pub(super) fn update(&mut self, text: SharedString, force: bool, cx: &mut App) { 62 | if self.text == text && !force { 63 | return; 64 | } 65 | 66 | let mut wrapped_lines = vec![]; 67 | let mut lines = vec![]; 68 | let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX); 69 | let mut line_wrapper = cx 70 | .text_system() 71 | .line_wrapper(self.font.clone(), self.font_size); 72 | 73 | let mut prev_line_ix = 0; 74 | for line in text.split('\n') { 75 | let mut line_wraps = vec![]; 76 | let mut prev_boundary_ix = 0; 77 | 78 | // Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty. 79 | for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) { 80 | line_wraps.push(prev_boundary_ix..boundary.ix); 81 | prev_boundary_ix = boundary.ix; 82 | } 83 | 84 | lines.push(LineWrap { 85 | wrap_lines: line_wraps.len(), 86 | range: prev_line_ix..prev_line_ix + line.len(), 87 | }); 88 | 89 | wrapped_lines.extend(line_wraps); 90 | // Reset of the line 91 | if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 { 92 | wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len()); 93 | } 94 | 95 | prev_line_ix += line.len() + 1; 96 | } 97 | 98 | // Add last empty line. 99 | if text.chars().last().unwrap_or('\n') == '\n' { 100 | wrapped_lines.push(text.len()..text.len()); 101 | lines.push(LineWrap { 102 | wrap_lines: 0, 103 | range: text.len()..text.len(), 104 | }); 105 | } 106 | 107 | self.text = text; 108 | self.wrapped_lines = wrapped_lines; 109 | self.lines = lines; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /crates/ui/src/label.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | div, rems, App, Div, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, 3 | }; 4 | 5 | use crate::ActiveTheme; 6 | 7 | const MASKED: &'static str = "•"; 8 | 9 | #[derive(IntoElement)] 10 | pub struct Label { 11 | base: Div, 12 | label: SharedString, 13 | chars_count: usize, 14 | masked: bool, 15 | } 16 | 17 | impl Label { 18 | pub fn new(label: impl Into) -> Self { 19 | let label: SharedString = label.into(); 20 | let chars_count = label.chars().count(); 21 | Self { 22 | base: div().line_height(rems(1.25)), 23 | label, 24 | chars_count, 25 | masked: false, 26 | } 27 | } 28 | 29 | pub fn masked(mut self, masked: bool) -> Self { 30 | self.masked = masked; 31 | self 32 | } 33 | } 34 | 35 | impl Styled for Label { 36 | fn style(&mut self) -> &mut gpui::StyleRefinement { 37 | self.base.style() 38 | } 39 | } 40 | 41 | impl RenderOnce for Label { 42 | fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { 43 | let text = if self.masked { 44 | SharedString::from(MASKED.repeat(self.chars_count)) 45 | } else { 46 | self.label 47 | }; 48 | 49 | div() 50 | .text_color(cx.theme().foreground) 51 | .child(self.base.child(text)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/ui/src/link.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | div, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, MouseButton, ParentElement, 3 | RenderOnce, SharedString, Stateful, StatefulInteractiveElement, Styled, 4 | }; 5 | 6 | use crate::ActiveTheme as _; 7 | 8 | /// A Link element like a `` tag in HTML. 9 | #[derive(IntoElement)] 10 | pub struct Link { 11 | base: Stateful
, 12 | href: Option, 13 | disabled: bool, 14 | on_click: Option>, 15 | } 16 | 17 | impl Link { 18 | pub fn new(id: impl Into) -> Self { 19 | Self { 20 | base: div().id(id), 21 | href: None, 22 | on_click: None, 23 | disabled: false, 24 | } 25 | } 26 | 27 | pub fn href(mut self, href: impl Into) -> Self { 28 | self.href = Some(href.into()); 29 | self 30 | } 31 | 32 | pub fn on_click( 33 | mut self, 34 | handler: impl Fn(&ClickEvent, &mut gpui::Window, &mut gpui::App) + 'static, 35 | ) -> Self { 36 | self.on_click = Some(Box::new(handler)); 37 | self 38 | } 39 | 40 | pub fn disabled(mut self, disabled: bool) -> Self { 41 | self.disabled = disabled; 42 | self 43 | } 44 | } 45 | 46 | impl Styled for Link { 47 | fn style(&mut self) -> &mut gpui::StyleRefinement { 48 | self.base.style() 49 | } 50 | } 51 | 52 | impl ParentElement for Link { 53 | fn extend(&mut self, elements: impl IntoIterator) { 54 | self.base.extend(elements) 55 | } 56 | } 57 | 58 | impl RenderOnce for Link { 59 | fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement { 60 | let href = self.href.clone(); 61 | let on_click = self.on_click; 62 | 63 | div() 64 | .text_color(cx.theme().link) 65 | .text_decoration_1() 66 | .text_decoration_color(cx.theme().link) 67 | .hover(|this| { 68 | this.text_color(cx.theme().link.opacity(0.8)) 69 | .text_decoration_1() 70 | }) 71 | .cursor_pointer() 72 | .child( 73 | self.base 74 | .active(|this| { 75 | this.text_color(cx.theme().link.opacity(0.6)) 76 | .text_decoration_1() 77 | }) 78 | .on_mouse_down(MouseButton::Left, |_, _, cx| { 79 | cx.stop_propagation(); 80 | }) 81 | .on_click({ 82 | move |e, window, cx| { 83 | if let Some(href) = &href { 84 | cx.open_url(&href.clone()); 85 | } 86 | if let Some(on_click) = &on_click { 87 | on_click(e, window, cx); 88 | } 89 | } 90 | }), 91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /crates/ui/src/list/loading.rs: -------------------------------------------------------------------------------- 1 | use super::ListItem; 2 | use crate::{skeleton::Skeleton, v_flex}; 3 | use gpui::{IntoElement, ParentElement as _, RenderOnce, Styled}; 4 | 5 | #[derive(IntoElement)] 6 | pub struct Loading; 7 | 8 | #[derive(IntoElement)] 9 | struct LoadingItem; 10 | 11 | impl RenderOnce for LoadingItem { 12 | fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl IntoElement { 13 | ListItem::new("skeleton").disabled(true).child( 14 | v_flex() 15 | .gap_1p5() 16 | .overflow_hidden() 17 | .child(Skeleton::new().h_5().w_48().max_w_full()) 18 | .child(Skeleton::new().secondary(true).h_3().w_64().max_w_full()), 19 | ) 20 | } 21 | } 22 | 23 | impl RenderOnce for Loading { 24 | fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl IntoElement { 25 | v_flex() 26 | .py_2p5() 27 | .gap_3() 28 | .child(LoadingItem) 29 | .child(LoadingItem) 30 | .child(LoadingItem) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/ui/src/list/mod.rs: -------------------------------------------------------------------------------- 1 | mod list; 2 | mod list_item; 3 | mod loading; 4 | 5 | pub use list::*; 6 | pub use list_item::*; 7 | -------------------------------------------------------------------------------- /crates/ui/src/menu/mod.rs: -------------------------------------------------------------------------------- 1 | use gpui::App; 2 | 3 | pub mod context_menu; 4 | pub mod popup_menu; 5 | 6 | pub fn init(cx: &mut App) { 7 | popup_menu::init(cx); 8 | } 9 | -------------------------------------------------------------------------------- /crates/ui/src/plot/grid.rs: -------------------------------------------------------------------------------- 1 | use gpui::{px, Bounds, Hsla, PathBuilder, Pixels, Point, Window}; 2 | 3 | use super::{dash_line, origin_point}; 4 | 5 | pub struct Grid { 6 | x: Vec, 7 | y: Vec, 8 | stroke: Hsla, 9 | dash_array: Option<[Pixels; 2]>, 10 | } 11 | 12 | impl Grid { 13 | #[allow(clippy::new_without_default)] 14 | pub fn new() -> Self { 15 | Self { 16 | x: vec![], 17 | y: vec![], 18 | stroke: Default::default(), 19 | dash_array: None, 20 | } 21 | } 22 | 23 | /// Set the x of the Grid. 24 | pub fn x(mut self, x: Vec>) -> Self { 25 | self.x = x.into_iter().map(|v| v.into()).collect(); 26 | self 27 | } 28 | 29 | /// Set the y of the Grid. 30 | pub fn y(mut self, y: Vec>) -> Self { 31 | self.y = y.into_iter().map(|v| v.into()).collect(); 32 | self 33 | } 34 | 35 | /// Set the stroke color of the Grid. 36 | pub fn stroke(mut self, stroke: impl Into) -> Self { 37 | self.stroke = stroke.into(); 38 | self 39 | } 40 | 41 | /// Set the dash array of the Grid. 42 | pub fn dash_array(mut self, dash_array: [Pixels; 2]) -> Self { 43 | self.dash_array = Some(dash_array); 44 | self 45 | } 46 | 47 | fn points(&self, bounds: &Bounds) -> Vec<(Point, Point)> { 48 | let size = bounds.size; 49 | let origin = bounds.origin; 50 | 51 | let mut x = self 52 | .x 53 | .iter() 54 | .map(|x| { 55 | ( 56 | origin_point(*x, px(0.), origin), 57 | origin_point(*x, size.height, origin), 58 | ) 59 | }) 60 | .collect::>(); 61 | 62 | let y = self 63 | .y 64 | .iter() 65 | .map(|y| { 66 | ( 67 | origin_point(px(0.), *y, origin), 68 | origin_point(size.width, *y, origin), 69 | ) 70 | }) 71 | .collect::>(); 72 | 73 | x.extend(y); 74 | x 75 | } 76 | 77 | /// Paint the Grid. 78 | pub fn paint(&self, bounds: &Bounds, window: &mut Window) { 79 | let points = self.points(bounds); 80 | 81 | if let Some(dash_array) = self.dash_array { 82 | for (start, end) in points { 83 | if let Some(line) = dash_line(start, end, dash_array) { 84 | window.paint_path(line, self.stroke); 85 | } 86 | } 87 | } else { 88 | for (start, end) in points { 89 | let mut builder = PathBuilder::stroke(px(1.)); 90 | builder.move_to(start); 91 | builder.line_to(end); 92 | if let Ok(line) = builder.build() { 93 | window.paint_path(line, self.stroke); 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/ui/src/plot/label.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use gpui::{ 4 | point, px, App, Bounds, FontWeight, Hsla, Pixels, Point, SharedString, TextAlign, TextRun, 5 | Window, 6 | }; 7 | 8 | use super::origin_point; 9 | 10 | pub const TEXT_SIZE: f64 = 10.; 11 | pub const TEXT_GAP: f64 = 2.; 12 | pub const TEXT_HEIGHT: f64 = TEXT_SIZE + TEXT_GAP; 13 | 14 | pub struct Text { 15 | pub text: SharedString, 16 | pub origin: Point, 17 | pub color: Hsla, 18 | pub font_size: Pixels, 19 | pub font_weight: FontWeight, 20 | pub align: TextAlign, 21 | } 22 | 23 | impl Text { 24 | pub fn new(text: impl Into, origin: Point, color: Hsla) -> Self 25 | where 26 | T: Default + Clone + Copy + Debug + PartialEq + Into, 27 | { 28 | let origin = point(origin.x.into(), origin.y.into()); 29 | 30 | Self { 31 | text: text.into(), 32 | origin, 33 | color, 34 | font_size: TEXT_SIZE.into(), 35 | font_weight: FontWeight::NORMAL, 36 | align: TextAlign::Left, 37 | } 38 | } 39 | 40 | /// Set the font size of the Text. 41 | pub fn font_size(mut self, font_size: impl Into) -> Self { 42 | self.font_size = font_size.into(); 43 | self 44 | } 45 | 46 | /// Set the font weight of the Text. 47 | pub fn font_weight(mut self, font_weight: FontWeight) -> Self { 48 | self.font_weight = font_weight; 49 | self 50 | } 51 | 52 | /// Set the alignment of the Text. 53 | pub fn align(mut self, align: TextAlign) -> Self { 54 | self.align = align; 55 | self 56 | } 57 | } 58 | 59 | impl From for Label 60 | where 61 | I: Iterator, 62 | { 63 | fn from(items: I) -> Self { 64 | Self::new(items.collect()) 65 | } 66 | } 67 | 68 | #[derive(Default)] 69 | pub struct Label(Vec); 70 | 71 | impl Label { 72 | pub fn new(items: Vec) -> Self { 73 | Self(items) 74 | } 75 | 76 | /// Paint the Label. 77 | pub fn paint(&self, bounds: &Bounds, window: &mut Window, cx: &mut App) { 78 | for Text { 79 | text, 80 | origin, 81 | color, 82 | font_size, 83 | font_weight, 84 | align, 85 | } in self.0.iter() 86 | { 87 | let origin = origin_point(origin.x, origin.y, bounds.origin); 88 | 89 | let text_run = TextRun { 90 | len: text.len(), 91 | font: window.text_style().highlight(*font_weight).font(), 92 | color: *color, 93 | background_color: None, 94 | underline: None, 95 | strikethrough: None, 96 | }; 97 | 98 | if let Ok(text) = 99 | window 100 | .text_system() 101 | .shape_text(text.clone(), *font_size, &[text_run], None, None) 102 | { 103 | for line in text { 104 | let origin = match align { 105 | TextAlign::Left => origin, 106 | TextAlign::Right => origin - point(line.size(*font_size).width, px(0.)), 107 | _ => origin - point(line.size(*font_size).width / 2., px(0.)), 108 | }; 109 | 110 | let _ = line.paint(origin, *font_size, *align, None, window, cx); 111 | } 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /crates/ui/src/plot/mod.rs: -------------------------------------------------------------------------------- 1 | mod axis; 2 | mod grid; 3 | pub mod label; 4 | pub mod scale; 5 | pub mod shape; 6 | pub mod tooltip; 7 | 8 | pub use gpui_component_macros::IntoPlot; 9 | 10 | use std::{fmt::Debug, ops::Add}; 11 | 12 | use gpui::{ 13 | point, px, App, Bounds, IntoElement, Path, PathBuilder, PathStyle, Pixels, Point, 14 | StrokeOptions, Window, 15 | }; 16 | 17 | pub use axis::{Axis, AxisText, AXIS_GAP}; 18 | pub use grid::Grid; 19 | pub use label::Label; 20 | 21 | pub trait Plot: 'static + IntoElement { 22 | fn paint(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App); 23 | } 24 | 25 | #[derive(Clone, Copy, Default)] 26 | pub enum StrokeStyle { 27 | #[default] 28 | Natural, 29 | Linear, 30 | } 31 | 32 | pub fn origin_point(x: T, y: T, origin: Point) -> Point 33 | where 34 | T: Default + Clone + Debug + PartialEq + Add, 35 | { 36 | point(x, y) + origin 37 | } 38 | 39 | // TODO: Move into gpui 40 | // 41 | // https://github.com/zed-industries/zed/pull/31678 42 | pub fn dash_line(start: Point, end: Point, dash_array: [T; 2]) -> Option> 43 | where 44 | T: Default + Clone + Copy + Debug + PartialEq + Add + Into, 45 | { 46 | let mut path = lyon::path::Path::builder(); 47 | path.begin(lyon::geom::point( 48 | start.x.into() as f32, 49 | start.y.into() as f32, 50 | )); 51 | path.line_to(lyon::geom::point(end.x.into() as f32, end.y.into() as f32)); 52 | path.end(false); 53 | let path = path.build(); 54 | 55 | // Make path dashable. 56 | let measure = lyon::algorithms::measure::PathMeasurements::from_path(&path, 0.01); 57 | let mut sampler = 58 | measure.create_sampler(&path, lyon::algorithms::measure::SampleType::Normalized); 59 | let mut dashes = lyon::path::Path::builder(); 60 | let length = sampler.length(); 61 | let dash_length = dash_array[0].into() as f32; 62 | let gap_length = dash_array[1].into() as f32; 63 | let pattern_length = dash_length + gap_length; 64 | let num_patterns = (length / pattern_length).ceil() as usize; 65 | for i in 0..num_patterns { 66 | let start = i as f32 * pattern_length / length; 67 | let end = (i as f32 * pattern_length + dash_length) / length; 68 | sampler.split_range(start..end.min(1.), &mut dashes); 69 | } 70 | 71 | let mut path: PathBuilder = dashes.into(); 72 | path = path.with_style(PathStyle::Stroke( 73 | StrokeOptions::default().with_line_width(1.), 74 | )); 75 | path.build().ok() 76 | } 77 | 78 | pub fn polygon(points: &[Point], bounds: &Bounds) -> Option> 79 | where 80 | T: Default + Clone + Copy + Debug + Into + PartialEq, 81 | { 82 | let mut path = PathBuilder::stroke(px(1.)); 83 | let points = &points 84 | .iter() 85 | .map(|p| { 86 | point( 87 | px((p.x.into() + bounds.origin.x.to_f64()) as f32), 88 | px((p.y.into() + bounds.origin.y.to_f64()) as f32), 89 | ) 90 | }) 91 | .collect::>(); 92 | path.add_polygon(points, false); 93 | path.build().ok() 94 | } 95 | -------------------------------------------------------------------------------- /crates/ui/src/plot/scale.rs: -------------------------------------------------------------------------------- 1 | mod band; 2 | mod linear; 3 | mod point; 4 | mod sealed; 5 | 6 | pub use band::ScaleBand; 7 | pub use linear::ScaleLinear; 8 | pub use point::ScalePoint; 9 | pub(crate) use sealed::Sealed; 10 | 11 | pub trait Scale { 12 | /// Get the tick of the scale. 13 | fn tick(&self, value: &T) -> Option; 14 | 15 | /// Get the least index of the scale. 16 | fn least_index(&self, tick: f64) -> usize; 17 | } 18 | -------------------------------------------------------------------------------- /crates/ui/src/plot/scale/band.rs: -------------------------------------------------------------------------------- 1 | // @reference: https://d3js.org/d3-scale/band 2 | 3 | use itertools::Itertools; 4 | use num_traits::Zero; 5 | 6 | use super::Scale; 7 | 8 | #[derive(Clone)] 9 | pub struct ScaleBand { 10 | domain: Vec, 11 | range_diff: f64, 12 | avg_width: f64, 13 | padding_inner: f64, 14 | padding_outer: f64, 15 | } 16 | 17 | impl ScaleBand { 18 | pub fn new(domain: Vec, range: Vec) -> Self { 19 | let len = domain.len() as f64; 20 | let range_diff = range 21 | .iter() 22 | .minmax() 23 | .into_option() 24 | .map_or(0., |(min, max)| max - min); 25 | 26 | Self { 27 | domain, 28 | range_diff, 29 | avg_width: if len.is_zero() { 0. } else { range_diff / len }, 30 | padding_inner: 0., 31 | padding_outer: 0., 32 | } 33 | } 34 | 35 | /// Get the width of the band. 36 | pub fn band_width(&self) -> f64 { 37 | (self.avg_width * (1. - self.padding_inner)).min(30.) 38 | } 39 | 40 | /// Set the padding inner of the band. 41 | pub fn padding_inner(mut self, padding_inner: f64) -> Self { 42 | self.padding_inner = padding_inner; 43 | self 44 | } 45 | 46 | /// Set the padding outer of the band. 47 | pub fn padding_outer(mut self, padding_outer: f64) -> Self { 48 | self.padding_outer = padding_outer; 49 | self 50 | } 51 | } 52 | 53 | impl Scale for ScaleBand 54 | where 55 | T: PartialEq, 56 | { 57 | fn tick(&self, value: &T) -> Option { 58 | let index = self.domain.iter().position(|v| v == value)?; 59 | let domain_len = self.domain.len(); 60 | 61 | // When there's only one element, place it in the center. 62 | if domain_len == 1 { 63 | return Some((self.range_diff - self.band_width()) / 2.); 64 | } 65 | 66 | let ratio = 1. + self.padding_inner / (self.domain.len() - 1) as f64; 67 | let padding_outer_width = self.avg_width * self.padding_outer; 68 | let avg_width = (self.range_diff - padding_outer_width * 2.) / self.domain.len() as f64; 69 | Some(index as f64 * avg_width * ratio + padding_outer_width) 70 | } 71 | 72 | fn least_index(&self, tick: f64) -> usize { 73 | let index = (tick / self.avg_width).round() as usize; 74 | index.min(self.domain.len().saturating_sub(1)) 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use super::*; 81 | 82 | #[test] 83 | fn test_scale_band() { 84 | let scale = ScaleBand::new(vec![1, 2, 3], vec![0., 90.]); 85 | assert_eq!(scale.tick(&1), Some(0.)); 86 | assert_eq!(scale.tick(&2), Some(30.)); 87 | assert_eq!(scale.tick(&3), Some(60.)); 88 | assert_eq!(scale.band_width(), 30.); 89 | } 90 | 91 | #[test] 92 | fn test_scale_band_zero() { 93 | let scale = ScaleBand::new(vec![], vec![0., 90.]); 94 | assert_eq!(scale.tick(&1), None); 95 | assert_eq!(scale.tick(&2), None); 96 | assert_eq!(scale.tick(&3), None); 97 | assert_eq!(scale.band_width(), 0.); 98 | 99 | let scale = ScaleBand::new(vec![1, 2, 3], vec![]); 100 | assert_eq!(scale.tick(&1), Some(0.)); 101 | assert_eq!(scale.tick(&2), Some(0.)); 102 | assert_eq!(scale.tick(&3), Some(0.)); 103 | assert_eq!(scale.band_width(), 0.); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /crates/ui/src/plot/scale/linear.rs: -------------------------------------------------------------------------------- 1 | // @reference: https://d3js.org/d3-scale/linear 2 | 3 | use itertools::Itertools; 4 | use num_traits::{Num, ToPrimitive}; 5 | 6 | use super::{sealed::Sealed, Scale}; 7 | 8 | #[derive(Clone)] 9 | pub struct ScaleLinear { 10 | domain_len: usize, 11 | domain_min: T, 12 | domain_diff: T, 13 | range_min: f64, 14 | range_diff: f64, 15 | } 16 | 17 | impl ScaleLinear 18 | where 19 | T: Copy + PartialOrd + Num + ToPrimitive + Sealed, 20 | { 21 | pub fn new(domain: Vec, range: Vec) -> Self { 22 | let (domain_min, domain_max) = domain 23 | .iter() 24 | .minmax() 25 | .into_option() 26 | .map_or((T::zero(), T::zero()), |(min, max)| (*min, *max)); 27 | 28 | let (range_min, range_max) = range 29 | .iter() 30 | .minmax() 31 | .into_option() 32 | .map_or((0., 0.), |(min, max)| (*min, *max)); 33 | 34 | Self { 35 | domain_len: domain.len(), 36 | domain_min, 37 | domain_diff: domain_max - domain_min, 38 | range_min, 39 | range_diff: range_max - range_min, 40 | } 41 | } 42 | } 43 | 44 | impl Scale for ScaleLinear 45 | where 46 | T: Copy + PartialOrd + Num + ToPrimitive + Sealed, 47 | { 48 | fn tick(&self, value: &T) -> Option { 49 | if self.domain_diff.is_zero() { 50 | return None; 51 | } 52 | 53 | let ratio = ((*value - self.domain_min) / self.domain_diff).to_f64()?; 54 | 55 | Some((1. - ratio) * self.range_diff + self.range_min) 56 | } 57 | 58 | fn least_index(&self, tick: f64) -> usize { 59 | let index = (tick / self.range_diff).round() as usize; 60 | index.min(self.domain_len.saturating_sub(1)) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | 68 | #[test] 69 | fn test_scale_linear_1() { 70 | let scale = ScaleLinear::new(vec![1., 2., 3.], vec![0., 100.]); 71 | assert_eq!(scale.tick(&1.), Some(100.)); 72 | assert_eq!(scale.tick(&2.), Some(50.)); 73 | assert_eq!(scale.tick(&3.), Some(0.)); 74 | } 75 | 76 | #[test] 77 | fn test_scale_linear_2() { 78 | let scale = ScaleLinear::new(vec![], vec![0., 100.]); 79 | assert_eq!(scale.tick(&1.), None); 80 | assert_eq!(scale.tick(&2.), None); 81 | assert_eq!(scale.tick(&3.), None); 82 | 83 | let scale = ScaleLinear::new(vec![1., 2., 3.], vec![]); 84 | assert_eq!(scale.tick(&1.), Some(0.)); 85 | assert_eq!(scale.tick(&2.), Some(0.)); 86 | assert_eq!(scale.tick(&3.), Some(0.)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /crates/ui/src/plot/scale/point.rs: -------------------------------------------------------------------------------- 1 | // @reference: https://d3js.org/d3-scale/point 2 | 3 | use itertools::Itertools; 4 | use num_traits::Zero; 5 | 6 | use super::Scale; 7 | 8 | #[derive(Clone)] 9 | pub struct ScalePoint { 10 | domain: Vec, 11 | range_tick: f64, 12 | } 13 | 14 | impl ScalePoint 15 | where 16 | T: PartialEq, 17 | { 18 | pub fn new(domain: Vec, range: Vec) -> Self { 19 | let len = domain.len(); 20 | let range_tick = if len.is_zero() { 21 | 0. 22 | } else { 23 | let range_diff = range 24 | .iter() 25 | .minmax() 26 | .into_option() 27 | .map_or(0., |(min, max)| max - min); 28 | 29 | range_diff / (len - 1) as f64 30 | }; 31 | 32 | Self { domain, range_tick } 33 | } 34 | } 35 | 36 | impl Scale for ScalePoint 37 | where 38 | T: PartialEq, 39 | { 40 | fn tick(&self, value: &T) -> Option { 41 | let index = self.domain.iter().position(|v| v == value)?; 42 | Some(index as f64 * self.range_tick) 43 | } 44 | 45 | fn least_index(&self, tick: f64) -> usize { 46 | let index = (tick / self.range_tick).round() as usize; 47 | index.min(self.domain.len().saturating_sub(1)) 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::*; 54 | 55 | #[test] 56 | fn test_scale_point_1() { 57 | let scale = ScalePoint::new(vec![1, 2, 3], vec![0., 100.]); 58 | assert_eq!(scale.tick(&1), Some(0.)); 59 | assert_eq!(scale.tick(&2), Some(50.)); 60 | assert_eq!(scale.tick(&3), Some(100.)); 61 | } 62 | 63 | #[test] 64 | fn test_scale_point_2() { 65 | let scale = ScalePoint::new(vec![], vec![0., 100.]); 66 | assert_eq!(scale.tick(&1), None); 67 | assert_eq!(scale.tick(&2), None); 68 | assert_eq!(scale.tick(&3), None); 69 | 70 | let scale = ScalePoint::new(vec![1, 2, 3], vec![]); 71 | assert_eq!(scale.tick(&1), Some(0.)); 72 | assert_eq!(scale.tick(&2), Some(0.)); 73 | assert_eq!(scale.tick(&3), Some(0.)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /crates/ui/src/plot/scale/sealed.rs: -------------------------------------------------------------------------------- 1 | pub trait Sealed {} 2 | 3 | impl Sealed for f64 {} 4 | 5 | #[cfg(feature = "decimal")] 6 | impl Sealed for rust_decimal::Decimal {} 7 | -------------------------------------------------------------------------------- /crates/ui/src/plot/shape.rs: -------------------------------------------------------------------------------- 1 | mod arc; 2 | mod area; 3 | mod bar; 4 | mod line; 5 | mod pie; 6 | 7 | pub use arc::Arc; 8 | pub use area::Area; 9 | pub use bar::Bar; 10 | pub use line::Line; 11 | pub use pie::Pie; 12 | -------------------------------------------------------------------------------- /crates/ui/src/plot/shape/pie.rs: -------------------------------------------------------------------------------- 1 | // @reference: https://d3js.org/d3-shape/pie 2 | 3 | use std::f64::consts::TAU; 4 | 5 | use super::arc::ArcData; 6 | 7 | #[allow(clippy::type_complexity)] 8 | pub struct Pie { 9 | value: Box Option>, 10 | start_angle: f64, 11 | end_angle: f64, 12 | pad_angle: f64, 13 | } 14 | 15 | impl Default for Pie { 16 | fn default() -> Self { 17 | Self { 18 | value: Box::new(|_| None), 19 | start_angle: 0., 20 | end_angle: TAU, 21 | pad_angle: 0., 22 | } 23 | } 24 | } 25 | 26 | impl Pie { 27 | pub fn new() -> Self { 28 | Self::default() 29 | } 30 | 31 | /// Set the value of the Pie. 32 | pub fn value(mut self, value: F) -> Self 33 | where 34 | F: 'static + Fn(&T) -> Option, 35 | { 36 | self.value = Box::new(value); 37 | self 38 | } 39 | 40 | /// Set the start angle of the Pie. 41 | pub fn start_angle(mut self, start_angle: f64) -> Self { 42 | self.start_angle = start_angle; 43 | self 44 | } 45 | 46 | /// Set the end angle of the Pie. 47 | pub fn end_angle(mut self, end_angle: f64) -> Self { 48 | self.end_angle = end_angle; 49 | self 50 | } 51 | 52 | /// Set the pad angle of the Pie. 53 | pub fn pad_angle(mut self, pad_angle: f64) -> Self { 54 | self.pad_angle = pad_angle; 55 | self 56 | } 57 | 58 | /// Get the arcs of the Pie. 59 | pub fn arcs<'a>(&self, data: &'a [T]) -> Vec> { 60 | let mut values = Vec::new(); 61 | let mut sum = 0.; 62 | 63 | for (idx, v) in data.iter().enumerate() { 64 | if let Some(value) = (self.value)(v) { 65 | if value > 0. { 66 | sum += value; 67 | values.push((idx, v, value)); 68 | } 69 | } 70 | } 71 | 72 | let mut arcs = Vec::with_capacity(values.len()); 73 | let mut k = self.start_angle; 74 | 75 | for (index, v, value) in values { 76 | let start_angle = k; 77 | let angle_delta = if sum > 0. { 78 | (value / sum) * (self.end_angle - self.start_angle) 79 | } else { 80 | 0. 81 | }; 82 | k += angle_delta; 83 | let end_angle = k; 84 | 85 | arcs.push(ArcData { 86 | data: v, 87 | index, 88 | value, 89 | start_angle, 90 | end_angle, 91 | pad_angle: self.pad_angle, 92 | }); 93 | } 94 | 95 | arcs 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod tests { 101 | use super::*; 102 | 103 | #[test] 104 | fn test_pie() { 105 | let pie = Pie::new().value(|v| Some(*v)); 106 | 107 | let data = vec![1., 1., 1.]; 108 | let arcs = pie.arcs(&data); 109 | 110 | assert_eq!(arcs.len(), 3); 111 | 112 | assert_eq!(arcs[0].value, 1.); 113 | assert_eq!(arcs[1].value, 1.); 114 | assert_eq!(arcs[2].value, 1.); 115 | 116 | assert_eq!(arcs[0].start_angle, 0.); 117 | assert_eq!(arcs[0].end_angle, arcs[1].start_angle); 118 | assert_eq!(arcs[1].end_angle, arcs[2].start_angle); 119 | assert_eq!(arcs[2].end_angle, TAU); 120 | } 121 | 122 | #[test] 123 | fn test_pie_zero_values() { 124 | let pie = Pie::new().value(|v| Some(*v)); 125 | let data = vec![0., 1., 0., 2.]; 126 | let arcs = pie.arcs(&data); 127 | 128 | assert_eq!(arcs.len(), 2); 129 | assert_eq!(arcs[0].value, 1.); 130 | assert_eq!(arcs[1].value, 2.); 131 | assert_eq!(arcs[0].index, 1); 132 | assert_eq!(arcs[1].index, 3); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /crates/ui/src/progress.rs: -------------------------------------------------------------------------------- 1 | use crate::ActiveTheme; 2 | use gpui::{ 3 | div, prelude::FluentBuilder, px, relative, App, IntoElement, ParentElement, RenderOnce, Styled, 4 | Window, 5 | }; 6 | 7 | /// A Progress bar element. 8 | #[derive(IntoElement)] 9 | pub struct Progress { 10 | value: f32, 11 | height: f32, 12 | } 13 | 14 | impl Progress { 15 | pub fn new() -> Self { 16 | Progress { 17 | value: Default::default(), 18 | height: 8., 19 | } 20 | } 21 | 22 | pub fn value(mut self, value: f32) -> Self { 23 | self.value = value; 24 | self 25 | } 26 | } 27 | 28 | impl RenderOnce for Progress { 29 | fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { 30 | // Match the theme radius, if theme radius is zero use it. 31 | let radius = px(self.height / 2.).min(cx.theme().radius); 32 | let relative_w = relative(match self.value { 33 | v if v < 0. => 0., 34 | v if v > 100. => 1., 35 | v => v / 100., 36 | }); 37 | 38 | div() 39 | .relative() 40 | .h(px(self.height)) 41 | .rounded(radius) 42 | .bg(cx.theme().progress_bar.opacity(0.2)) 43 | .child( 44 | div() 45 | .absolute() 46 | .top_0() 47 | .left_0() 48 | .h_full() 49 | .w(relative_w) 50 | .bg(cx.theme().progress_bar) 51 | .map(|this| match self.value { 52 | v if v >= 100. => this.rounded(radius), 53 | _ => this.rounded_l(radius), 54 | }), 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/ui/src/scroll/mod.rs: -------------------------------------------------------------------------------- 1 | mod scrollable; 2 | mod scrollable_mask; 3 | mod scrollbar; 4 | 5 | pub use scrollable::*; 6 | pub use scrollable_mask::*; 7 | pub use scrollbar::*; 8 | -------------------------------------------------------------------------------- /crates/ui/src/sidebar/footer.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | prelude::FluentBuilder as _, Div, ElementId, InteractiveElement, IntoElement, ParentElement, 3 | RenderOnce, SharedString, Styled, 4 | }; 5 | 6 | use crate::{h_flex, popup_menu::PopupMenuExt, ActiveTheme as _, Collapsible, Selectable}; 7 | 8 | #[derive(IntoElement)] 9 | pub struct SidebarFooter { 10 | id: ElementId, 11 | base: Div, 12 | selected: bool, 13 | collapsed: bool, 14 | } 15 | 16 | impl SidebarFooter { 17 | pub fn new() -> Self { 18 | Self { 19 | id: SharedString::from("sidebar-footer").into(), 20 | base: h_flex().gap_2().w_full(), 21 | selected: false, 22 | collapsed: false, 23 | } 24 | } 25 | } 26 | impl Selectable for SidebarFooter { 27 | fn selected(mut self, selected: bool) -> Self { 28 | self.selected = selected; 29 | self 30 | } 31 | 32 | fn element_id(&self) -> &gpui::ElementId { 33 | &self.id 34 | } 35 | 36 | fn is_selected(&self) -> bool { 37 | self.selected 38 | } 39 | } 40 | impl Collapsible for SidebarFooter { 41 | fn is_collapsed(&self) -> bool { 42 | self.collapsed 43 | } 44 | 45 | fn collapsed(mut self, collapsed: bool) -> Self { 46 | self.collapsed = collapsed; 47 | self 48 | } 49 | } 50 | impl ParentElement for SidebarFooter { 51 | fn extend(&mut self, elements: impl IntoIterator) { 52 | self.base.extend(elements); 53 | } 54 | } 55 | impl Styled for SidebarFooter { 56 | fn style(&mut self) -> &mut gpui::StyleRefinement { 57 | self.base.style() 58 | } 59 | } 60 | impl PopupMenuExt for SidebarFooter {} 61 | impl RenderOnce for SidebarFooter { 62 | fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement { 63 | h_flex() 64 | .id(self.id) 65 | .gap_2() 66 | .p_2() 67 | .w_full() 68 | .justify_between() 69 | .rounded(cx.theme().radius) 70 | .hover(|this| { 71 | this.bg(cx.theme().sidebar_accent) 72 | .text_color(cx.theme().sidebar_accent_foreground) 73 | }) 74 | .when(self.selected, |this| { 75 | this.bg(cx.theme().sidebar_accent) 76 | .text_color(cx.theme().sidebar_accent_foreground) 77 | }) 78 | .child(self.base) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/ui/src/sidebar/group.rs: -------------------------------------------------------------------------------- 1 | use crate::{v_flex, ActiveTheme, Collapsible}; 2 | use gpui::{ 3 | div, prelude::FluentBuilder as _, App, Div, IntoElement, ParentElement, RenderOnce, 4 | SharedString, Styled as _, Window, 5 | }; 6 | 7 | /// A sidebar group 8 | #[derive(IntoElement)] 9 | pub struct SidebarGroup { 10 | base: Div, 11 | label: SharedString, 12 | collapsed: bool, 13 | children: Vec, 14 | } 15 | 16 | impl SidebarGroup { 17 | pub fn new(label: impl Into) -> Self { 18 | Self { 19 | base: div().gap_2().flex_col(), 20 | label: label.into(), 21 | collapsed: false, 22 | children: Vec::new(), 23 | } 24 | } 25 | 26 | pub fn child(mut self, child: E) -> Self { 27 | self.children.push(child); 28 | self 29 | } 30 | 31 | pub fn children(mut self, children: impl IntoIterator) -> Self { 32 | self.children.extend(children); 33 | self 34 | } 35 | } 36 | impl Collapsible for SidebarGroup { 37 | fn is_collapsed(&self) -> bool { 38 | self.collapsed 39 | } 40 | 41 | fn collapsed(mut self, collapsed: bool) -> Self { 42 | self.collapsed = collapsed; 43 | self 44 | } 45 | } 46 | impl RenderOnce for SidebarGroup { 47 | fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { 48 | v_flex() 49 | .relative() 50 | .p_2() 51 | .when(!self.collapsed, |this| { 52 | this.child( 53 | div() 54 | .flex_shrink_0() 55 | .px_2() 56 | .rounded(cx.theme().radius) 57 | .text_xs() 58 | .text_color(cx.theme().sidebar_foreground.opacity(0.7)) 59 | .h_8() 60 | .child(self.label), 61 | ) 62 | }) 63 | .child( 64 | self.base.children( 65 | self.children 66 | .into_iter() 67 | .map(|child| child.collapsed(self.collapsed)), 68 | ), 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/ui/src/sidebar/header.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | prelude::FluentBuilder as _, Div, ElementId, InteractiveElement, IntoElement, ParentElement, 3 | RenderOnce, SharedString, Styled, 4 | }; 5 | 6 | use crate::{h_flex, popup_menu::PopupMenuExt, ActiveTheme as _, Collapsible, Selectable}; 7 | 8 | #[derive(IntoElement)] 9 | pub struct SidebarHeader { 10 | id: ElementId, 11 | base: Div, 12 | selected: bool, 13 | collapsed: bool, 14 | } 15 | 16 | impl SidebarHeader { 17 | pub fn new() -> Self { 18 | Self { 19 | id: SharedString::from("sidebar-header").into(), 20 | base: h_flex().gap_2().w_full(), 21 | selected: false, 22 | collapsed: false, 23 | } 24 | } 25 | } 26 | impl Selectable for SidebarHeader { 27 | fn selected(mut self, selected: bool) -> Self { 28 | self.selected = selected; 29 | self 30 | } 31 | 32 | fn element_id(&self) -> &gpui::ElementId { 33 | &self.id 34 | } 35 | 36 | fn is_selected(&self) -> bool { 37 | self.selected 38 | } 39 | } 40 | 41 | impl Collapsible for SidebarHeader { 42 | fn is_collapsed(&self) -> bool { 43 | self.collapsed 44 | } 45 | 46 | fn collapsed(mut self, collapsed: bool) -> Self { 47 | self.collapsed = collapsed; 48 | self 49 | } 50 | } 51 | impl ParentElement for SidebarHeader { 52 | fn extend(&mut self, elements: impl IntoIterator) { 53 | self.base.extend(elements); 54 | } 55 | } 56 | impl Styled for SidebarHeader { 57 | fn style(&mut self) -> &mut gpui::StyleRefinement { 58 | self.base.style() 59 | } 60 | } 61 | impl PopupMenuExt for SidebarHeader {} 62 | impl RenderOnce for SidebarHeader { 63 | fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement { 64 | h_flex() 65 | .id(self.id) 66 | .gap_2() 67 | .p_2() 68 | .w_full() 69 | .justify_between() 70 | .rounded(cx.theme().radius) 71 | .hover(|this| { 72 | this.bg(cx.theme().sidebar_accent) 73 | .text_color(cx.theme().sidebar_accent_foreground) 74 | }) 75 | .when(self.selected, |this| { 76 | this.bg(cx.theme().sidebar_accent) 77 | .text_color(cx.theme().sidebar_accent_foreground) 78 | }) 79 | .child(self.base) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/ui/src/skeleton.rs: -------------------------------------------------------------------------------- 1 | use crate::ActiveTheme; 2 | use gpui::{ 3 | bounce, div, ease_in_out, Animation, AnimationExt, Div, IntoElement, RenderOnce, Styled, 4 | }; 5 | use std::time::Duration; 6 | 7 | #[derive(IntoElement)] 8 | pub struct Skeleton { 9 | base: Div, 10 | secondary: bool, 11 | } 12 | 13 | impl Skeleton { 14 | pub fn new() -> Self { 15 | Self { 16 | base: div().w_full().h_4(), 17 | secondary: false, 18 | } 19 | } 20 | 21 | /// Set use secondary color. 22 | pub fn secondary(mut self, secondary: bool) -> Self { 23 | self.secondary = secondary; 24 | self 25 | } 26 | } 27 | 28 | impl Styled for Skeleton { 29 | fn style(&mut self) -> &mut gpui::StyleRefinement { 30 | self.base.style() 31 | } 32 | } 33 | 34 | impl RenderOnce for Skeleton { 35 | fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement { 36 | let color = if self.secondary { 37 | cx.theme().skeleton.opacity(0.5) 38 | } else { 39 | cx.theme().skeleton 40 | }; 41 | 42 | self.base.bg(color).with_animation( 43 | "skeleton", 44 | Animation::new(Duration::from_secs(2)) 45 | .repeat() 46 | .with_easing(bounce(ease_in_out)), 47 | move |this, delta| { 48 | let v = 1.0 - delta * 0.5; 49 | this.opacity(v) 50 | }, 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/ui/src/tab.rs: -------------------------------------------------------------------------------- 1 | mod tab; 2 | mod tab_bar; 3 | 4 | pub use tab::*; 5 | pub use tab_bar::*; 6 | -------------------------------------------------------------------------------- /crates/ui/src/table/loading.rs: -------------------------------------------------------------------------------- 1 | use crate::{h_flex, skeleton::Skeleton, v_flex, ActiveTheme, Size}; 2 | use gpui::{prelude::FluentBuilder as _, IntoElement, ParentElement as _, RenderOnce, Styled}; 3 | 4 | #[derive(IntoElement)] 5 | pub struct Loading { 6 | size: Size, 7 | } 8 | 9 | impl Loading { 10 | pub fn new() -> Self { 11 | Self { size: Size::Medium } 12 | } 13 | 14 | pub fn size(mut self, size: Size) -> Self { 15 | self.size = size; 16 | self 17 | } 18 | } 19 | 20 | #[derive(IntoElement)] 21 | struct LoadingRow { 22 | header: bool, 23 | size: Size, 24 | } 25 | 26 | impl LoadingRow { 27 | pub fn header() -> Self { 28 | Self { 29 | header: true, 30 | size: Size::Medium, 31 | } 32 | } 33 | 34 | pub fn row() -> Self { 35 | Self { 36 | header: false, 37 | size: Size::Medium, 38 | } 39 | } 40 | 41 | pub fn size(mut self, size: Size) -> Self { 42 | self.size = size; 43 | self 44 | } 45 | } 46 | 47 | impl RenderOnce for LoadingRow { 48 | fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement { 49 | let paddings = self.size.table_cell_padding(); 50 | let height = self.size.table_row_height() * 0.5; 51 | 52 | h_flex() 53 | .gap_3() 54 | .h(self.size.table_row_height()) 55 | .overflow_hidden() 56 | .pt(paddings.top) 57 | .pb(paddings.bottom) 58 | .pl(paddings.left) 59 | .pr(paddings.right) 60 | .items_center() 61 | .justify_between() 62 | .overflow_hidden() 63 | .when(self.header, |this| this.bg(cx.theme().table_head)) 64 | .when(!self.header, |this| { 65 | this.border_t_1().border_color(cx.theme().table_row_border) 66 | }) 67 | .child( 68 | h_flex() 69 | .gap_3() 70 | .flex_1() 71 | .child(Skeleton::new().secondary(self.header).h(height).w_24()) 72 | .child(Skeleton::new().secondary(self.header).h(height).w_48()) 73 | .child(Skeleton::new().secondary(self.header).h(height).w_16()), 74 | ) 75 | .child(Skeleton::new().secondary(self.header).h(height).w_24()) 76 | } 77 | } 78 | 79 | impl RenderOnce for Loading { 80 | fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl IntoElement { 81 | v_flex() 82 | .gap_0() 83 | .child(LoadingRow::header().size(self.size)) 84 | .child(LoadingRow::row().size(self.size)) 85 | .child(LoadingRow::row().size(self.size)) 86 | .child(LoadingRow::row().size(self.size)) 87 | .child(LoadingRow::row().size(self.size)) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/ui/src/text/mod.rs: -------------------------------------------------------------------------------- 1 | mod element; 2 | mod html; 3 | mod markdown; 4 | mod text_view; 5 | mod utils; 6 | 7 | pub use text_view::*; 8 | -------------------------------------------------------------------------------- /crates/ui/src/text/utils.rs: -------------------------------------------------------------------------------- 1 | const NUMBERED_PREFIXES_1: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 2 | const NUMBERED_PREFIXES_2: &str = "abcdefghijklmnopqrstuvwxyz"; 3 | 4 | const BULLETS: [&str; 5] = ["▪", "•", "◦", "‣", "⁃"]; 5 | 6 | /// Returns the prefix for a list item. 7 | pub fn list_item_prefix(ix: usize, ordered: bool, depth: usize) -> String { 8 | if ordered { 9 | if depth == 0 { 10 | return format!("{}. ", ix + 1); 11 | } 12 | 13 | if depth == 1 { 14 | return format!( 15 | "{}. ", 16 | NUMBERED_PREFIXES_1 17 | .chars() 18 | .nth(ix % NUMBERED_PREFIXES_1.len()) 19 | .unwrap() 20 | ); 21 | } else { 22 | return format!( 23 | "{}. ", 24 | NUMBERED_PREFIXES_2 25 | .chars() 26 | .nth(ix % NUMBERED_PREFIXES_2.len()) 27 | .unwrap() 28 | ); 29 | } 30 | } else { 31 | let depth = depth.min(BULLETS.len() - 1); 32 | let bullet = BULLETS[depth]; 33 | return format!("{} ", bullet); 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use crate::text::utils::list_item_prefix; 40 | 41 | #[test] 42 | fn test_list_item_prefix() { 43 | assert_eq!(list_item_prefix(0, true, 0), "1. "); 44 | assert_eq!(list_item_prefix(1, true, 0), "2. "); 45 | assert_eq!(list_item_prefix(2, true, 0), "3. "); 46 | assert_eq!(list_item_prefix(10, true, 0), "11. "); 47 | assert_eq!(list_item_prefix(0, true, 1), "A. "); 48 | assert_eq!(list_item_prefix(1, true, 1), "B. "); 49 | assert_eq!(list_item_prefix(2, true, 1), "C. "); 50 | assert_eq!(list_item_prefix(0, true, 2), "a. "); 51 | assert_eq!(list_item_prefix(1, true, 2), "b. "); 52 | assert_eq!(list_item_prefix(6, true, 2), "g. "); 53 | assert_eq!(list_item_prefix(0, true, 1), "A. "); 54 | assert_eq!(list_item_prefix(0, true, 2), "a. "); 55 | assert_eq!(list_item_prefix(0, false, 0), "▪ "); 56 | assert_eq!(list_item_prefix(0, false, 1), "• "); 57 | assert_eq!(list_item_prefix(0, false, 2), "◦ "); 58 | assert_eq!(list_item_prefix(0, false, 3), "‣ "); 59 | assert_eq!(list_item_prefix(0, false, 4), "⁃ "); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/ui/src/time/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod calendar; 2 | pub mod date_picker; 3 | mod utils; 4 | -------------------------------------------------------------------------------- /crates/ui/src/tooltip.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | div, prelude::FluentBuilder, px, Action, AnyElement, AnyView, App, AppContext, Context, 3 | IntoElement, ParentElement, Render, SharedString, Styled, Window, 4 | }; 5 | 6 | use crate::{h_flex, text::Text, ActiveTheme, Kbd}; 7 | 8 | enum TooltipContext { 9 | Text(Text), 10 | Element(Box AnyElement>), 11 | } 12 | 13 | pub struct Tooltip { 14 | content: TooltipContext, 15 | key_binding: Option, 16 | action: Option<(Box, Option)>, 17 | } 18 | 19 | impl Tooltip { 20 | /// Create a Tooltip with a text content. 21 | pub fn new(text: impl Into) -> Self { 22 | Self { 23 | content: TooltipContext::Text(text.into()), 24 | key_binding: None, 25 | action: None, 26 | } 27 | } 28 | 29 | /// Create a Tooltip with a custom element. 30 | pub fn element(builder: F) -> Self 31 | where 32 | E: IntoElement, 33 | F: Fn(&mut Window, &mut App) -> E + 'static, 34 | { 35 | Self { 36 | key_binding: None, 37 | action: None, 38 | content: TooltipContext::Element(Box::new(move |window, cx| { 39 | builder(window, cx).into_any_element() 40 | })), 41 | } 42 | } 43 | 44 | /// Set Action to display key binding information for the tooltip if it exists. 45 | pub fn action(mut self, action: &dyn Action, context: Option<&str>) -> Self { 46 | self.action = Some((action.boxed_clone(), context.map(SharedString::new))); 47 | self 48 | } 49 | 50 | /// Set KeyBinding information for the tooltip. 51 | pub fn key_binding(mut self, key_binding: Option) -> Self { 52 | self.key_binding = key_binding; 53 | self 54 | } 55 | 56 | /// Build the tooltip and return it as an `AnyView`. 57 | pub fn build(self, _: &mut Window, cx: &mut App) -> AnyView { 58 | cx.new(|_| self).into() 59 | } 60 | } 61 | 62 | impl FluentBuilder for Tooltip {} 63 | 64 | impl Render for Tooltip { 65 | fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { 66 | let key_binding = if let Some(key_binding) = &self.key_binding { 67 | Some(key_binding.clone()) 68 | } else { 69 | if let Some((action, context)) = &self.action { 70 | Kbd::binding_for_action( 71 | action.as_ref(), 72 | context.as_ref().map(|s| s.as_ref()), 73 | window, 74 | ) 75 | } else { 76 | None 77 | } 78 | }; 79 | 80 | div().child( 81 | // Wrap in a child, to ensure the left margin is applied to the tooltip 82 | h_flex() 83 | .font_family(".SystemUIFont") 84 | .m_3() 85 | .bg(cx.theme().popover) 86 | .text_color(cx.theme().popover_foreground) 87 | .bg(cx.theme().popover) 88 | .border_1() 89 | .border_color(cx.theme().border) 90 | .shadow_md() 91 | .rounded(px(6.)) 92 | .justify_between() 93 | .py_0p5() 94 | .px_2() 95 | .text_sm() 96 | .gap_3() 97 | .map(|this| { 98 | this.child(div().map(|this| match self.content { 99 | TooltipContext::Text(ref text) => this.child(text.clone()), 100 | TooltipContext::Element(ref builder) => this.child(builder(window, cx)), 101 | })) 102 | }) 103 | .when_some(key_binding, |this, kbd| { 104 | this.child( 105 | div() 106 | .text_xs() 107 | .flex_shrink_0() 108 | .text_color(cx.theme().muted_foreground) 109 | .child(kbd.appearance(false)), 110 | ) 111 | }), 112 | ) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1746461020, 24 | "narHash": "sha256-7+pG1I9jvxNlmln4YgnlW4o+w0TZX24k688mibiFDUE=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "3730d8a308f94996a9ba7c7138ede69c1b9ac4ae", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs_2": { 38 | "locked": { 39 | "lastModified": 1744536153, 40 | "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "NixOS", 48 | "ref": "nixpkgs-unstable", 49 | "repo": "nixpkgs", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "flake-utils": "flake-utils", 56 | "nixpkgs": "nixpkgs", 57 | "rust-overlay": "rust-overlay" 58 | } 59 | }, 60 | "rust-overlay": { 61 | "inputs": { 62 | "nixpkgs": "nixpkgs_2" 63 | }, 64 | "locked": { 65 | "lastModified": 1746585402, 66 | "narHash": "sha256-Pf+ufu6bYNA1+KQKHnGMNEfTwpD9ZIcAeLoE2yPWIP0=", 67 | "owner": "oxalica", 68 | "repo": "rust-overlay", 69 | "rev": "72dd969389583664f87aa348b3458f2813693617", 70 | "type": "github" 71 | }, 72 | "original": { 73 | "owner": "oxalica", 74 | "repo": "rust-overlay", 75 | "type": "github" 76 | } 77 | }, 78 | "systems": { 79 | "locked": { 80 | "lastModified": 1681028828, 81 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 82 | "owner": "nix-systems", 83 | "repo": "default", 84 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "nix-systems", 89 | "repo": "default", 90 | "type": "github" 91 | } 92 | } 93 | }, 94 | "root": "root", 95 | "version": 7 96 | } 97 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "gpui-component"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | rust-overlay.url = "github:oxalica/rust-overlay"; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: 11 | flake-utils.lib.eachDefaultSystem (system: 12 | let 13 | overlays = [ (import rust-overlay) ]; 14 | pkgs = import nixpkgs { 15 | inherit system overlays; 16 | }; 17 | in 18 | { 19 | devShells.default = with pkgs; mkShell { 20 | buildInputs = [ 21 | openssl 22 | pkg-config 23 | xorg.libX11 24 | glib 25 | pango 26 | atkmm 27 | gdk-pixbuf 28 | gtk3 29 | libsoup_3 30 | webkitgtk_4_1 31 | libxkbcommon 32 | vulkan-loader 33 | (rust-bin.beta.latest.default.override { 34 | extensions = [ "rust-src" ]; 35 | }) 36 | ]; 37 | 38 | env = { 39 | RUST_BACKTRACE = "1"; 40 | LD_LIBRARY_PATH = lib.makeLibraryPath [ vulkan-loader ]; 41 | }; 42 | }; 43 | } 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 4 | echo "Install Linux dependencies..." 5 | script/install-linux-deps 6 | else 7 | echo "Install macOS dependencies..." 8 | fi 9 | -------------------------------------------------------------------------------- /script/install-linux-deps: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo apt update 4 | # Test on Ubuntu 24.04 5 | sudo apt install -y \ 6 | gcc g++ clang libfontconfig-dev libwayland-dev \ 7 | libwebkit2gtk-4.1-dev libxkbcommon-x11-dev libx11-xcb-dev \ 8 | libssl-dev libzstd-dev \ 9 | vulkan-validationlayers libvulkan1 10 | --------------------------------------------------------------------------------