Charts · Navigation · Forms: band names widen a sideways chart's gutter; Pagination takes a size, speaks the locale and jumps to a typed page
- A horizontal chart's gutter grows to its widest band name, floored at the theme gutter and capped at half the chart; a name past the cap ends in an ellipsis (#10). - Pagination's previous and next tooltips read i18n (English and zh-CN built in, app catalogs override); `.size(ControlSize)` sizes every control, Sm by default; `.jump()` adds a Go to field (#11). - NumberInput keys the owner's value as shown: a draft survives a blur, a value set while focused lands when focus leaves, and an echo of its own commit is no new value. A ranged field holds room for its widest bound through `Input::least`.
This commit is contained in:
1 parent
2ce336895c
commit
bd3772ad54
17 files changed
+438
-51
No files matched your search
+71
-18
@@ -7,7 +7,8 @@ use gpui::{
|
||||
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant, IconButton},
|
||||
forms::Run,
|
||||
forms::{NumberInput, Run},
|
||||
i18n,
|
||||
primitives::IconName,
|
||||
theme::{ActiveTheme, ControlSize, TextSize},
|
||||
typography::format::{self, Separators},
|
||||
@@ -49,6 +50,8 @@ pub struct Pagination {
|
||||
page: usize,
|
||||
pages: usize,
|
||||
near: usize,
|
||||
size: ControlSize,
|
||||
jump: bool,
|
||||
on_change: Option<OnPage>,
|
||||
}
|
||||
|
||||
@@ -60,10 +63,23 @@ impl Pagination {
|
||||
page,
|
||||
pages,
|
||||
near: 1,
|
||||
size: ControlSize::Sm,
|
||||
jump: false,
|
||||
on_change: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: ControlSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// A field after Next that goes to the page typed.
|
||||
pub fn jump(mut self) -> Self {
|
||||
self.jump = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Pages shown on each side of the current one.
|
||||
pub fn near(mut self, near: usize) -> Self {
|
||||
self.near = near;
|
||||
@@ -89,16 +105,43 @@ impl RenderOnce for Pagination {
|
||||
}
|
||||
})
|
||||
};
|
||||
let subtle = cx.theme().colors.fg_subtle;
|
||||
let (back, ahead) = (go.clone(), go.clone());
|
||||
let theme = cx.theme();
|
||||
let (size, subtle, small) = (
|
||||
self.size,
|
||||
theme.colors.fg_subtle,
|
||||
theme.text_size(TextSize::Sm),
|
||||
);
|
||||
let (back, ahead, jump) = (go.clone(), go.clone(), go.clone());
|
||||
let to = i18n::text(cx, "pagination.jump", &[]);
|
||||
let jump = self.jump.then(|| {
|
||||
let words = div()
|
||||
.whitespace_nowrap()
|
||||
.text_size(small)
|
||||
.text_color(subtle)
|
||||
.child(to.clone());
|
||||
div().flex().items_center().gap_2().child(words).child(
|
||||
div().flex_none().child(
|
||||
NumberInput::new((self.id.clone(), "jump"), page as f64)
|
||||
.label(to)
|
||||
.range(1.0, pages as f64)
|
||||
.precision(0)
|
||||
.size(size)
|
||||
.on_commit(move |to, window, cx| {
|
||||
if to as usize != page {
|
||||
jump(to as usize, window, cx);
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
});
|
||||
let cells = list
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(move |(ix, entry)| match entry {
|
||||
Some(number) => {
|
||||
let go = go.clone();
|
||||
Button::new(("page", number), number.to_string())
|
||||
.size(ControlSize::Sm)
|
||||
let button = Button::new(("page", number), number.to_string())
|
||||
.size(size)
|
||||
.variant(if number == page {
|
||||
ButtonVariant::Secondary
|
||||
} else {
|
||||
@@ -108,7 +151,10 @@ impl RenderOnce for Pagination {
|
||||
if number != page {
|
||||
go(number, window, cx);
|
||||
}
|
||||
})
|
||||
});
|
||||
div()
|
||||
.debug_selector(move || format!("pagination-page-{number}"))
|
||||
.child(button)
|
||||
.into_any_element()
|
||||
}
|
||||
None => div()
|
||||
@@ -120,26 +166,33 @@ impl RenderOnce for Pagination {
|
||||
});
|
||||
div()
|
||||
.id(self.id)
|
||||
.debug_selector(|| "pagination-root".into())
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("previous", IconName::ChevronLeft)
|
||||
.size(ControlSize::Sm)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.tooltip("Previous page")
|
||||
.disabled(page == 1)
|
||||
.on_click(move |_, window, cx| back(page - 1, window, cx)),
|
||||
div().debug_selector(|| "pagination-previous".into()).child(
|
||||
IconButton::new("previous", IconName::ChevronLeft)
|
||||
.size(size)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.tooltip(i18n::text(cx, "pagination.previous", &[]))
|
||||
.disabled(page == 1)
|
||||
.on_click(move |_, window, cx| back(page - 1, window, cx)),
|
||||
),
|
||||
)
|
||||
.children(cells)
|
||||
.child(
|
||||
IconButton::new("next", IconName::ChevronRight)
|
||||
.size(ControlSize::Sm)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.tooltip("Next page")
|
||||
.disabled(page == pages)
|
||||
.on_click(move |_, window, cx| ahead(page + 1, window, cx)),
|
||||
div().debug_selector(|| "pagination-next".into()).child(
|
||||
IconButton::new("next", IconName::ChevronRight)
|
||||
.size(size)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.tooltip(i18n::text(cx, "pagination.next", &[]))
|
||||
.disabled(page == pages)
|
||||
.on_click(move |_, window, cx| ahead(page + 1, window, cx)),
|
||||
),
|
||||
)
|
||||
.children(jump)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod pages;
|
||||
mod tabs;
|
||||
|
||||
use gpui::{
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
use gpui::{
|
||||
Context, IntoElement, ParentElement, Render, Styled, TestAppContext, Window, div,
|
||||
prelude::FluentBuilder,
|
||||
};
|
||||
|
||||
use super::setup;
|
||||
use crate::{
|
||||
navigation::Pagination,
|
||||
theme::{ActiveTheme, ControlSize, TextSize},
|
||||
typography::text_width,
|
||||
};
|
||||
|
||||
struct Paged {
|
||||
size: ControlSize,
|
||||
page: usize,
|
||||
jump: bool,
|
||||
picked: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Render for Paged {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
let text = cx.theme().text_size(TextSize::Base);
|
||||
div().text_size(text).child(
|
||||
Pagination::new("pages", self.page, 100)
|
||||
.size(self.size)
|
||||
.when(self.jump, Pagination::jump)
|
||||
.on_change(move |to, _, cx| {
|
||||
view.update(cx, |paged, cx| {
|
||||
paged.picked.push(to);
|
||||
paged.page = to;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_jump_field_goes_to_the_page_typed_within_range(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Paged {
|
||||
size: ControlSize::Sm,
|
||||
page: 3,
|
||||
jump: true,
|
||||
picked: Vec::new(),
|
||||
});
|
||||
cx.update(|window, cx| {
|
||||
window.activate_window();
|
||||
window.focus_prev(cx);
|
||||
});
|
||||
for typed in ["78", "500"] {
|
||||
cx.simulate_keystrokes("secondary-a");
|
||||
cx.simulate_input(typed);
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.run_until_parked();
|
||||
}
|
||||
cx.simulate_keystrokes("secondary-a");
|
||||
cx.simulate_input("88");
|
||||
cx.update(|window, cx| window.blur(cx));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
view.read_with(cx, |paged, _| paged.picked.clone()),
|
||||
[78, 100, 88]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn every_control_takes_the_size_asked(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
for size in [ControlSize::Sm, ControlSize::Md, ControlSize::Lg] {
|
||||
let (_, cx) = cx.add_window_view(move |_, _| Paged {
|
||||
size,
|
||||
page: 3,
|
||||
jump: false,
|
||||
picked: Vec::new(),
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let tall =
|
||||
cx.update(|window, cx| cx.theme().control_height(size).to_pixels(window.rem_size()));
|
||||
for control in [
|
||||
"pagination-previous",
|
||||
"pagination-page-3",
|
||||
"pagination-page-4",
|
||||
"pagination-next",
|
||||
] {
|
||||
let bounds = cx.debug_bounds(control).expect("drawn");
|
||||
assert_eq!(bounds.size.height, tall, "{control} at {size:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_jump_field_holds_the_last_page_at_every_size(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let sizes = [
|
||||
(ControlSize::Sm, TextSize::Sm),
|
||||
(ControlSize::Md, TextSize::Base),
|
||||
(ControlSize::Lg, TextSize::Md),
|
||||
];
|
||||
for (size, text) in sizes {
|
||||
let (_, cx) = cx.add_window_view(move |_, _| Paged {
|
||||
size,
|
||||
page: 100,
|
||||
jump: true,
|
||||
picked: Vec::new(),
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let slot = cx.debug_bounds("input-text").expect("the field draws");
|
||||
let digits = cx.update(|window, cx| {
|
||||
let text = cx.theme().text_size(text);
|
||||
text_width("100", text.to_pixels(window.rem_size()), window)
|
||||
});
|
||||
assert!(
|
||||
slot.size.width >= digits,
|
||||
"{size:?}: {slot:?} under {digits:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user