T05 buttons and actions: groups, toggles, segmented, confirm, copy, share, bars, sheet, quick actions
This commit is contained in:
29 files changed
+2659
-151
No files matched your search
@@ -100,6 +100,45 @@ pub fn number(window: &Window) -> Result<u32> {
|
||||
u32::try_from(number).context("window number out of range")
|
||||
}
|
||||
|
||||
/// This process's frontmost window above the normal level: a popover or menu.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn popup_number() -> Result<u32> {
|
||||
use core_foundation::{
|
||||
base::{CFType, TCFType},
|
||||
dictionary::CFDictionary,
|
||||
number::CFNumber,
|
||||
string::CFString,
|
||||
};
|
||||
use core_graphics::window;
|
||||
|
||||
let read = |dict: &CFDictionary<CFString, CFType>, key: &'static str| {
|
||||
dict.find(CFString::from_static_string(key))
|
||||
.and_then(|value| value.downcast::<CFNumber>())
|
||||
.and_then(|value| value.to_i64())
|
||||
};
|
||||
let pid = i64::from(std::process::id());
|
||||
let list = window::copy_window_info(
|
||||
window::kCGWindowListOptionOnScreenOnly,
|
||||
window::kCGNullWindowID,
|
||||
)
|
||||
.context("window list unavailable")?;
|
||||
let id = list
|
||||
.iter()
|
||||
.map(|item| unsafe { CFDictionary::<CFString, CFType>::wrap_under_get_rule(*item as _) })
|
||||
.find(|dict| {
|
||||
read(dict, "kCGWindowOwnerPID") == Some(pid)
|
||||
&& read(dict, "kCGWindowLayer").is_some_and(|layer| layer > 0)
|
||||
})
|
||||
.and_then(|dict| read(&dict, "kCGWindowNumber"))
|
||||
.context("no popup window of this process is on screen")?;
|
||||
u32::try_from(id).context("window number out of range")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn popup_number() -> Result<u32> {
|
||||
bail!("capture needs macOS window APIs")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn number(_: &Window) -> Result<u32> {
|
||||
bail!("capture needs macOS window APIs")
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
use ely_gpui_component::{
|
||||
buttons::{
|
||||
ActionBar, ActionSheet, BulkActionBar, Button, ButtonVariant, ConfirmButton, ConfirmMode,
|
||||
CopyButton, FloatingActionButton, QuickAction, QuickActions, ShareButton, back_button,
|
||||
close_button, more_button,
|
||||
},
|
||||
primitives::IconName,
|
||||
theme::{ActiveTheme, ControlSize, Radius},
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, prelude::*, px};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{code, row, section, specimen, specimens},
|
||||
};
|
||||
|
||||
pub fn later(cx: &App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"SplitButton / DropdownButton / MenuButton / LoadingButton",
|
||||
"Built from parts in later chapters.",
|
||||
cx,
|
||||
)
|
||||
.child(Caption::new(
|
||||
"Split and dropdown buttons open a Menu: they land with Menus, chapter 8.",
|
||||
))
|
||||
.child(Caption::new(
|
||||
"The loading state spins a Spinner: it lands with Loading, chapter 11.",
|
||||
))
|
||||
}
|
||||
|
||||
pub fn fab(cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
section(
|
||||
"FloatingActionButton",
|
||||
"Round and raised for the screen's one main action. It rises in.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.w(px(420.0))
|
||||
.h(px(160.0))
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.bg(theme.colors.sunken)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_4()
|
||||
.bottom_4()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(
|
||||
FloatingActionButton::new("fab-compose", IconName::Pencil).label("Compose"),
|
||||
)
|
||||
.child(
|
||||
FloatingActionButton::new("fab-add", IconName::Plus).tooltip("New item"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn confirm(cx: &App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"ConfirmButton / HoldToConfirm",
|
||||
"Hold until the fill completes, or click twice within three seconds.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
specimens()
|
||||
.child(specimen(
|
||||
"hold",
|
||||
probe(
|
||||
"confirm-hold",
|
||||
ConfirmButton::new("confirm-hold", "Hold to delete", ConfirmMode::Hold)
|
||||
.on_confirm(|_, _| log::info!("gallery: held to delete")),
|
||||
),
|
||||
cx,
|
||||
))
|
||||
.child(specimen(
|
||||
"twice",
|
||||
probe(
|
||||
"confirm-twice",
|
||||
ConfirmButton::new("confirm-twice", "Discard draft", ConfirmMode::Twice)
|
||||
.on_confirm(|_, _| log::info!("gallery: draft discarded")),
|
||||
),
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn small_buttons(cx: &App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"CopyButton / ShareButton / CloseButton / BackButton / MoreButton",
|
||||
"Small, familiar verbs. Each names itself on hover.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.child(specimen(
|
||||
"copy",
|
||||
probe(
|
||||
"copy-button",
|
||||
CopyButton::new("copy-invite", "https://ely.dev/invite/7Q2"),
|
||||
),
|
||||
cx,
|
||||
))
|
||||
.child(specimen(
|
||||
"share",
|
||||
probe(
|
||||
"share-button",
|
||||
ShareButton::new("share-page")
|
||||
.text("Ely, components for GPUI")
|
||||
.url("https://github.com/ZacharyZhang-NY/Ely-GPUI-Components"),
|
||||
),
|
||||
cx,
|
||||
))
|
||||
.child(specimen("close", close_button("preset-close"), cx))
|
||||
.child(specimen("back", back_button("preset-back"), cx))
|
||||
.child(specimen("more", more_button("preset-more"), cx)),
|
||||
)
|
||||
.child(code(
|
||||
"close_button(id), back_button(id), more_button(id)",
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn action_bar(cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
section(
|
||||
"ActionBar",
|
||||
"Docked along a panel's foot, or floating as a pill.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(560.0))
|
||||
.h(px(200.0))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_end()
|
||||
.items_center()
|
||||
.overflow_hidden()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.bg(theme.colors.sunken)
|
||||
.child(
|
||||
div().pb_4().child(
|
||||
ActionBar::new()
|
||||
.floating()
|
||||
.child(
|
||||
Button::new("bar-reply", "Reply")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(IconName::Reply),
|
||||
)
|
||||
.child(
|
||||
Button::new("bar-forward", "Forward")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(IconName::Forward),
|
||||
)
|
||||
.child(
|
||||
Button::new("bar-archive", "Archive")
|
||||
.primary()
|
||||
.icon(IconName::Archive),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
ActionBar::new()
|
||||
.child(Button::new("bar-cancel", "Cancel").variant(ButtonVariant::Ghost))
|
||||
.child(Button::new("bar-save", "Save changes").primary()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn bulk(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let picked = window.use_keyed_state("bulk-picked", cx, |_, _| 0usize);
|
||||
let count = *picked.read(cx);
|
||||
let (add, clear) = (picked.clone(), picked);
|
||||
section(
|
||||
"BulkActionBar",
|
||||
"Rises once rows are selected. The count rolls as it changes.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.child(probe(
|
||||
"bulk-select",
|
||||
Button::new("bulk-select", "Select one more")
|
||||
.variant(ButtonVariant::Outline)
|
||||
.on_click(move |_, _, cx| {
|
||||
add.update(cx, |count, cx| {
|
||||
*count += 1;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
))
|
||||
.child(Caption::new(format!("{count} selected"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.h(cx.theme().control_height(ControlSize::Lg) * 1.5)
|
||||
.child(
|
||||
BulkActionBar::new("bulk-bar", count)
|
||||
.action(
|
||||
Button::new("bulk-move", "Move")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(IconName::Folder),
|
||||
)
|
||||
.action(
|
||||
Button::new("bulk-delete", "Delete")
|
||||
.variant(ButtonVariant::Danger)
|
||||
.icon(IconName::Trash2),
|
||||
)
|
||||
.on_clear(move |_, cx| {
|
||||
clear.update(cx, |count, cx| {
|
||||
*count = 0;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn action_sheet(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let open = window.use_keyed_state("sheet-shown", cx, |_, _| false);
|
||||
let shown = *open.read(cx);
|
||||
let (show, hide) = (open.clone(), open);
|
||||
section(
|
||||
"ActionSheet",
|
||||
"A few choices from the bottom, and a separate way out.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"action-sheet-open",
|
||||
Button::new("action-sheet-open", "Photo options")
|
||||
.variant(ButtonVariant::Outline)
|
||||
.on_click(move |_, _, cx| {
|
||||
show.update(cx, |shown, cx| {
|
||||
*shown = true;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
))
|
||||
.when(shown, |block| {
|
||||
block.child(
|
||||
ActionSheet::new("photo-sheet", move |_, cx| {
|
||||
hide.update(cx, |shown, cx| {
|
||||
*shown = false;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.title("dunes.jpg")
|
||||
.message("Taken at dusk. 4.2 MB.")
|
||||
.action("Set as wallpaper", |_, _| log::info!("gallery: wallpaper"))
|
||||
.action("Duplicate", |_, _| log::info!("gallery: duplicate"))
|
||||
.destructive("Delete photo", |_, _| log::info!("gallery: delete photo")),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn quick_actions(cx: &App) -> impl IntoElement + use<> {
|
||||
let note = |label: &'static str| {
|
||||
move |_: &mut Window, _: &mut App| log::info!("gallery: quick {label}")
|
||||
};
|
||||
section(
|
||||
"QuickActions",
|
||||
"Common moves with their keys. Arrows choose, Enter runs.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"quick",
|
||||
div().w(px(420.0)).child(
|
||||
QuickActions::new("quick-actions")
|
||||
.action(
|
||||
QuickAction::new(IconName::FilePlus, "New document", note("new"))
|
||||
.note("Blank, or from a template")
|
||||
.shortcut("cmd-n"),
|
||||
)
|
||||
.action(
|
||||
QuickAction::new(IconName::FolderOpen, "Open…", note("open")).shortcut("cmd-o"),
|
||||
)
|
||||
.action(
|
||||
QuickAction::new(IconName::Search, "Search everywhere", note("search"))
|
||||
.note("Files, symbols, commands")
|
||||
.shortcut("cmd-shift-p"),
|
||||
)
|
||||
.action(
|
||||
QuickAction::new(IconName::Settings, "Settings", note("settings"))
|
||||
.shortcut("cmd-,"),
|
||||
),
|
||||
),
|
||||
))
|
||||
.child(Caption::new(SharedString::from(
|
||||
"Tab in, then use the arrows.",
|
||||
)))
|
||||
}
|
||||
@@ -1,21 +1,11 @@
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, ButtonVariant, IconButton},
|
||||
buttons::{Button, ButtonGroup, ButtonVariant, IconButton},
|
||||
primitives::IconName,
|
||||
theme::ControlSize,
|
||||
};
|
||||
use gpui::{AnyElement, App, ElementId, IntoElement, ParentElement, Styled, Window, div};
|
||||
use gpui::{AnyElement, App, ElementId, IntoElement, ParentElement, Styled, div};
|
||||
|
||||
use super::Page;
|
||||
use crate::ui::{row, section, specimen, specimens};
|
||||
|
||||
pub const PAGE: Page = Page {
|
||||
number: 5,
|
||||
slug: "buttons",
|
||||
title: "Buttons & Actions",
|
||||
summary: "Seven variants, three sizes, one focus ring.",
|
||||
render,
|
||||
script: &[],
|
||||
};
|
||||
use crate::ui::{code, row, section, specimen, specimens};
|
||||
|
||||
const VARIANTS: [(ButtonVariant, &str); 7] = [
|
||||
(ButtonVariant::Primary, "Primary"),
|
||||
@@ -27,7 +17,7 @@ const VARIANTS: [(ButtonVariant, &str); 7] = [
|
||||
(ButtonVariant::Link, "Link"),
|
||||
];
|
||||
|
||||
fn render(_: &mut Window, cx: &mut App) -> AnyElement {
|
||||
pub fn basics(cx: &App) -> AnyElement {
|
||||
let sizes = [
|
||||
(ControlSize::Sm, "Small"),
|
||||
(ControlSize::Md, "Medium"),
|
||||
@@ -128,5 +118,47 @@ fn render(_: &mut Window, cx: &mut App) -> AnyElement {
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"ShortcutHint",
|
||||
"The keystroke rides along, quiet, in the platform's spelling.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
row()
|
||||
.child(Button::new("hint-save", "Save").shortcut("cmd-s"))
|
||||
.child(
|
||||
Button::new("hint-find", "Find")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(IconName::Search)
|
||||
.shortcut("cmd-shift-f"),
|
||||
),
|
||||
)
|
||||
.child(code("Button::new(id, label).shortcut(\"cmd-s\")", cx)),
|
||||
)
|
||||
.child(
|
||||
section("ButtonGroup", "Buttons joined edge to edge.", cx).child(
|
||||
row()
|
||||
.child(
|
||||
ButtonGroup::new()
|
||||
.button(Button::new("group-day", "Day"))
|
||||
.button(Button::new("group-week", "Week"))
|
||||
.button(Button::new("group-month", "Month")),
|
||||
)
|
||||
.child(
|
||||
ButtonGroup::new()
|
||||
.button(
|
||||
Button::new("group-prev", "Previous")
|
||||
.variant(ButtonVariant::Outline)
|
||||
.icon(IconName::ChevronLeft),
|
||||
)
|
||||
.button(
|
||||
Button::new("group-next", "Next")
|
||||
.variant(ButtonVariant::Outline)
|
||||
.trailing_icon(IconName::ChevronRight),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
use ely_gpui_component::{
|
||||
buttons::{SegmentedControl, ToggleButton, ToggleGroup, ToggleItem},
|
||||
primitives::IconName,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{row, section, specimen, specimens},
|
||||
};
|
||||
|
||||
pub fn toggle_button(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let pinned = window.use_keyed_state("toggle-pinned", cx, |_, _| true);
|
||||
let muted = window.use_keyed_state("toggle-muted", cx, |_, _| false);
|
||||
let (pin_on, mute_on) = (*pinned.read(cx), *muted.read(cx));
|
||||
section("ToggleButton", "Stays pressed until pressed again.", cx).child(
|
||||
row()
|
||||
.child(
|
||||
ToggleButton::new(
|
||||
"toggle-pin",
|
||||
ToggleItem::new("pin").label("Pinned").icon(IconName::Pin),
|
||||
pin_on,
|
||||
)
|
||||
.on_toggle(move |on, _, cx| {
|
||||
pinned.update(cx, |pinned, cx| {
|
||||
*pinned = on;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new(
|
||||
"toggle-mute",
|
||||
ToggleItem::new("mute")
|
||||
.icon(IconName::VolumeX)
|
||||
.tooltip("Mute"),
|
||||
mute_on,
|
||||
)
|
||||
.on_toggle(move |on, _, cx| {
|
||||
muted.update(cx, |muted, cx| {
|
||||
*muted = on;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn choice(
|
||||
key: &'static str,
|
||||
initial: &'static [&'static str],
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> gpui::Entity<Vec<SharedString>> {
|
||||
window.use_keyed_state(key, cx, |_, _| {
|
||||
initial.iter().map(|v| SharedString::from(*v)).collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_group(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let marks = choice("toggle-marks", &["bold"], window, cx);
|
||||
let align = choice("toggle-align", &["left"], window, cx);
|
||||
let (marks_now, align_now) = (marks.read(cx).clone(), align.read(cx).clone());
|
||||
let store = |state: gpui::Entity<Vec<SharedString>>| {
|
||||
move |values: &[SharedString], _: &mut Window, cx: &mut App| {
|
||||
let values = values.to_vec();
|
||||
state.update(cx, |state, cx| {
|
||||
*state = values;
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
};
|
||||
section(
|
||||
"ToggleGroup",
|
||||
"Several at once for marks; one at a time for alignment.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
specimens()
|
||||
.child(specimen(
|
||||
"multiple",
|
||||
ToggleGroup::new("marks")
|
||||
.multiple()
|
||||
.item(ToggleItem::new("bold").icon(IconName::Bold).tooltip("Bold"))
|
||||
.item(
|
||||
ToggleItem::new("italic")
|
||||
.icon(IconName::Italic)
|
||||
.tooltip("Italic"),
|
||||
)
|
||||
.item(
|
||||
ToggleItem::new("underline")
|
||||
.icon(IconName::Underline)
|
||||
.tooltip("Underline"),
|
||||
)
|
||||
.selected(marks_now)
|
||||
.on_change(store(marks)),
|
||||
cx,
|
||||
))
|
||||
.child(specimen(
|
||||
"single",
|
||||
ToggleGroup::new("align")
|
||||
.item(
|
||||
ToggleItem::new("left")
|
||||
.label("Left")
|
||||
.icon(IconName::AlignLeft),
|
||||
)
|
||||
.item(
|
||||
ToggleItem::new("center")
|
||||
.label("Center")
|
||||
.icon(IconName::AlignCenter),
|
||||
)
|
||||
.item(
|
||||
ToggleItem::new("right")
|
||||
.label("Right")
|
||||
.icon(IconName::AlignRight),
|
||||
)
|
||||
.selected(align_now)
|
||||
.on_change(store(align)),
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn segmented(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let range = window.use_keyed_state("segment-range", cx, |_, _| SharedString::from("day"));
|
||||
let view = window.use_keyed_state("segment-view", cx, |_, _| SharedString::from("list"));
|
||||
let (range_now, view_now) = (range.read(cx).clone(), view.read(cx).clone());
|
||||
let store = |state: gpui::Entity<SharedString>| {
|
||||
move |value: &SharedString, _: &mut Window, cx: &mut App| {
|
||||
let value = value.clone();
|
||||
state.update(cx, |state, cx| {
|
||||
*state = value;
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
};
|
||||
section(
|
||||
"SegmentedControl",
|
||||
"One of a few. The thumb springs to the choice.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
specimens()
|
||||
.child(specimen(
|
||||
"text",
|
||||
probe(
|
||||
"segmented",
|
||||
div().w(px(300.0)).child(
|
||||
SegmentedControl::new("range", range_now)
|
||||
.segment("day", "Day", None)
|
||||
.segment("week", "Week", None)
|
||||
.segment("month", "Month", None)
|
||||
.on_change(store(range)),
|
||||
),
|
||||
),
|
||||
cx,
|
||||
))
|
||||
.child(specimen(
|
||||
"with icons",
|
||||
SegmentedControl::new("view", view_now)
|
||||
.segment("list", "List", Some(IconName::List))
|
||||
.segment("grid", "Grid", Some(IconName::LayoutGrid))
|
||||
.on_change(store(view)),
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
mod actions;
|
||||
mod basics;
|
||||
mod choices;
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
|
||||
|
||||
use super::Page;
|
||||
use crate::script::Step;
|
||||
|
||||
pub const PAGE: Page = Page {
|
||||
number: 5,
|
||||
slug: "buttons",
|
||||
title: "Buttons & Actions",
|
||||
summary: "Seven variants, three sizes, one focus ring, and the verbs around them.",
|
||||
render,
|
||||
script: SCRIPT,
|
||||
};
|
||||
|
||||
const SCRIPT: &[Step] = &[
|
||||
Step::Rest,
|
||||
Step::DownAt("segmented", 150.0, 16.0),
|
||||
Step::UpAt("segmented", 150.0, 16.0),
|
||||
Step::Wait(500),
|
||||
Step::Shot("segmented"),
|
||||
Step::DownAt("confirm-hold", 40.0, 14.0),
|
||||
Step::Wait(500),
|
||||
Step::Shot("holding"),
|
||||
Step::Wait(900),
|
||||
Step::Shot("held"),
|
||||
Step::UpAt("confirm-hold", 40.0, 14.0),
|
||||
Step::Click("confirm-twice"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("armed"),
|
||||
Step::Click("confirm-twice"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("confirmed"),
|
||||
Step::Wait(3200),
|
||||
Step::Click("confirm-twice"),
|
||||
Step::Click("confirm-twice"),
|
||||
Step::Click("confirm-twice"),
|
||||
Step::Wait(1600),
|
||||
Step::Shot("twice-reset"),
|
||||
Step::Click("copy-button"),
|
||||
Step::Wait(300),
|
||||
Step::Shot("copied"),
|
||||
Step::Click("share-button"),
|
||||
Step::Wait(900),
|
||||
Step::ShotPopup("share-picker"),
|
||||
Step::NativeKey("escape"),
|
||||
Step::Wait(400),
|
||||
Step::Click("bulk-select"),
|
||||
Step::Click("bulk-select"),
|
||||
Step::Click("bulk-select"),
|
||||
Step::Wait(500),
|
||||
Step::Shot("bulk"),
|
||||
Step::Click("action-sheet-open"),
|
||||
Step::Wait(400),
|
||||
Step::Key("tab"),
|
||||
Step::Key("tab"),
|
||||
Step::Key("tab"),
|
||||
Step::Key("tab"),
|
||||
Step::Key("tab"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("action-sheet"),
|
||||
Step::Key("escape"),
|
||||
Step::Click("quick"),
|
||||
Step::Key("down"),
|
||||
Step::Key("down"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("quick"),
|
||||
Step::Rest,
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div()
|
||||
.child(basics::basics(cx))
|
||||
.child(actions::later(cx))
|
||||
.child(choices::toggle_button(window, cx))
|
||||
.child(choices::toggle_group(window, cx))
|
||||
.child(choices::segmented(window, cx))
|
||||
.child(actions::fab(cx))
|
||||
.child(actions::confirm(cx))
|
||||
.child(actions::small_buttons(cx))
|
||||
.child(actions::action_bar(cx))
|
||||
.child(actions::bulk(window, cx))
|
||||
.child(actions::action_sheet(window, cx))
|
||||
.child(actions::quick_actions(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use anyhow::{Context as _, Result};
|
||||
use gpui::{AsyncApp, Bounds, Keystroke, Pixels, Point, WindowHandle, point, px};
|
||||
|
||||
use crate::{
|
||||
capture::{number, snapshot},
|
||||
capture::{number, popup_number, snapshot},
|
||||
probe::{Opened, Probes},
|
||||
shell::Gallery,
|
||||
};
|
||||
@@ -35,6 +35,10 @@ pub enum Step {
|
||||
CloseNative(&'static str),
|
||||
/// Fails unless the window is gone.
|
||||
ExpectClosed(&'static str),
|
||||
/// Photographs this app's frontmost popup, such as the share picker.
|
||||
ShotPopup(&'static str),
|
||||
/// Posts a key to AppKit itself; only `escape` is known.
|
||||
NativeKey(&'static str),
|
||||
}
|
||||
|
||||
const FRAME: Duration = Duration::from_millis(120);
|
||||
@@ -152,6 +156,16 @@ pub async fn play(
|
||||
let number = opened(key, cx)?.update(cx, |_, window, _| number(window))??;
|
||||
perform_close(number)?;
|
||||
}
|
||||
Step::ShotPopup(name) => {
|
||||
let file = path(name);
|
||||
snapshot(popup_number()?, &file)?;
|
||||
log::info!("script: wrote {}", file.display());
|
||||
}
|
||||
Step::NativeKey(key) => {
|
||||
anyhow::ensure!(key == "escape", "native key {key} is not known");
|
||||
let number = window.update(cx, |_, window, _| number(window))??;
|
||||
post_escape(number)?;
|
||||
}
|
||||
Step::ExpectClosed(key) => {
|
||||
let handle = opened(key, cx)?;
|
||||
let open = cx.update(|cx| cx.windows().contains(&handle))?;
|
||||
@@ -215,6 +229,40 @@ fn post(kind: Mouse, x: f64, y: f64, number: u32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queues an Escape key press on this app, for AppKit popovers.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn post_escape(number: u32) -> Result<()> {
|
||||
use cocoa::{
|
||||
appkit::{NSApp, NSApplication, NSEvent, NSEventModifierFlags, NSEventType},
|
||||
base::{NO, id, nil},
|
||||
foundation::{NSPoint, NSString},
|
||||
};
|
||||
unsafe {
|
||||
let escape = NSString::alloc(nil).init_str("\u{1b}");
|
||||
let event = <id as NSEvent>::keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode_(
|
||||
nil,
|
||||
NSEventType::NSKeyDown,
|
||||
NSPoint::new(0.0, 0.0),
|
||||
NSEventModifierFlags::empty(),
|
||||
0.0,
|
||||
i64::from(number),
|
||||
nil,
|
||||
escape,
|
||||
escape,
|
||||
NO,
|
||||
53,
|
||||
);
|
||||
anyhow::ensure!(event != nil, "AppKit refused a synthetic key event");
|
||||
NSApp().postEvent_atStart_(event, NO);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn post_escape(_: u32) -> Result<()> {
|
||||
anyhow::bail!("native keys need macOS")
|
||||
}
|
||||
|
||||
/// Runs `performClose:` on a window, which asks before closing.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn perform_close(number: u32) -> Result<()> {
|
||||
|
||||
Reference in new issue
Block a user