Data display · a tag stays inside its container
A Tag is capped at its container's width, and its label ends in an ellipsis beside a fixed remove mark, so a long name in a narrow tag field stays inside the field instead of running past it.
This commit is contained in:
1 parent
ccd48f9ba2
commit
14b5faa80c
4 files changed
+43
-8
No files matched your search
@@ -119,7 +119,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- Paths with `/` between folders become a tree through `lists::Folder::of`, for `FileTree` and `files::ArchiveViewer` alike.
|
||||
- A folder's path over its entries is `lists::path_bar`, and its first order is `lists::listed`, folders first by name: DirectoryListing and FileExplorer share both, so every view shows one path and one order.
|
||||
- A file's icon comes from `primitives::file_icon`, one map from name to icon for every chapter; `files::FileIcon` draws it. What fits across a width comes from `layout::columns_for`, and `layout::fit` adds the inset that centers it.
|
||||
- Badges, tags and avatars live in `data_display`; forms and shell draw theirs from there, and `data_display` imports neither. Avatars take square pictures.
|
||||
- Badges, tags and avatars live in `data_display`; forms and shell draw theirs from there, and `data_display` imports neither. Avatars take square pictures. A tag is never wider than its container; a long label ends in an ellipsis beside its remove mark.
|
||||
- A picture drawn in code reaches gpui as a `RenderImage`. A host that replaces one frees the old through `App::drop_image`, or every frame stays in the sprite atlas (the gallery's model).
|
||||
- `img()` keeps loading state, and moves an animated picture's frames, only with an id. Content masks are rectangles, so rounded corners survive only when the image fills its box without cropping.
|
||||
- gpui reads a `&str` picture source as a web address. A file goes through `Path` (`documents::source` in the crate, `Path::new` in the gallery).
|
||||
|
||||
@@ -9,7 +9,7 @@ use gpui::{
|
||||
use crate::{
|
||||
primitives::{Icon, IconName, Severity},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Palette, Radius, TextSize},
|
||||
typography::AnimatedNumber,
|
||||
typography::{AnimatedNumber, Ellipsis},
|
||||
};
|
||||
|
||||
/// How a badge or a tag is tinted: quiet, the accent, or a severity.
|
||||
@@ -246,7 +246,7 @@ impl RenderOnce for DotBadge {
|
||||
|
||||
type OnRemove = Rc<dyn Fn(&mut Window, &mut App)>;
|
||||
|
||||
/// A short label for a kind, a topic or a filter: an optional icon, the label, and an x when it can be removed.
|
||||
/// A short label for a kind, a topic or a filter: an optional icon, the label, and an x when it can be removed. It is never wider than its container; a long label ends in an ellipsis.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Tag {
|
||||
id: ElementId,
|
||||
@@ -292,10 +292,12 @@ impl RenderOnce for Tag {
|
||||
Tone::Neutral => (colors.hover, colors.fg),
|
||||
other => other.colors(colors),
|
||||
};
|
||||
let remove = self.id.clone();
|
||||
let (remove, named) = (self.id.clone(), self.id.clone());
|
||||
div()
|
||||
.debug_selector(move || format!("tag {named}"))
|
||||
.flex()
|
||||
.flex_none()
|
||||
.max_w_full()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.pl_2()
|
||||
@@ -307,13 +309,18 @@ impl RenderOnce for Tag {
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(text)
|
||||
.when_some(self.icon, |tag, icon| {
|
||||
tag.child(Icon::new(icon).size(IconSize::Xs).color(text))
|
||||
tag.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.child(Icon::new(icon).size(IconSize::Xs).color(text)),
|
||||
)
|
||||
})
|
||||
.child(self.label)
|
||||
.child(div().flex_1().min_w_0().child(Ellipsis::new(self.label)))
|
||||
.when_some(self.on_remove, |tag, remove_it| {
|
||||
tag.child(
|
||||
div()
|
||||
.id(remove)
|
||||
.flex_none()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(colors.active))
|
||||
|
||||
@@ -206,8 +206,10 @@ impl RenderOnce for TagInput {
|
||||
}
|
||||
.render(window, cx)
|
||||
});
|
||||
let field = self.id.clone();
|
||||
div()
|
||||
.id(self.id)
|
||||
.debug_selector(move || format!("tag-input {field}"))
|
||||
.relative()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
|
||||
+28
-2
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
Context, Entity, IntoElement, KeyUpEvent, Keystroke, Render, SharedString, TestAppContext,
|
||||
VisualTestContext, Window,
|
||||
Context, Entity, IntoElement, KeyUpEvent, Keystroke, ParentElement, Render, SharedString,
|
||||
Styled, TestAppContext, VisualTestContext, Window, div, px,
|
||||
};
|
||||
|
||||
use super::setup;
|
||||
@@ -109,3 +109,29 @@ fn escape_sets_the_suggestions_aside(cx: &mut TestAppContext) {
|
||||
press("enter", cx);
|
||||
assert_eq!(asked(&host, cx), [vec![SharedString::from("be")]]);
|
||||
}
|
||||
|
||||
/// A narrow field holding one address whose name runs long.
|
||||
struct Narrow;
|
||||
|
||||
impl Render for Narrow {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.w(px(200.0))
|
||||
.child(
|
||||
TagInput::new("to", ["ana@atrium.studio"]).suggestions([Choice::new(
|
||||
"ana@atrium.studio",
|
||||
"Anastasia Lima-Fernández de la Torre, of the Atrium studio",
|
||||
)]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_long_chip_ends_inside_its_field(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (_, cx) = cx.add_window_view(|_, _| Narrow);
|
||||
settle(cx);
|
||||
let chip = cx.debug_bounds("tag to-tag-0").expect("the chip draws");
|
||||
let field = cx.debug_bounds("tag-input to").expect("the field draws");
|
||||
assert!(chip.right() <= field.right(), "{chip:?} inside {field:?}");
|
||||
}
|
||||
Reference in new issue
Block a user