Project · focus that follows: segments, the tracker's action, tests
A segmented control too narrow for its segments now scrolls sideways in a strip of its own, and a segment that takes focus comes into view: its focus is a tab_stop handle, and layout::reveal_when_focused, now shared with the board's cards, measures it from a box that holds its ring. The DatabaseView's own strip goes. The time tracker's Start and Stop are one Button that keeps one handle through the new Button::focus_handle. A start moves focus to it, a stop hands focus back to the field, and the field starts on Enter's release, so the release no longer clicks the Stop that took focus. ProgressRing's percent refuses content inside, as content refuses a percent. Tests: Enter starts, Enter stops after 90 seconds, and the field takes the keys again; Skip moves on at once; a focused Timeline segment at 280px comes into view; the ring's refusal.
This commit is contained in:
1 parent
f70726476a
commit
384872cbf0
13 files changed
+294
-132
No files matched your search
+15
-40
@@ -1,14 +1,14 @@
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, Axis, Bounds, ElementId, EmptyView, Entity, EntityId, FocusHandle,
|
||||
InteractiveElement, IntoElement, ParentElement, Pixels, Point, RenderOnce, ScrollHandle,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window, anchored, canvas, div, prelude::*,
|
||||
AnyElement, App, Axis, Bounds, ElementId, EmptyView, EntityId, FocusHandle, InteractiveElement,
|
||||
IntoElement, ParentElement, Pixels, Point, RenderOnce, ScrollHandle, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window, anchored, canvas, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::moves::{landing, stepped};
|
||||
use crate::{
|
||||
layout::{bring_into_view, on_axis},
|
||||
layout::{on_axis, reveal_when_focused},
|
||||
motion::Flip,
|
||||
primitives::{FocusRing, raise, tab_stop},
|
||||
theme::{ActiveTheme, Elevation, Radius, TextSize},
|
||||
@@ -106,39 +106,6 @@ fn measure(
|
||||
.size_full()
|
||||
}
|
||||
|
||||
/// The card shown after focus, by key and column.
|
||||
type Revealed = Option<(SharedString, SharedString)>;
|
||||
|
||||
/// Scrolls the board so a card that takes focus shows, once per focus and again in a new column.
|
||||
fn reveal(
|
||||
scroll: &ScrollHandle,
|
||||
revealed: &Entity<Revealed>,
|
||||
focus: &FocusHandle,
|
||||
place: (SharedString, SharedString),
|
||||
) -> impl IntoElement {
|
||||
let (scroll, revealed, focus) = (scroll.clone(), revealed.clone(), focus.clone());
|
||||
canvas(
|
||||
move |bounds, window, cx| {
|
||||
let focused = focus.is_focused(window);
|
||||
let held = revealed.read(cx).clone();
|
||||
if focused && held.as_ref() != Some(&place) {
|
||||
revealed.update(cx, |revealed, _| *revealed = Some(place.clone()));
|
||||
if bring_into_view(&scroll, bounds, Axis::Horizontal) {
|
||||
log::info!("board: {} scrolled into view", place.0);
|
||||
window.request_animation_frame();
|
||||
}
|
||||
} else if !focused && held.is_some_and(|(key, _)| key == place.0) {
|
||||
revealed.update(cx, |revealed, _| *revealed = None);
|
||||
}
|
||||
},
|
||||
|_, _, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full()
|
||||
}
|
||||
|
||||
/// Columns of cards: a card drags to a place in any column, the others gliding aside, and Option with an arrow moves the focused one. Each move asks the owner through `on_move(card, column, index)`, the index counted without the card.
|
||||
#[derive(IntoElement)]
|
||||
pub struct KanbanBoard {
|
||||
@@ -203,7 +170,6 @@ impl RenderOnce for KanbanBoard {
|
||||
.use_keyed_state((id.clone(), "scroll"), cx, |_, _| ScrollHandle::new())
|
||||
.read(cx)
|
||||
.clone();
|
||||
let revealed = window.use_keyed_state((id.clone(), "revealed"), cx, |_, _| None);
|
||||
let owner = hold.entity_id();
|
||||
let stale = hold
|
||||
.read(cx)
|
||||
@@ -244,6 +210,15 @@ impl RenderOnce for KanbanBoard {
|
||||
.map(|column| column.cards.len())
|
||||
.collect(),
|
||||
);
|
||||
let mut reveals: HashMap<SharedString, AnyElement> = places
|
||||
.iter()
|
||||
.map(|(key, (column, _))| {
|
||||
let place = (id.clone(), format!("reveal-{key}-{}", keys[*column]));
|
||||
let reveal =
|
||||
reveal_when_focused(place, &scroll, &focus[key], Axis::Horizontal, window, cx);
|
||||
(key.clone(), reveal.into_any_element())
|
||||
})
|
||||
.collect();
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let width = theme.project().column;
|
||||
@@ -345,12 +320,12 @@ impl RenderOnce for KanbanBoard {
|
||||
measures.cards.insert(key.clone(), bounds) != Some(bounds)
|
||||
}
|
||||
}));
|
||||
let place = (key.clone(), column.key.clone());
|
||||
let reveal = reveals.remove(&key).expect("each card has a reveal");
|
||||
let row = div()
|
||||
.relative()
|
||||
.pb_2()
|
||||
.child(face)
|
||||
.child(reveal(&scroll, &revealed, &handle, place))
|
||||
.child(reveal)
|
||||
.child(measure(&row_measures, {
|
||||
let key = key.clone();
|
||||
move |measures, bounds| {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window, div,
|
||||
AnyElement, App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
|
||||
Window, div,
|
||||
};
|
||||
use jiff::civil::Date;
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::{
|
||||
calendar::{CalendarMonthView, Event},
|
||||
data_display::Tone,
|
||||
forms::OnValue,
|
||||
layout::{on_axis, seeded::use_seeded},
|
||||
layout::seeded::use_seeded,
|
||||
primitives::IconName,
|
||||
tables::{Cell, Column, DataTable, Row},
|
||||
theme::{ActiveTheme, ControlSize, TextSize},
|
||||
@@ -328,11 +328,7 @@ impl RenderOnce for DatabaseView {
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(
|
||||
on_axis(div().id((id.clone(), "switch")).overflow_x_scroll())
|
||||
.min_w_0()
|
||||
.child(switch),
|
||||
)
|
||||
.child(switch)
|
||||
.child(
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
|
||||
@@ -20,7 +20,7 @@ fn card(key: &str) -> KanbanCard {
|
||||
KanbanCard::new(SharedString::from(key.to_string()), face)
|
||||
}
|
||||
|
||||
fn board(owner: Entity<Desk>) -> AnyElement {
|
||||
fn board(_: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
let (moved, opened) = (owner.clone(), owner);
|
||||
KanbanBoard::new("board")
|
||||
.column(
|
||||
@@ -92,10 +92,10 @@ fn option_arrows_move_the_focused_card_and_enter_opens_it(cx: &mut TestAppContex
|
||||
);
|
||||
}
|
||||
|
||||
fn narrow(owner: Entity<Desk>) -> AnyElement {
|
||||
fn narrow(desk: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
gpui::div()
|
||||
.w(px(300.0))
|
||||
.child(board(owner))
|
||||
.child(board(desk, owner))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ fn a_focused_card_past_the_fold_scrolls_into_view(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
fn labels(owner: Entity<Desk>) -> AnyElement {
|
||||
fn labels(_: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
let (renamed, made) = (owner.clone(), owner);
|
||||
LabelManager::new(
|
||||
"labels",
|
||||
@@ -149,7 +149,7 @@ fn a_label_keeps_a_name_of_its_own(cx: &mut TestAppContext) {
|
||||
assert_eq!(heard(&host, cx), ["rename bug Defect", "make bug report"]);
|
||||
}
|
||||
|
||||
fn panel(owner: Entity<Desk>) -> AnyElement {
|
||||
fn panel(_: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
let task = Task::new("brief", "Write the brief").status(Status::Todo);
|
||||
TaskDetailPanel::new("panel", task, [Person::new("ana", "Ana Lima")])
|
||||
.on_change(move |task, _, cx| {
|
||||
@@ -187,7 +187,7 @@ fn the_panel_hands_on_the_task_after_each_edit(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
fn roadmap(_: Entity<Desk>) -> AnyElement {
|
||||
fn roadmap(_: &Desk, _: Entity<Desk>) -> AnyElement {
|
||||
crate::project::Roadmap::new("roadmap", date(2026, 9, 1), 2)
|
||||
.lane(
|
||||
"Design",
|
||||
|
||||
@@ -2,6 +2,9 @@ use gpui::{
|
||||
AnyElement, Context, Entity, IntoElement, KeyBinding, KeyUpEvent, Keystroke, Modifiers,
|
||||
ParentElement, Render, Styled, TestAppContext, VisualTestContext, Window, div, px,
|
||||
};
|
||||
use std::time::Instant;
|
||||
|
||||
use gpui::SharedString;
|
||||
use jiff::civil::date;
|
||||
|
||||
use super::{AssigneePicker, IssueId, Person, Priority, Status, StatusSelect, Task, TaskList};
|
||||
@@ -12,13 +15,14 @@ mod time;
|
||||
|
||||
/// A view that shows one project part and keeps what it heard.
|
||||
struct Desk {
|
||||
part: fn(Entity<Desk>) -> AnyElement,
|
||||
part: fn(&Desk, Entity<Desk>) -> AnyElement,
|
||||
heard: Vec<String>,
|
||||
clock: Option<(SharedString, Instant)>,
|
||||
}
|
||||
|
||||
impl Render for Desk {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().w(px(640.0)).child((self.part)(cx.entity()))
|
||||
div().w(px(640.0)).child((self.part)(self, cx.entity()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +35,7 @@ fn settle(cx: &mut VisualTestContext) {
|
||||
}
|
||||
|
||||
fn desk(
|
||||
part: fn(Entity<Desk>) -> AnyElement,
|
||||
part: fn(&Desk, Entity<Desk>) -> AnyElement,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Desk>, &mut VisualTestContext) {
|
||||
cx.update(|cx| {
|
||||
@@ -43,6 +47,7 @@ fn desk(
|
||||
let (host, cx) = cx.add_window_view(|_, _| Desk {
|
||||
part,
|
||||
heard: Vec::new(),
|
||||
clock: None,
|
||||
});
|
||||
cx.update(|window, _| window.activate_window());
|
||||
settle(cx);
|
||||
@@ -92,7 +97,7 @@ fn tasks() -> Vec<Task> {
|
||||
]
|
||||
}
|
||||
|
||||
fn list(owner: Entity<Desk>) -> AnyElement {
|
||||
fn list(_: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
let (opened, ticked) = (owner.clone(), owner);
|
||||
TaskList::new("tasks", tasks())
|
||||
.today(date(2026, 9, 27))
|
||||
@@ -128,7 +133,7 @@ fn enter_opens_the_task_under_the_cursor(cx: &mut TestAppContext) {
|
||||
assert_eq!(heard(&host, cx), ["open brief", "open specs"]);
|
||||
}
|
||||
|
||||
fn status(owner: Entity<Desk>) -> AnyElement {
|
||||
fn status(_: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
StatusSelect::new("status", Status::Todo)
|
||||
.on_change(move |status, _, cx| note(&owner, status.words().into(), cx))
|
||||
.into_any_element()
|
||||
@@ -143,7 +148,7 @@ fn a_status_picked_by_key_hands_on_its_status(cx: &mut TestAppContext) {
|
||||
assert_eq!(heard(&host, cx), ["In progress"]);
|
||||
}
|
||||
|
||||
fn assignee(owner: Entity<Desk>) -> AnyElement {
|
||||
fn assignee(_: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
let people = [
|
||||
Person::new("ana", "Ana Lima"),
|
||||
Person::new("ben", "Ben Ito"),
|
||||
@@ -169,7 +174,7 @@ fn the_picker_hands_on_a_person_or_no_one(cx: &mut TestAppContext) {
|
||||
assert_eq!(heard(&host, cx), ["Ben Ito", "no one"]);
|
||||
}
|
||||
|
||||
fn checklist(owner: Entity<Desk>) -> AnyElement {
|
||||
fn checklist(_: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
Checklist::new(
|
||||
"subtasks",
|
||||
"Subtasks",
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{AnyElement, Entity, IntoElement, TestAppContext};
|
||||
use gpui::{
|
||||
AnyElement, Entity, InteractiveElement, IntoElement, ParentElement, Styled, TestAppContext,
|
||||
};
|
||||
use jiff::civil::date;
|
||||
|
||||
use super::{Desk, desk, heard, note, press, settle, tab_to};
|
||||
use crate::project::{DatabaseView, PomodoroTimer, Status, Task, TimeEntry, TimeTracker};
|
||||
|
||||
fn pomodoro(owner: Entity<Desk>) -> AnyElement {
|
||||
fn pomodoro(_: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
PomodoroTimer::new("pomodoro")
|
||||
.minutes(2, 1, 3)
|
||||
.rounds(2)
|
||||
@@ -34,24 +36,62 @@ fn a_paused_phase_keeps_its_time_and_one_run_out_hands_on_the_next(cx: &mut Test
|
||||
assert_eq!(heard(&host, cx), ["Short break"]);
|
||||
}
|
||||
|
||||
fn tracker(owner: Entity<Desk>) -> AnyElement {
|
||||
fn tracker(desk: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
let entry = TimeEntry::new("e1", "Brief", Duration::from_secs(600));
|
||||
TimeTracker::new("tracker", [entry])
|
||||
.on_start(move |task, _, _, cx| note(&owner, format!("start {task}"), cx))
|
||||
.into_any_element()
|
||||
let (started, stopped) = (owner.clone(), owner);
|
||||
let tracker = TimeTracker::new("tracker", [entry])
|
||||
.on_start(move |task, at, _, cx| {
|
||||
started.update(cx, |desk, cx| {
|
||||
desk.heard.push(format!("start {task}"));
|
||||
desk.clock = Some((task.clone(), at));
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.on_stop(move |at, _, cx| {
|
||||
stopped.update(cx, |desk, cx| {
|
||||
let (task, since) = desk.clock.take().expect("a task runs");
|
||||
desk.heard.push(format!(
|
||||
"stop {task} {}s",
|
||||
at.duration_since(since).as_secs()
|
||||
));
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
match &desk.clock {
|
||||
Some((task, since)) => tracker.running(task.clone(), *since),
|
||||
None => tracker,
|
||||
}
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_named_task_starts_on_enter_and_a_blank_one_does_not(cx: &mut TestAppContext) {
|
||||
fn enter_starts_and_stops_and_the_field_takes_the_keys_again(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(tracker, cx);
|
||||
tab_to(1, cx);
|
||||
press("enter", cx);
|
||||
cx.simulate_input(" Site photos ");
|
||||
press("enter", cx);
|
||||
assert_eq!(heard(&host, cx), ["start Site photos"]);
|
||||
cx.executor().advance_clock(Duration::from_secs(90));
|
||||
settle(cx);
|
||||
press("enter", cx);
|
||||
cx.simulate_input("Budget");
|
||||
press("enter", cx);
|
||||
assert_eq!(
|
||||
heard(&host, cx),
|
||||
["start Site photos", "stop Site photos 90s", "start Budget"],
|
||||
"a blank start does nothing, and focus follows each turn"
|
||||
);
|
||||
}
|
||||
|
||||
fn database(owner: Entity<Desk>) -> AnyElement {
|
||||
#[gpui::test]
|
||||
fn skip_moves_on_at_once(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(pomodoro, cx);
|
||||
tab_to(3, cx);
|
||||
press("space", cx);
|
||||
assert_eq!(heard(&host, cx), ["Short break"]);
|
||||
}
|
||||
|
||||
fn database(_: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
let tasks = [
|
||||
Task::new("a", "Alpha")
|
||||
.starts(date(2026, 9, 20))
|
||||
@@ -84,3 +124,25 @@ fn the_switch_shows_each_way_and_the_board_moves_a_record(cx: &mut TestAppContex
|
||||
press("alt-right", cx);
|
||||
assert_eq!(heard(&host, cx), ["a In progress"]);
|
||||
}
|
||||
|
||||
fn narrow_database(desk: &Desk, owner: Entity<Desk>) -> AnyElement {
|
||||
gpui::div()
|
||||
.debug_selector(|| "narrow".into())
|
||||
.w(gpui::px(280.0))
|
||||
.child(database(desk, owner))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_focused_segment_past_the_fold_comes_into_view(cx: &mut TestAppContext) {
|
||||
let (_, cx) = desk(narrow_database, cx);
|
||||
tab_to(5, cx);
|
||||
let frame = cx.debug_bounds("narrow").expect("the box draws");
|
||||
let segment = cx
|
||||
.debug_bounds("segment Timeline")
|
||||
.expect("the segment draws");
|
||||
assert!(
|
||||
frame.left() <= segment.left() && segment.right() <= frame.right(),
|
||||
"Timeline at {segment:?} inside {frame:?}"
|
||||
);
|
||||
}
|
||||
+60
-42
@@ -11,7 +11,7 @@ use gpui::{
|
||||
use crate::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
forms::{Enter, Input, Run, TextInput},
|
||||
primitives::IconName,
|
||||
primitives::{IconName, tab_stop},
|
||||
theme::{ActiveTheme, TextSize},
|
||||
typography::{Ellipsis, format, tabular},
|
||||
};
|
||||
@@ -50,7 +50,7 @@ pub(crate) fn spent(duration: Duration) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// A timer against a task: a field names it, Start or Enter starts it and Stop ends it, the running time counting up; under it, the entries made and their total. The owner keeps what runs and what was spent.
|
||||
/// A timer against a task: a field names it, Start or Enter's release starts it and Stop ends it, the running time counting up; under it, the entries made and their total. The owner keeps what runs and what was spent.
|
||||
#[derive(IntoElement)]
|
||||
pub struct TimeTracker {
|
||||
id: ElementId,
|
||||
@@ -116,8 +116,10 @@ impl RenderOnce for TimeTracker {
|
||||
.running
|
||||
.as_ref()
|
||||
.map_or(Duration::ZERO, |(_, since)| now.duration_since(*since));
|
||||
let action = tab_stop((id.clone(), "action").into(), true, window, cx);
|
||||
let start: Run = {
|
||||
let (id, field, on_start) = (id.clone(), field.clone(), self.on_start);
|
||||
let (id, field, on_start, action) =
|
||||
(id.clone(), field.clone(), self.on_start, action.clone());
|
||||
Rc::new(move |window, cx| {
|
||||
let task = SharedString::from(field.read(cx).text().trim().to_string());
|
||||
if task.is_empty() {
|
||||
@@ -127,54 +129,70 @@ impl RenderOnce for TimeTracker {
|
||||
let now = cx.background_executor().now();
|
||||
log::info!("time tracker {id:?}: start {task}");
|
||||
field.update(cx, |input, cx| input.set_text(String::new(), cx));
|
||||
window.focus(&action);
|
||||
if let Some(on_start) = &on_start {
|
||||
on_start(&task, now, window, cx);
|
||||
}
|
||||
})
|
||||
};
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let (task, action) = match &self.running {
|
||||
Some((task, _)) => {
|
||||
let (id, on_stop) = (id.clone(), self.on_stop.clone());
|
||||
(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(Ellipsis::new(task.clone()))
|
||||
.into_any_element(),
|
||||
Button::new((self.id.clone(), "stop"), "Stop")
|
||||
.icon(IconName::Square)
|
||||
.on_click(move |_, window, cx| {
|
||||
let now = cx.background_executor().now();
|
||||
log::info!("time tracker {id:?}: stop");
|
||||
if let Some(on_stop) = &on_stop {
|
||||
on_stop(now, window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
let stop: Run = {
|
||||
let (id, on_stop, typing) = (
|
||||
id.clone(),
|
||||
self.on_stop.clone(),
|
||||
field.read(cx).focus().clone(),
|
||||
);
|
||||
Rc::new(move |window, cx| {
|
||||
let now = cx.background_executor().now();
|
||||
log::info!("time tracker {id:?}: stop");
|
||||
window.focus(&typing);
|
||||
if let Some(on_stop) = &on_stop {
|
||||
on_stop(now, window, cx);
|
||||
}
|
||||
})
|
||||
};
|
||||
let running = self.running.is_some();
|
||||
let task = match &self.running {
|
||||
Some((task, _)) => div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(Ellipsis::new(task.clone()))
|
||||
.into_any_element(),
|
||||
None => {
|
||||
let (typed, pressed) = (start.clone(), start);
|
||||
(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.capture_action(move |_: &Enter, window, cx| {
|
||||
cx.stop_propagation();
|
||||
let typed = start.clone();
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.capture_action(|_: &Enter, _, cx| cx.stop_propagation())
|
||||
.on_key_up(move |event, window, cx| {
|
||||
if event.keystroke.key == "enter" {
|
||||
typed(window, cx)
|
||||
})
|
||||
.child(Input::new(&field))
|
||||
.into_any_element(),
|
||||
Button::new((self.id.clone(), "start"), "Start")
|
||||
.variant(ButtonVariant::Primary)
|
||||
.icon(IconName::Play)
|
||||
.on_click(move |_, window, cx| pressed(window, cx))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(Input::new(&field))
|
||||
.into_any_element()
|
||||
}
|
||||
};
|
||||
let action = Button::new(
|
||||
(id.clone(), "action"),
|
||||
if running { "Stop" } else { "Start" },
|
||||
)
|
||||
.focus_handle(&action)
|
||||
.variant(if running {
|
||||
ButtonVariant::default()
|
||||
} else {
|
||||
ButtonVariant::Primary
|
||||
})
|
||||
.icon(if running {
|
||||
IconName::Square
|
||||
} else {
|
||||
IconName::Play
|
||||
})
|
||||
.on_click(move |_, window, cx| match running {
|
||||
true => stop(window, cx),
|
||||
false => start(window, cx),
|
||||
});
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let clock = tabular(div())
|
||||
.flex_none()
|
||||
.text_size(theme.text_size(TextSize::Lg))
|
||||
|
||||
Reference in new issue
Block a user