Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/test-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
pull_request:
paths:
- "website/**"
- ".github/workflows/test-docs.yml"
- "crates/base/**"
- "crates/story-web/**"

Expand All @@ -12,15 +13,28 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Checkout latest approved showcases
uses: actions/checkout@v4
with:
repository: longbridge/gpui-kit-showcases
ref: main
path: .showcases
persist-credentials: false
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.6
bun-version: 1.4.0
- name: Install Showcase validation and rendering dependencies
working-directory: .showcases
run: bun install --frozen-lockfile
- name: Install
working-directory: website
run: bun install
- name: Build
working-directory: website
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SHOWCASES_DIR: ${{ github.workspace }}/.showcases
run: |
bun run test:showcases
bun run build
bun run test:seo
13 changes: 11 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"crates/kit",
"crates/base",
"crates/base/examples/wasm",
"crates/base/examples/native",
"crates/component-macros",
"crates/story",
"crates/story-web",
Expand Down
8 changes: 3 additions & 5 deletions crates/base/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ description = "Behavior, interaction, and infrastructure foundations for GPUI ap
keywords = ["desktop", "gpui", "ui", "foundation"]
license = "Apache-2.0"
name = "gpui-base"
# The showcase binary and examples are development tools that read fixtures
# from `crates/story`; they stay out of the published package.
exclude = ["src/bin/", "examples/"]
# The examples are development tools that read fixtures from `crates/story`;
# they stay out of the published package.
exclude = ["examples/"]
repository = "https://github.com/longbridge/gpui-kit"
homepage = "https://gpui-kit.com"
documentation = "https://docs.rs/gpui-base"
Expand Down Expand Up @@ -45,11 +45,9 @@ sum-tree.workspace = true
tracing.workspace = true
unicode-segmentation = "1.12.0"
web-time = "1"
syntect.workspace = true

[target.'cfg(not(target_family = "wasm"))'.dependencies]
smol.workspace = true
gpui_platform.workspace = true

[target.'cfg(target_family = "wasm")'.dependencies]
async-channel = "2.3.1"
Expand Down
2 changes: 1 addition & 1 deletion crates/base/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ Foundation controls do not install animation automatically. Applications choose
See the [Motion guide](../../website/base/motion.md) and run its five focused interactive demonstrations with:

```bash
cargo run -p gpui-base --example motion
cargo run -p gpui-base-examples --bin motion
```

### Themes and Styles
Expand Down
10 changes: 5 additions & 5 deletions crates/base/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@ WebAssembly previews exercise the same Rust code without producing one binary pe
Run an individual component natively:

```bash
cargo run -p gpui-base --example components -- button
cargo run -p gpui-base --example components -- alert-dialog
cargo run -p gpui-base --example components -- virtual-list
cargo run -p gpui-base-examples -- button
cargo run -p gpui-base-examples -- alert-dialog
cargo run -p gpui-base-examples -- virtual-list
```

Run without a component slug to show the overview:

```bash
cargo run -p gpui-base --example components
cargo run -p gpui-base-examples
```

Motion has a separate example because it demonstrates continuous behavior rather than a component catalog entry. It contains focused pages for transitions, springs, keyframes, presence, and stagger:

```bash
cargo run -p gpui-base --example motion
cargo run -p gpui-base-examples --bin motion
```

The website builds `examples/wasm`, which imports the same `showcase/mod.rs` and selects the
Expand Down
21 changes: 21 additions & 0 deletions crates/base/examples/native/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "gpui-base-examples"
default-run = "components"
version = "0.6.0"
publish = false
edition.workspace = true

[dependencies]
gpui.workspace = true
gpui-base = { path = "../.." }
gpui_platform.workspace = true
reqwest_client.workspace = true
syntect.workspace = true

[lints]
workspace = true

# The shared showcase is included with `#[path]`, which cargo-machete cannot
# follow when detecting its `gpui`, `gpui_base`, and `syntect` imports.
[package.metadata.cargo-machete]
ignored = ["gpui", "gpui-base", "syntect"]
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#[path = "../../../showcase/mod.rs"]
mod showcase;

use std::sync::Arc;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#[path = "motion/mod.rs"]
#[path = "../../../motion/mod.rs"]
mod app;

fn main() {
Expand Down
10 changes: 0 additions & 10 deletions crates/base/src/bin/components.rs

This file was deleted.

129 changes: 119 additions & 10 deletions crates/base/src/input/base/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ use crate::input::{
};
use crate::{AutoScroll, StepAction};

/// Vertical clearance to retain when revealing a text position.
pub(crate) enum ScrollPadding {
Minimal,
SurroundingLines,
}

#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = input, no_json)]
pub struct Enter {
Expand Down Expand Up @@ -1873,6 +1879,23 @@ impl<M: InputModeKind> InputBaseState<M> {
offset: usize,
direction: Option<MoveDirection>,
cx: &mut Context<Self>,
) {
let padding = if direction.is_some() {
ScrollPadding::SurroundingLines
} else {
ScrollPadding::Minimal
};
self.scroll_to_with_padding(offset, direction, padding, cx);
}

/// Reveal an offset with independently chosen direction restriction and padding.
/// Search uses surrounding lines without restricting movement to match order.
pub(crate) fn scroll_to_with_padding(
&mut self,
offset: usize,
direction: Option<MoveDirection>,
padding: ScrollPadding,
cx: &mut Context<Self>,
) {
let Some(last_layout) = self.last_layout.as_ref() else {
return;
Expand Down Expand Up @@ -1921,16 +1944,17 @@ impl<M: InputModeKind> InputBaseState<M> {
// `TextElement::layout_cursor` so both scroll-into-view paths agree
// (a mismatch flickered on `Down` at end-of-buffer with a small
// `cursor_surrounding_lines` override).
let edge_height = if direction.is_some() && self.is_code_editor() {
super::element::cursor_surrounding_padding(
self.mode.is_auto_grow(),
self.cursor_surrounding_lines,
last_layout.visible_range.len(),
line_height,
)
} else {
line_height
};
let edge_height =
if matches!(padding, ScrollPadding::SurroundingLines) && self.is_code_editor() {
super::element::cursor_surrounding_padding(
self.mode.is_auto_grow(),
self.cursor_surrounding_lines,
last_layout.visible_range.len(),
line_height,
)
} else {
line_height
};
if row_offset_y - edge_height + line_height < -scroll_offset.y {
// Scroll up
scroll_offset.y = -row_offset_y + edge_height - line_height;
Expand Down Expand Up @@ -3454,6 +3478,91 @@ mod tests {
});
}

#[gpui::test]
fn test_next_search_match_reveals_with_padding_after_manual_scroll(cx: &mut TestAppContext) {
assert_search_reveals_with_padding_after_manual_scroll(false, cx);
}

#[gpui::test]
fn test_previous_search_match_reveals_with_padding_after_manual_scroll(
cx: &mut TestAppContext,
) {
assert_search_reveals_with_padding_after_manual_scroll(true, cx);
}

fn assert_search_reveals_with_padding_after_manual_scroll(
previous: bool,
cx: &mut TestAppContext,
) {
let input_view = InputView::new(cx);
let mut cx = VisualTestContext::from_window(input_view.window_handle.into(), cx);
let input = input_view.input;
let text = (0..160)
.map(|row| {
if matches!(row, 20 | 60 | 100) {
format!("match on row {row}")
} else {
format!("line {row}")
}
})
.collect::<Vec<_>>()
.join("\n");
let start = text.match_indices("match").nth(1).unwrap().0;
let expected_match = start..start + "match".len();
cx.update(|window, cx| {
input.update(cx, |state, cx| {
state.set_cursor_surrounding_lines(Some(3), window, cx);
state.set_value(text, window, cx);
state.set_search_query("match", true, cx);
if previous {
state.search_session.matcher.next();
state.search_session.matcher.next();
}
});
});
cx.run_until_parked();
cx.update(|_, cx| {
input.update(cx, |state, cx| {
let line_height = state.last_layout.as_ref().unwrap().line_height;
let y = if previous { px(0.) } else { -line_height * 80. };
state.set_scroll_offset(point(px(0.), y), cx);
});
});
cx.run_until_parked();
input.read_with(&cx, |state, _| {
let visible = state.visible_row_range().unwrap();
if previous {
assert!(visible.end <= 60, "target must be below the viewport");
} else {
assert!(visible.start > 60, "target must be above the viewport");
}
});
cx.update(|_, cx| {
input.update(cx, |state, cx| {
let range = if previous {
state.previous_search_match(cx)
} else {
state.next_search_match(cx)
};
assert_eq!(range, Some(expected_match));
assert_eq!(state.search_session.matcher.label(), "2/3");
});
});
cx.run_until_parked();
input.read_with(&cx, |state, _| {
assert!(state.visible_row_range().unwrap().contains(&60));
let line_height = state.last_layout.as_ref().unwrap().line_height;
let target_y = line_height * 60. + state.scroll_handle.offset().y;
// Three lines of edge clearance include the matched line itself.
assert!(target_y >= line_height * 2. - px(0.1));
assert!(
target_y + line_height * 3.
<= state.last_bounds.as_ref().unwrap().size.height + px(0.1),
"search must preserve the configured surrounding-line padding"
);
});
}

#[gpui::test]
fn test_number_step(cx: &mut TestAppContext) {
let input = InputView::build(cx, |state| state).input;
Expand Down
Loading