DB & Dev Tools · Databases: narrow boxes scroll or wrap; the form's rules tested
QueryPlanViewer's step column takes the label width and the table scrolls sideways past its least width; TableStructureEditor's rows stretch to a least width that scrolls sideways; RedisKeyBrowser's columns wrap below each other inside 280px. The form's rules are pure, parse_port and ready, with unit tests; gpui tests cover saving, a new connection refilling the form, another engine bringing its port, and a press on a row's button leaving the row alone.
This commit is contained in:
1 parent
c137ddb0f7
commit
0c8e0d47ba
8 files changed
+214
-46
No files matched your search
@@ -384,3 +384,4 @@ Per-component lines live in `tasks/`. Tags there:
|
|||||||
| T32d | 1 | FAIL | Tab in a mind map's rename field added a topic; the replaced wire, the connector released on its own shape and a deleted shape's links went untested |
|
| T32d | 1 | FAIL | Tab in a mind map's rename field added a topic; the replaced wire, the connector released on its own shape and a deleted shape's links went untested |
|
||||||
| T32d | 2 | FAIL | Enter or Escape in a canvas field handed focus nowhere, so the map's and the layer's keys went dead until the next press |
|
| T32d | 2 | FAIL | Enter or Escape in a canvas field handed focus nowhere, so the map's and the layer's keys went dead until the next press |
|
||||||
| T32d | 3 | PASS | |
|
| T32d | 3 | PASS | |
|
||||||
|
| T33a | 1 | FAIL | the plan, the Redis browser and the structure editor drew past a 280px box; the form's rules and a row button's press went untested |
|
||||||
@@ -270,8 +270,7 @@ pub fn structure(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
|
|||||||
.flex_wrap()
|
.flex_wrap()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.child(
|
.child(
|
||||||
div().w(px(620.)).child(
|
div().w(px(660.)).child(TableStructureEditor::new("devtools-structure", fields)
|
||||||
TableStructureEditor::new("devtools-structure", fields)
|
|
||||||
.on_change(move |next, _, cx| change(&edited, cx, |build| build.fields = next)),
|
.on_change(move |next, _, cx| change(&edited, cx, |build| build.fields = next)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,15 +24,23 @@ pub enum TestState {
|
|||||||
Failed(SharedString),
|
Failed(SharedString),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The port a field names, from 1 to 65535.
|
/// The port `text` names, from 1 to 65535.
|
||||||
|
pub(crate) fn parse_port(text: &str) -> Option<u16> {
|
||||||
|
text.trim().parse().ok().filter(|port| *port > 0)
|
||||||
|
}
|
||||||
|
|
||||||
fn port(field: &Entity<TextInput>, cx: &App) -> Option<u16> {
|
fn port(field: &Entity<TextInput>, cx: &App) -> Option<u16> {
|
||||||
field
|
parse_port(field.read(cx).text())
|
||||||
.read(cx)
|
}
|
||||||
.text()
|
|
||||||
.trim()
|
/// Whether a connection is filled enough to test or save: a name, and a file for SQLite, or a host and a port for the rest.
|
||||||
.parse()
|
pub(crate) fn ready(name: &str, engine: Engine, host: &str, database: &str, port: &str) -> bool {
|
||||||
.ok()
|
let filled = |text: &str| !text.trim().is_empty();
|
||||||
.filter(|port| *port > 0)
|
filled(name)
|
||||||
|
&& match engine {
|
||||||
|
Engine::Sqlite => filled(database),
|
||||||
|
_ => filled(host) && parse_port(port).is_some(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The form's own fields, filled from the connection named by `seed`.
|
/// The form's own fields, filled from the connection named by `seed`.
|
||||||
@@ -162,9 +170,15 @@ impl RenderOnce for ConnectionForm {
|
|||||||
now.port.clone(),
|
now.port.clone(),
|
||||||
);
|
);
|
||||||
let file = engine == Engine::Sqlite;
|
let file = engine == Engine::Sqlite;
|
||||||
let filled = |input: &Entity<TextInput>| !input.read(cx).text().trim().is_empty();
|
let text = |input: &Entity<TextInput>| input.read(cx).text().to_string();
|
||||||
let good_port = file || port(&port_field, cx).is_some();
|
let good_port = file || port(&port_field, cx).is_some();
|
||||||
let ready = filled(&name) && filled(if file { &database } else { &host }) && good_port;
|
let ready = ready(
|
||||||
|
&text(&name),
|
||||||
|
engine,
|
||||||
|
&text(&host),
|
||||||
|
&text(&database),
|
||||||
|
&text(&port_field),
|
||||||
|
);
|
||||||
let testing = self.test == Some(TestState::Testing);
|
let testing = self.test == Some(TestState::Testing);
|
||||||
let (chosen, switched) = (fields.clone(), fields.clone());
|
let (chosen, switched) = (fields.clone(), fields.clone());
|
||||||
let field = |key: &'static str, label: &'static str, control: gpui::AnyElement| {
|
let field = |key: &'static str, label: &'static str, control: gpui::AnyElement| {
|
||||||
@@ -280,3 +294,40 @@ impl RenderOnce for ConnectionForm {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{parse_port, ready};
|
||||||
|
use crate::devtools::Engine;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_port_runs_from_one_to_65535() {
|
||||||
|
assert_eq!(parse_port("5433"), Some(5433));
|
||||||
|
assert_eq!(parse_port(" 22 "), Some(22));
|
||||||
|
assert_eq!(parse_port("0"), None);
|
||||||
|
assert_eq!(parse_port("70000"), None);
|
||||||
|
assert_eq!(parse_port("http"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_connection_waits_for_a_name_and_a_place() {
|
||||||
|
assert!(ready("Shop", Engine::Postgres, "localhost", "", "5432"));
|
||||||
|
assert!(
|
||||||
|
!ready("", Engine::Postgres, "localhost", "", "5432"),
|
||||||
|
"no name"
|
||||||
|
);
|
||||||
|
assert!(!ready("Shop", Engine::Postgres, "", "", "5432"), "no host");
|
||||||
|
assert!(
|
||||||
|
!ready("Shop", Engine::Postgres, "localhost", "", "0"),
|
||||||
|
"a bad port"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!ready("Notes", Engine::Sqlite, "localhost", "", "5432"),
|
||||||
|
"SQLite waits for a file"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ready("Notes", Engine::Sqlite, "", "notes.db", ""),
|
||||||
|
"and needs nothing else"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-7
@@ -1,12 +1,14 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
|
App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
|
||||||
|
StatefulInteractiveElement, Styled, Window, div, rems,
|
||||||
};
|
};
|
||||||
use jiff::Timestamp;
|
use jiff::Timestamp;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
forms::{Input, TextInput},
|
forms::{Input, TextInput},
|
||||||
|
layout::on_axis,
|
||||||
lists::{ListItem, SelectableList},
|
lists::{ListItem, SelectableList},
|
||||||
primitives::{Icon, IconName},
|
primitives::{Icon, IconName},
|
||||||
tables::{Cell, Column, TreeRow, TreeTable},
|
tables::{Cell, Column, TreeRow, TreeTable},
|
||||||
@@ -179,7 +181,7 @@ fn row(step: &PlanStep, whole: f64) -> TreeRow {
|
|||||||
.children(step.children.iter().map(|child| row(child, whole)))
|
.children(step.children.iter().map(|child| row(child, whole)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A query's plan as a tree of steps, each with its share of the whole cost drawn as a bar, the rows it yields and its time. Every step starts open.
|
/// A query's plan as a tree of steps, each with its share of the whole cost drawn as a bar, the rows it yields and its time. Every step starts open; a box narrower than its columns scrolls sideways.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct QueryPlanViewer {
|
pub struct QueryPlanViewer {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
@@ -203,20 +205,22 @@ fn keys(step: &PlanStep, out: &mut Vec<SharedString>) {
|
|||||||
|
|
||||||
impl RenderOnce for QueryPlanViewer {
|
impl RenderOnce for QueryPlanViewer {
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let step = gpui::rems(cx.theme().label_width().0 * 2.0);
|
let wide = cx.theme().label_width();
|
||||||
let mut open = Vec::new();
|
let mut open = Vec::new();
|
||||||
keys(&self.plan, &mut open);
|
keys(&self.plan, &mut open);
|
||||||
TreeTable::new(
|
let table = TreeTable::new(
|
||||||
self.id,
|
(self.id.clone(), "table"),
|
||||||
[
|
[
|
||||||
Column::new("step", "Step").width(step),
|
Column::new("step", "Step").width(wide),
|
||||||
Column::new("cost", "Cost").unsorted(),
|
Column::new("cost", "Cost").unsorted(),
|
||||||
Column::new("rows", "Rows").end(),
|
Column::new("rows", "Rows").end(),
|
||||||
Column::new("ms", "Time").end().decimals(1).suffix(" ms"),
|
Column::new("ms", "Time").end().decimals(1).suffix(" ms"),
|
||||||
],
|
],
|
||||||
[row(&self.plan, self.plan.cost)],
|
[row(&self.plan, self.plan.cost)],
|
||||||
)
|
)
|
||||||
.open(open)
|
.open(open);
|
||||||
|
on_axis(div().id((self.id, "sideways")).overflow_x_scroll())
|
||||||
|
.child(div().min_w(rems(wide.0 * 2.5)).child(table))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ impl RenderOnce for RedisKeyBrowser {
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w(theme.label_width() * 2.)
|
.min_w(theme.label_width())
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
@@ -248,7 +248,7 @@ impl RenderOnce for RedisKeyBrowser {
|
|||||||
.child(format!("{} of {} keys", found.len(), self.keys.len()))
|
.child(format!("{} of {} keys", found.len(), self.keys.len()))
|
||||||
.child(list),
|
.child(list),
|
||||||
)
|
)
|
||||||
.child(div().flex_1().min_w(theme.label_width() * 2.).child(detail))
|
.child(div().flex_1().min_w(theme.label_width()).child(detail))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-10
@@ -1,8 +1,9 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, ElementId, Entity, FontWeight, IntoElement, ParentElement, RenderOnce,
|
App, AppContext as _, ElementId, Entity, FontWeight, InteractiveElement, IntoElement,
|
||||||
SharedString, Styled, Window, div, relative,
|
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div,
|
||||||
|
relative, rems,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::schema::{Field, FieldKey};
|
use super::schema::{Field, FieldKey};
|
||||||
@@ -11,6 +12,7 @@ use crate::{
|
|||||||
canvas::{OnEdit, editing},
|
canvas::{OnEdit, editing},
|
||||||
data_display::{Badge, Tone},
|
data_display::{Badge, Tone},
|
||||||
forms::{Checkbox, Choice, InlineEdit, Input, MultiSelect, Select, Switch, TextInput},
|
forms::{Checkbox, Choice, InlineEdit, Input, MultiSelect, Select, Switch, TextInput},
|
||||||
|
layout::on_axis,
|
||||||
primitives::IconName,
|
primitives::IconName,
|
||||||
theme::{ActiveTheme, TextSize},
|
theme::{ActiveTheme, TextSize},
|
||||||
typography::Ellipsis,
|
typography::Ellipsis,
|
||||||
@@ -34,9 +36,10 @@ const TYPES: [&str; 10] = [
|
|||||||
const SHARES: [f32; 6] = [0.28, 0.24, 0.1, 0.1, 0.2, 0.08];
|
const SHARES: [f32; 6] = [0.28, 0.24, 0.1, 0.1, 0.2, 0.08];
|
||||||
|
|
||||||
fn cells(parts: [gpui::AnyElement; 6]) -> gpui::Div {
|
fn cells(parts: [gpui::AnyElement; 6]) -> gpui::Div {
|
||||||
parts.into_iter().zip(SHARES).fold(
|
parts
|
||||||
div().flex().items_center().w_full(),
|
.into_iter()
|
||||||
|row, (part, share)| {
|
.zip(SHARES)
|
||||||
|
.fold(div().flex().items_center(), |row, (part, share)| {
|
||||||
row.child(
|
row.child(
|
||||||
div()
|
div()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
@@ -45,11 +48,10 @@ fn cells(parts: [gpui::AnyElement; 6]) -> gpui::Div {
|
|||||||
.pr_2()
|
.pr_2()
|
||||||
.child(part),
|
.child(part),
|
||||||
)
|
)
|
||||||
},
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A table's fields to edit in place: a name and a default rewritten on a press, a type from the common ones, whether it may be empty, and the primary key. A foreign key shows where it points. Minus drops a field and Add field appends one; each edit hands the owner every field.
|
/// A table's fields to edit in place, scrolling sideways in a box too narrow for their columns: a name and a default rewritten on a press, a type from the common ones, whether it may be empty, and the primary key. A foreign key shows where it points. Minus drops a field and Add field appends one; each edit hands the owner every field.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct TableStructureEditor {
|
pub struct TableStructureEditor {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
@@ -180,12 +182,22 @@ impl RenderOnce for TableStructureEditor {
|
|||||||
all.push(Field::new(name, "text").nullable());
|
all.push(Field::new(name, "text").nullable());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
let least = rems(theme.label_width().0 * 4.0);
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(head)
|
.child(
|
||||||
.children(rows)
|
on_axis(div().id((id.clone(), "sideways")).overflow_x_scroll()).child(
|
||||||
|
div()
|
||||||
|
.min_w(least)
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(head)
|
||||||
|
.children(rows),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div().flex().child(
|
div().flex().child(
|
||||||
Button::new((id, "add"), "Add field")
|
Button::new((id, "add"), "Add field")
|
||||||
|
|||||||
+75
-12
@@ -1,20 +1,25 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Context, Entity, IntoElement, KeyBinding, ParentElement, Render, Styled,
|
AnyElement, App, Context, Entity, IntoElement, KeyBinding, ParentElement, Render, Styled,
|
||||||
TestAppContext, VisualTestContext, Window, div, px,
|
TestAppContext, VisualTestContext, Window, div, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Connection, ConnectionManager, Engine, Field, TableStructureEditor};
|
use super::{Connection, ConnectionForm, ConnectionManager, Engine, Field, TableStructureEditor};
|
||||||
use crate::{forms, primitives::FocusNext, theme::Theme};
|
use crate::{forms, primitives::FocusNext, theme::Theme};
|
||||||
|
|
||||||
/// A view that shows one tool and keeps the words it heard.
|
type Part = fn(&Bench, &mut Window, &mut App, Entity<Bench>) -> AnyElement;
|
||||||
|
|
||||||
|
/// A view that shows one tool, keeps the words it heard, and holds a choice among its part's cases.
|
||||||
struct Bench {
|
struct Bench {
|
||||||
part: fn(Entity<Bench>) -> AnyElement,
|
part: Part,
|
||||||
said: Vec<String>,
|
said: Vec<String>,
|
||||||
|
choice: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Bench {
|
impl Render for Bench {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div().w(px(640.0)).child((self.part)(cx.entity()))
|
let owner = cx.entity();
|
||||||
|
let part = (self.part)(self, window, cx, owner);
|
||||||
|
div().w(px(640.0)).child(part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,10 +31,7 @@ fn settle(cx: &mut VisualTestContext) {
|
|||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bench(
|
fn bench(part: Part, cx: &mut TestAppContext) -> (Entity<Bench>, &mut VisualTestContext) {
|
||||||
part: fn(Entity<Bench>) -> AnyElement,
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
) -> (Entity<Bench>, &mut VisualTestContext) {
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
Theme::init(cx);
|
Theme::init(cx);
|
||||||
Theme::update(cx, |theme| theme.reduced_motion = true);
|
Theme::update(cx, |theme| theme.reduced_motion = true);
|
||||||
@@ -39,6 +41,7 @@ fn bench(
|
|||||||
let (host, cx) = cx.add_window_view(|_, _| Bench {
|
let (host, cx) = cx.add_window_view(|_, _| Bench {
|
||||||
part,
|
part,
|
||||||
said: Vec::new(),
|
said: Vec::new(),
|
||||||
|
choice: 0,
|
||||||
});
|
});
|
||||||
settle(cx);
|
settle(cx);
|
||||||
(host, cx)
|
(host, cx)
|
||||||
@@ -71,7 +74,7 @@ fn tap(key: &str, cx: &mut VisualTestContext) {
|
|||||||
settle(cx);
|
settle(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manager(owner: Entity<Bench>) -> AnyElement {
|
fn manager(_: &Bench, _: &mut Window, _: &mut App, owner: Entity<Bench>) -> AnyElement {
|
||||||
let [opened, edited, picked] = [(); 3].map(|_| owner.clone());
|
let [opened, edited, picked] = [(); 3].map(|_| owner.clone());
|
||||||
ConnectionManager::new(
|
ConnectionManager::new(
|
||||||
"connections",
|
"connections",
|
||||||
@@ -97,7 +100,7 @@ fn enter_opens_a_connection_and_a_rows_button_leaves_the_row_alone(cx: &mut Test
|
|||||||
assert_eq!(said(&host, cx), ["open local", "edit local"]);
|
assert_eq!(said(&host, cx), ["open local", "edit local"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn structure(owner: Entity<Bench>) -> AnyElement {
|
fn structure(_: &Bench, _: &mut Window, _: &mut App, owner: Entity<Bench>) -> AnyElement {
|
||||||
TableStructureEditor::new("structure", [Field::new("id", "int").primary()])
|
TableStructureEditor::new("structure", [Field::new("id", "int").primary()])
|
||||||
.on_change(move |fields, _, cx| {
|
.on_change(move |fields, _, cx| {
|
||||||
let names: Vec<String> = fields
|
let names: Vec<String> = fields
|
||||||
@@ -131,3 +134,63 @@ fn an_address_names_the_host_or_the_file() {
|
|||||||
file.database = "app.db".into();
|
file.database = "app.db".into();
|
||||||
assert_eq!(file.address(), "app.db");
|
assert_eq!(file.address(), "app.db");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn form(bench: &Bench, _: &mut Window, _: &mut App, owner: Entity<Bench>) -> AnyElement {
|
||||||
|
let connection = match bench.choice {
|
||||||
|
0 => {
|
||||||
|
let mut shop = Connection::new("shop", "Shop", Engine::Postgres);
|
||||||
|
shop.database = "shop".into();
|
||||||
|
shop
|
||||||
|
}
|
||||||
|
_ => Connection::new("cache", "Cache", Engine::Redis),
|
||||||
|
};
|
||||||
|
ConnectionForm::new("form", connection)
|
||||||
|
.on_save(move |connection, _, cx| {
|
||||||
|
say(
|
||||||
|
&owner,
|
||||||
|
format!("save {} {}", connection.name, connection.address()),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save is the form's last stop, so a step back from nothing reaches it.
|
||||||
|
fn last(cx: &mut VisualTestContext) {
|
||||||
|
cx.update(|window, _| {
|
||||||
|
window.blur();
|
||||||
|
window.focus_prev();
|
||||||
|
});
|
||||||
|
settle(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn save_takes_the_form_and_a_new_connection_refills_it(cx: &mut TestAppContext) {
|
||||||
|
let (host, cx) = bench(form, cx);
|
||||||
|
last(cx);
|
||||||
|
tap("space", cx);
|
||||||
|
host.update(cx, |bench, cx| {
|
||||||
|
bench.choice = 1;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
settle(cx);
|
||||||
|
last(cx);
|
||||||
|
tap("space", cx);
|
||||||
|
assert_eq!(
|
||||||
|
said(&host, cx),
|
||||||
|
["save Shop localhost:5432/shop", "save Cache localhost:6379"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops: the name, then the engine.
|
||||||
|
#[gpui::test]
|
||||||
|
fn another_engine_brings_its_port(cx: &mut TestAppContext) {
|
||||||
|
let (host, cx) = bench(form, cx);
|
||||||
|
tab(2, cx);
|
||||||
|
for key in ["down", "down", "enter"] {
|
||||||
|
tap(key, cx);
|
||||||
|
}
|
||||||
|
last(cx);
|
||||||
|
tap("space", cx);
|
||||||
|
assert_eq!(said(&host, cx), ["save Shop localhost:3306/shop"]);
|
||||||
|
}
|
||||||
+41
-3
@@ -4,9 +4,9 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Context, Entity, IntoElement, Modifiers, ParentElement, Pixels, Point, Render, ScrollDelta,
|
Context, Entity, InteractiveElement, IntoElement, Modifiers, ParentElement, Pixels, Point,
|
||||||
ScrollWheelEvent, SharedString, Styled, TestAppContext, TouchPhase, VisualTestContext, Window,
|
Render, ScrollDelta, ScrollWheelEvent, SharedString, StatefulInteractiveElement, Styled,
|
||||||
div, point, px,
|
TestAppContext, TouchPhase, VisualTestContext, Window, div, point, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -431,3 +431,41 @@ fn a_narrow_listing_keeps_room_for_the_name(cx: &mut TestAppContext) {
|
|||||||
});
|
});
|
||||||
assert!(name.size.width >= least, "{name:?} under {least:?}");
|
assert!(name.size.width >= least, "{name:?} under {least:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A list whose row holds a button kept by `row_action`, which keeps what the list reports and what the button heard.
|
||||||
|
struct Held(Vec<SharedString>, Rc<Cell<usize>>);
|
||||||
|
|
||||||
|
impl Render for Held {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let (view, pressed) = (cx.entity(), self.1.clone());
|
||||||
|
let button = div()
|
||||||
|
.id("held-button")
|
||||||
|
.debug_selector(|| "held-button".into())
|
||||||
|
.size(px(24.0))
|
||||||
|
.on_click(move |_, _, _| pressed.set(pressed.get() + 1));
|
||||||
|
SelectableList::new("held")
|
||||||
|
.row(
|
||||||
|
"a",
|
||||||
|
ListItem::new("a", "A").trailing(super::row_action(button)),
|
||||||
|
)
|
||||||
|
.on_change(move |keys, _, cx| view.update(cx, |held, _| held.0 = keys.to_vec()))
|
||||||
|
.w(px(240.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_press_on_a_rows_button_leaves_the_row_alone(cx: &mut TestAppContext) {
|
||||||
|
setup(cx);
|
||||||
|
let pressed = Rc::new(Cell::new(0));
|
||||||
|
let heard = pressed.clone();
|
||||||
|
let (view, cx) = cx.add_window_view(move |_, _| Held(Vec::new(), heard));
|
||||||
|
settle(cx);
|
||||||
|
let button = cx.debug_bounds("held-button").expect("the row's button");
|
||||||
|
cx.simulate_click(button.center(), Modifiers::none());
|
||||||
|
settle(cx);
|
||||||
|
assert_eq!(pressed.get(), 1, "the button heard its press");
|
||||||
|
assert!(
|
||||||
|
view.read_with(cx, |held, _| held.0.is_empty()),
|
||||||
|
"the row stayed unselected"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user