Vendor gpui-pre 0.3.8 unmodified (crates.io source)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ba5c0798cb
commit
48a66ca6c1
162 files changed
+104826
-2
No files matched your search
+426
@@ -0,0 +1,426 @@
|
||||
use crate::{Action, App, Platform, SharedString};
|
||||
|
||||
/// A menu of the application, either a main menu or a submenu
|
||||
pub struct Menu {
|
||||
/// The name of the menu
|
||||
pub name: SharedString,
|
||||
|
||||
/// The items in the menu
|
||||
pub items: Vec<MenuItem>,
|
||||
|
||||
/// Whether this menu is disabled
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
impl Menu {
|
||||
/// Create a new Menu with the given name
|
||||
pub fn new(name: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
items: vec![],
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set items to be in this menu
|
||||
pub fn items(mut self, items: impl IntoIterator<Item = MenuItem>) -> Self {
|
||||
self.items = items.into_iter().collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether this menu is disabled
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create an OwnedMenu from this Menu
|
||||
pub fn owned(self) -> OwnedMenu {
|
||||
OwnedMenu {
|
||||
name: self.name.to_string().into(),
|
||||
items: self.items.into_iter().map(|item| item.owned()).collect(),
|
||||
disabled: self.disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// OS menus are menus that are recognized by the operating system
|
||||
/// This allows the operating system to provide specialized items for
|
||||
/// these menus
|
||||
pub struct OsMenu {
|
||||
/// The name of the menu
|
||||
pub name: SharedString,
|
||||
|
||||
/// The type of menu
|
||||
pub menu_type: SystemMenuType,
|
||||
}
|
||||
|
||||
impl OsMenu {
|
||||
/// Create an OwnedOsMenu from this OsMenu
|
||||
pub fn owned(self) -> OwnedOsMenu {
|
||||
OwnedOsMenu {
|
||||
name: self.name.to_string().into(),
|
||||
menu_type: self.menu_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of system menu
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum SystemMenuType {
|
||||
/// The 'Services' menu in the Application menu on macOS
|
||||
Services,
|
||||
}
|
||||
|
||||
/// The different kinds of items that can be in a menu
|
||||
pub enum MenuItem {
|
||||
/// A separator between items
|
||||
Separator,
|
||||
|
||||
/// A submenu
|
||||
Submenu(Menu),
|
||||
|
||||
/// A menu, managed by the system (for example, the Services menu on macOS)
|
||||
SystemMenu(OsMenu),
|
||||
|
||||
/// An action that can be performed
|
||||
Action {
|
||||
/// The name of this menu item
|
||||
name: SharedString,
|
||||
|
||||
/// The action to perform when this menu item is selected
|
||||
action: Box<dyn Action>,
|
||||
|
||||
/// The OS Action that corresponds to this action, if any
|
||||
/// See [`OsAction`] for more information
|
||||
os_action: Option<OsAction>,
|
||||
|
||||
/// Whether this action is checked
|
||||
checked: bool,
|
||||
|
||||
/// Whether this action is disabled
|
||||
disabled: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl MenuItem {
|
||||
/// Creates a new menu item that is a separator
|
||||
pub fn separator() -> Self {
|
||||
Self::Separator
|
||||
}
|
||||
|
||||
/// Creates a new menu item that is a submenu
|
||||
pub fn submenu(menu: Menu) -> Self {
|
||||
Self::Submenu(menu)
|
||||
}
|
||||
|
||||
/// Creates a new submenu that is populated by the OS
|
||||
pub fn os_submenu(name: impl Into<SharedString>, menu_type: SystemMenuType) -> Self {
|
||||
Self::SystemMenu(OsMenu {
|
||||
name: name.into(),
|
||||
menu_type,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new menu item that invokes an action
|
||||
pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self {
|
||||
Self::Action {
|
||||
name: name.into(),
|
||||
action: Box::new(action),
|
||||
os_action: None,
|
||||
checked: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new menu item that invokes an action and has an OS action
|
||||
pub fn os_action(
|
||||
name: impl Into<SharedString>,
|
||||
action: impl Action,
|
||||
os_action: OsAction,
|
||||
) -> Self {
|
||||
Self::Action {
|
||||
name: name.into(),
|
||||
action: Box::new(action),
|
||||
os_action: Some(os_action),
|
||||
checked: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an OwnedMenuItem from this MenuItem
|
||||
pub fn owned(self) -> OwnedMenuItem {
|
||||
match self {
|
||||
MenuItem::Separator => OwnedMenuItem::Separator,
|
||||
MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()),
|
||||
MenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
checked,
|
||||
disabled,
|
||||
} => OwnedMenuItem::Action {
|
||||
name: name.into(),
|
||||
action,
|
||||
os_action,
|
||||
checked,
|
||||
disabled,
|
||||
},
|
||||
MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set whether this menu item is checked
|
||||
///
|
||||
/// Only for [`MenuItem::Action`], otherwise, will be ignored
|
||||
pub fn checked(mut self, checked: bool) -> Self {
|
||||
match &mut self {
|
||||
MenuItem::Action { checked: old, .. } => {
|
||||
*old = checked;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether this menu item is checked
|
||||
///
|
||||
/// Only for [`MenuItem::Action`], otherwise, returns false
|
||||
#[inline]
|
||||
pub fn is_checked(&self) -> bool {
|
||||
match self {
|
||||
MenuItem::Action { checked, .. } => *checked,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set whether this menu item is disabled
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
match &mut self {
|
||||
MenuItem::Action { disabled: old, .. } => {
|
||||
*old = disabled;
|
||||
}
|
||||
MenuItem::Submenu(submenu) => {
|
||||
submenu.disabled = disabled;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether this menu item is disabled
|
||||
///
|
||||
/// Only for [`MenuItem::Action`] and [`MenuItem::Submenu`], otherwise, returns false
|
||||
#[inline]
|
||||
pub fn is_disabled(&self) -> bool {
|
||||
match self {
|
||||
MenuItem::Action { disabled, .. } => *disabled,
|
||||
MenuItem::Submenu(submenu) => submenu.disabled,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// OS menus are menus that are recognized by the operating system
|
||||
/// This allows the operating system to provide specialized items for
|
||||
/// these menus
|
||||
#[derive(Clone)]
|
||||
pub struct OwnedOsMenu {
|
||||
/// The name of the menu
|
||||
pub name: SharedString,
|
||||
|
||||
/// The type of menu
|
||||
pub menu_type: SystemMenuType,
|
||||
}
|
||||
|
||||
/// A menu of the application, either a main menu or a submenu
|
||||
#[derive(Clone)]
|
||||
pub struct OwnedMenu {
|
||||
/// The name of the menu
|
||||
pub name: SharedString,
|
||||
|
||||
/// The items in the menu
|
||||
pub items: Vec<OwnedMenuItem>,
|
||||
|
||||
/// Whether this menu is disabled
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
/// The different kinds of items that can be in a menu
|
||||
pub enum OwnedMenuItem {
|
||||
/// A separator between items
|
||||
Separator,
|
||||
|
||||
/// A submenu
|
||||
Submenu(OwnedMenu),
|
||||
|
||||
/// A menu, managed by the system (for example, the Services menu on macOS)
|
||||
SystemMenu(OwnedOsMenu),
|
||||
|
||||
/// An action that can be performed
|
||||
Action {
|
||||
/// The name of this menu item
|
||||
name: String,
|
||||
|
||||
/// The action to perform when this menu item is selected
|
||||
action: Box<dyn Action>,
|
||||
|
||||
/// The OS Action that corresponds to this action, if any
|
||||
/// See [`OsAction`] for more information
|
||||
os_action: Option<OsAction>,
|
||||
|
||||
/// Whether this action is checked
|
||||
checked: bool,
|
||||
|
||||
/// Whether this action is disabled
|
||||
disabled: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Clone for OwnedMenuItem {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
OwnedMenuItem::Separator => OwnedMenuItem::Separator,
|
||||
OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()),
|
||||
OwnedMenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
checked,
|
||||
disabled,
|
||||
} => OwnedMenuItem::Action {
|
||||
name: name.clone(),
|
||||
action: action.boxed_clone(),
|
||||
os_action: *os_action,
|
||||
checked: *checked,
|
||||
disabled: *disabled,
|
||||
},
|
||||
OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: As part of the global selections refactor, these should
|
||||
// be moved to GPUI-provided actions that make this association
|
||||
// without leaking the platform details to GPUI users
|
||||
|
||||
/// OS actions are actions that are recognized by the operating system
|
||||
/// This allows the operating system to provide specialized behavior for
|
||||
/// these actions
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum OsAction {
|
||||
/// The 'cut' action
|
||||
Cut,
|
||||
|
||||
/// The 'copy' action
|
||||
Copy,
|
||||
|
||||
/// The 'paste' action
|
||||
Paste,
|
||||
|
||||
/// The 'select all' action
|
||||
SelectAll,
|
||||
|
||||
/// The 'undo' action
|
||||
Undo,
|
||||
|
||||
/// The 'redo' action
|
||||
Redo,
|
||||
}
|
||||
|
||||
pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) {
|
||||
platform.on_will_open_app_menu(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move || {
|
||||
if let Some(app) = cx.app.upgrade() {
|
||||
app.borrow_mut().update(|cx| cx.clear_pending_keystrokes());
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_validate_app_menu_command(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
cx.app
|
||||
.upgrade()
|
||||
.map(|app| app.borrow_mut().update(|cx| cx.is_action_available(action)))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_app_menu_action(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
if let Some(app) = cx.app.upgrade() {
|
||||
app.borrow_mut().update(|cx| cx.dispatch_action(action));
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Menu;
|
||||
|
||||
#[test]
|
||||
fn test_menu() {
|
||||
let menu = Menu::new("App")
|
||||
.items(vec![
|
||||
crate::MenuItem::action("Action 1", gpui::NoAction),
|
||||
crate::MenuItem::separator(),
|
||||
])
|
||||
.disabled(true);
|
||||
|
||||
assert_eq!(menu.name.as_ref(), "App");
|
||||
assert_eq!(menu.items.len(), 2);
|
||||
assert!(menu.disabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_menu_item_builder() {
|
||||
use super::MenuItem;
|
||||
|
||||
let item = MenuItem::action("Test Action", gpui::NoAction);
|
||||
assert_eq!(
|
||||
match &item {
|
||||
MenuItem::Action { name, .. } => name.as_ref(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
"Test Action"
|
||||
);
|
||||
assert!(matches!(
|
||||
item,
|
||||
MenuItem::Action {
|
||||
checked: false,
|
||||
disabled: false,
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
assert!(
|
||||
MenuItem::action("Test Action", gpui::NoAction)
|
||||
.checked(true)
|
||||
.is_checked()
|
||||
);
|
||||
assert!(
|
||||
MenuItem::action("Test Action", gpui::NoAction)
|
||||
.disabled(true)
|
||||
.is_disabled()
|
||||
);
|
||||
|
||||
let submenu = MenuItem::submenu(super::Menu {
|
||||
name: "Submenu".into(),
|
||||
items: vec![],
|
||||
disabled: true,
|
||||
});
|
||||
assert_eq!(
|
||||
match &submenu {
|
||||
MenuItem::Submenu(menu) => menu.name.as_ref(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
"Submenu"
|
||||
);
|
||||
assert!(!submenu.is_checked());
|
||||
assert!(submenu.is_disabled());
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{KeybindingKeystroke, Keystroke};
|
||||
|
||||
/// A trait for platform-specific keyboard layouts
|
||||
pub trait PlatformKeyboardLayout {
|
||||
/// Get the keyboard layout ID, which should be unique to the layout
|
||||
fn id(&self) -> &str;
|
||||
/// Get the keyboard layout display name
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
/// A trait for platform-specific keyboard mappings
|
||||
pub trait PlatformKeyboardMapper {
|
||||
/// Map a key equivalent to its platform-specific representation
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
keystroke: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke;
|
||||
/// Get the key equivalents for the current keyboard layout,
|
||||
/// only used on macOS
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>>;
|
||||
}
|
||||
|
||||
/// A dummy implementation of the platform keyboard mapper
|
||||
pub struct DummyKeyboardMapper;
|
||||
|
||||
impl PlatformKeyboardMapper for DummyKeyboardMapper {
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
keystroke: Keystroke,
|
||||
_use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke {
|
||||
KeybindingKeystroke::from_keystroke(keystroke)
|
||||
}
|
||||
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
+782
@@ -0,0 +1,782 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Write},
|
||||
};
|
||||
|
||||
use crate::PlatformKeyboardMapper;
|
||||
|
||||
/// This is a helper trait so that we can simplify the implementation of some functions
|
||||
pub trait AsKeystroke {
|
||||
/// Returns the GPUI representation of the keystroke.
|
||||
fn as_keystroke(&self) -> &Keystroke;
|
||||
}
|
||||
|
||||
/// A keystroke and associated metadata generated by the platform
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||
pub struct Keystroke {
|
||||
/// the state of the modifier keys at the time the keystroke was generated
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
/// key is the character printed on the key that was pressed
|
||||
/// e.g. for option-s, key is "s"
|
||||
/// On layouts that do not have ascii keys (e.g. Thai)
|
||||
/// this will be the ASCII-equivalent character (q instead of ๆ),
|
||||
/// and the typed character will be present in key_char.
|
||||
pub key: String,
|
||||
|
||||
/// key_char is the character that could have been typed when
|
||||
/// this binding was pressed.
|
||||
/// e.g. for s this is "s", for option-s "ß", and cmd-s None
|
||||
pub key_char: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents a keystroke that can be used in keybindings and displayed to the user.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct KeybindingKeystroke {
|
||||
/// The GPUI representation of the keystroke.
|
||||
inner: Keystroke,
|
||||
/// The modifiers to display.
|
||||
#[cfg(target_os = "windows")]
|
||||
display_modifiers: Modifiers,
|
||||
/// The key to display.
|
||||
#[cfg(target_os = "windows")]
|
||||
display_key: String,
|
||||
}
|
||||
|
||||
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
||||
/// markdown to display it.
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidKeystrokeError {
|
||||
/// The invalid keystroke.
|
||||
pub keystroke: String,
|
||||
}
|
||||
|
||||
impl Error for InvalidKeystrokeError {}
|
||||
|
||||
impl Display for InvalidKeystrokeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Invalid keystroke \"{}\". {}",
|
||||
self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sentence explaining what keystroke parser expects, starting with "Expected ..."
|
||||
pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
|
||||
(`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
|
||||
followed by a key, separated by `-`.";
|
||||
|
||||
impl Keystroke {
|
||||
/// When matching a key we cannot know whether the user intended to type
|
||||
/// the key_char or the key itself. On some non-US keyboards keys we use in our
|
||||
/// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
|
||||
/// and on some keyboards the IME handler converts a sequence of keys into a
|
||||
/// specific character (for example `"` is typed as `" space` on a brazilian keyboard).
|
||||
///
|
||||
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
||||
/// both possibilities for self against the target.
|
||||
pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(key_char) = self
|
||||
.key_char
|
||||
.as_ref()
|
||||
.filter(|key_char| key_char != &&self.key)
|
||||
{
|
||||
let ime_modifiers = Modifiers {
|
||||
control: self.modifiers.control,
|
||||
platform: self.modifiers.platform,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(key_char) = self
|
||||
.key_char
|
||||
.as_ref()
|
||||
.filter(|key_char| key_char != &&self.key)
|
||||
{
|
||||
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
||||
if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
target.inner.modifiers == self.modifiers && target.inner.key == self.key
|
||||
}
|
||||
|
||||
/// key syntax is:
|
||||
/// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
|
||||
/// key_char syntax is only used for generating test events,
|
||||
/// secondary means "cmd" on macOS and "ctrl" on other platforms
|
||||
/// when matching a key with an key_char set will be matched without it.
|
||||
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut modifiers = Modifiers::none();
|
||||
let mut key = None;
|
||||
let mut key_char = None;
|
||||
|
||||
let mut components = source.split('-').peekable();
|
||||
while let Some(component) = components.next() {
|
||||
if component.eq_ignore_ascii_case("ctrl") {
|
||||
modifiers.control = true;
|
||||
continue;
|
||||
}
|
||||
if component.eq_ignore_ascii_case("alt") {
|
||||
modifiers.alt = true;
|
||||
continue;
|
||||
}
|
||||
if component.eq_ignore_ascii_case("shift") {
|
||||
modifiers.shift = true;
|
||||
continue;
|
||||
}
|
||||
if component.eq_ignore_ascii_case("fn") {
|
||||
modifiers.function = true;
|
||||
continue;
|
||||
}
|
||||
if component.eq_ignore_ascii_case("secondary") {
|
||||
if cfg!(target_os = "macos") {
|
||||
modifiers.platform = true;
|
||||
} else {
|
||||
modifiers.control = true;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_platform = component.eq_ignore_ascii_case("cmd")
|
||||
|| component.eq_ignore_ascii_case("super")
|
||||
|| component.eq_ignore_ascii_case("win");
|
||||
|
||||
if is_platform {
|
||||
modifiers.platform = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut key_str = component.to_string();
|
||||
|
||||
if let Some(next) = components.peek() {
|
||||
if next.is_empty() && source.ends_with('-') {
|
||||
key = Some(String::from("-"));
|
||||
break;
|
||||
} else if next.len() > 1 && next.starts_with('>') {
|
||||
key = Some(key_str);
|
||||
key_char = Some(String::from(&next[1..]));
|
||||
components.next();
|
||||
} else {
|
||||
return Err(InvalidKeystrokeError {
|
||||
keystroke: source.to_owned(),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() {
|
||||
// Convert to shift + lowercase char
|
||||
modifiers.shift = true;
|
||||
key_str.make_ascii_lowercase();
|
||||
} else {
|
||||
// convert ascii chars to lowercase so that named keys like "tab" and "enter"
|
||||
// are accepted case insensitively and stored how we expect so they are matched properly
|
||||
key_str.make_ascii_lowercase()
|
||||
}
|
||||
key = Some(key_str);
|
||||
}
|
||||
|
||||
// Allow for the user to specify a keystroke modifier as the key itself
|
||||
// This sets the `key` to the modifier, and disables the modifier
|
||||
key = key.or_else(|| {
|
||||
use std::mem;
|
||||
// std::mem::take clears bool incase its true
|
||||
if mem::take(&mut modifiers.shift) {
|
||||
Some("shift".to_string())
|
||||
} else if mem::take(&mut modifiers.control) {
|
||||
Some("control".to_string())
|
||||
} else if mem::take(&mut modifiers.alt) {
|
||||
Some("alt".to_string())
|
||||
} else if mem::take(&mut modifiers.platform) {
|
||||
Some("platform".to_string())
|
||||
} else if mem::take(&mut modifiers.function) {
|
||||
Some("function".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let key = key.ok_or_else(|| InvalidKeystrokeError {
|
||||
keystroke: source.to_owned(),
|
||||
})?;
|
||||
|
||||
Ok(Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
key_char,
|
||||
})
|
||||
}
|
||||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
unparse(&self.modifiers, &self.key)
|
||||
}
|
||||
|
||||
/// Returns true if this keystroke left
|
||||
/// the ime system in an incomplete state.
|
||||
pub fn is_ime_in_progress(&self) -> bool {
|
||||
self.key_char.is_none()
|
||||
&& (is_printable_key(&self.key) || self.key.is_empty())
|
||||
&& !(self.modifiers.platform
|
||||
|| self.modifiers.control
|
||||
|| self.modifiers.function
|
||||
|| self.modifiers.alt)
|
||||
}
|
||||
|
||||
/// Returns a new keystroke with the key_char filled.
|
||||
/// This is used for dispatch_keystroke where we want users to
|
||||
/// be able to simulate typing "space", etc.
|
||||
pub fn with_simulated_ime(mut self) -> Self {
|
||||
if self.key_char.is_none()
|
||||
&& !self.modifiers.platform
|
||||
&& !self.modifiers.control
|
||||
&& !self.modifiers.function
|
||||
&& !self.modifiers.alt
|
||||
{
|
||||
self.key_char = match self.key.as_str() {
|
||||
"space" => Some(" ".into()),
|
||||
"tab" => Some("\t".into()),
|
||||
"enter" => Some("\n".into()),
|
||||
key if !is_printable_key(key) || key.is_empty() => None,
|
||||
key => {
|
||||
if self.modifiers.shift {
|
||||
Some(key.to_uppercase())
|
||||
} else {
|
||||
Some(key.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl KeybindingKeystroke {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[expect(missing_docs)]
|
||||
pub fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
|
||||
KeybindingKeystroke {
|
||||
inner,
|
||||
display_modifiers,
|
||||
display_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new keybinding keystroke from the given keystroke using the given keyboard mapper.
|
||||
pub fn new_with_mapper(
|
||||
inner: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> Self {
|
||||
keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
|
||||
}
|
||||
|
||||
/// Create a new keybinding keystroke from the given keystroke, without any platform-specific mapping.
|
||||
pub fn from_keystroke(keystroke: Keystroke) -> Self {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let key = keystroke.key.clone();
|
||||
let modifiers = keystroke.modifiers;
|
||||
KeybindingKeystroke {
|
||||
inner: keystroke,
|
||||
display_modifiers: modifiers,
|
||||
display_key: key,
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
KeybindingKeystroke { inner: keystroke }
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the GPUI representation of the keystroke.
|
||||
pub fn inner(&self) -> &Keystroke {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Returns the modifiers.
|
||||
///
|
||||
/// Platform-specific behavior:
|
||||
/// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke.
|
||||
/// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as
|
||||
/// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`.
|
||||
pub fn modifiers(&self) -> &Modifiers {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
&self.display_modifiers
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
&self.inner.modifiers
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the key.
|
||||
///
|
||||
/// Platform-specific behavior:
|
||||
/// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke.
|
||||
/// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`.
|
||||
pub fn key(&self) -> &str {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
&self.display_key
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
&self.inner.key
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`.
|
||||
pub fn set_modifiers(&mut self, modifiers: Modifiers) {
|
||||
self.inner.modifiers = modifiers;
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.display_modifiers = modifiers;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the key. On Windows this modifies both `inner.key` and `display_key`.
|
||||
pub fn set_key(&mut self, key: String) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.display_key = key.clone();
|
||||
}
|
||||
self.inner.key = key;
|
||||
}
|
||||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
unparse(&self.display_modifiers, &self.display_key)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
unparse(&self.inner.modifiers, &self.inner.key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the key_char
|
||||
pub fn remove_key_char(&mut self) {
|
||||
self.inner.key_char = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_printable_key(key: &str) -> bool {
|
||||
!matches!(
|
||||
key,
|
||||
"f1" | "f2"
|
||||
| "f3"
|
||||
| "f4"
|
||||
| "f5"
|
||||
| "f6"
|
||||
| "f7"
|
||||
| "f8"
|
||||
| "f9"
|
||||
| "f10"
|
||||
| "f11"
|
||||
| "f12"
|
||||
| "f13"
|
||||
| "f14"
|
||||
| "f15"
|
||||
| "f16"
|
||||
| "f17"
|
||||
| "f18"
|
||||
| "f19"
|
||||
| "f20"
|
||||
| "f21"
|
||||
| "f22"
|
||||
| "f23"
|
||||
| "f24"
|
||||
| "f25"
|
||||
| "f26"
|
||||
| "f27"
|
||||
| "f28"
|
||||
| "f29"
|
||||
| "f30"
|
||||
| "f31"
|
||||
| "f32"
|
||||
| "f33"
|
||||
| "f34"
|
||||
| "f35"
|
||||
| "backspace"
|
||||
| "delete"
|
||||
| "left"
|
||||
| "right"
|
||||
| "up"
|
||||
| "down"
|
||||
| "pageup"
|
||||
| "pagedown"
|
||||
| "insert"
|
||||
| "home"
|
||||
| "end"
|
||||
| "back"
|
||||
| "forward"
|
||||
| "escape"
|
||||
)
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Keystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
display_modifiers(&self.modifiers, f)?;
|
||||
display_key(&self.key, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KeybindingKeystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
display_modifiers(self.modifiers(), f)?;
|
||||
display_key(self.key(), f)
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of the modifier keys at some point in time
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
|
||||
pub struct Modifiers {
|
||||
/// The control key
|
||||
#[serde(default)]
|
||||
pub control: bool,
|
||||
|
||||
/// The alt key
|
||||
/// Sometimes also known as the 'meta' key
|
||||
#[serde(default)]
|
||||
pub alt: bool,
|
||||
|
||||
/// The shift key
|
||||
#[serde(default)]
|
||||
pub shift: bool,
|
||||
|
||||
/// The command key, on macos
|
||||
/// the windows key, on windows
|
||||
/// the super key, on linux
|
||||
#[serde(default)]
|
||||
pub platform: bool,
|
||||
|
||||
/// The function key
|
||||
#[serde(default)]
|
||||
pub function: bool,
|
||||
}
|
||||
|
||||
impl Modifiers {
|
||||
/// Returns whether any modifier key is pressed.
|
||||
pub fn modified(&self) -> bool {
|
||||
self.control || self.alt || self.shift || self.platform || self.function
|
||||
}
|
||||
|
||||
/// Whether the semantically 'secondary' modifier key is pressed.
|
||||
///
|
||||
/// On macOS, this is the command key.
|
||||
/// On Linux and Windows, this is the control key.
|
||||
pub fn secondary(&self) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
self.platform
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
self.control
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns how many modifier keys are pressed.
|
||||
pub fn number_of_modifiers(&self) -> u8 {
|
||||
self.control as u8
|
||||
+ self.alt as u8
|
||||
+ self.shift as u8
|
||||
+ self.platform as u8
|
||||
+ self.function as u8
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with no modifiers.
|
||||
pub fn none() -> Modifiers {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just the command key.
|
||||
pub fn command() -> Modifiers {
|
||||
Modifiers {
|
||||
platform: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Returns [`Modifiers`] with just the secondary key pressed.
|
||||
pub fn secondary_key() -> Modifiers {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Modifiers {
|
||||
platform: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
Modifiers {
|
||||
control: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just the windows key.
|
||||
pub fn windows() -> Modifiers {
|
||||
Modifiers {
|
||||
platform: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just the super key.
|
||||
pub fn super_key() -> Modifiers {
|
||||
Modifiers {
|
||||
platform: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just control.
|
||||
pub fn control() -> Modifiers {
|
||||
Modifiers {
|
||||
control: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just alt.
|
||||
pub fn alt() -> Modifiers {
|
||||
Modifiers {
|
||||
alt: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just shift.
|
||||
pub fn shift() -> Modifiers {
|
||||
Modifiers {
|
||||
shift: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just function.
|
||||
pub fn function() -> Modifiers {
|
||||
Modifiers {
|
||||
function: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with command + shift.
|
||||
pub fn command_shift() -> Modifiers {
|
||||
Modifiers {
|
||||
shift: true,
|
||||
platform: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with command + shift.
|
||||
pub fn control_shift() -> Modifiers {
|
||||
Modifiers {
|
||||
shift: true,
|
||||
control: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
|
||||
pub fn is_subset_of(&self, other: &Modifiers) -> bool {
|
||||
(*other & *self) == *self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOr for Modifiers {
|
||||
type Output = Self;
|
||||
|
||||
fn bitor(mut self, other: Self) -> Self::Output {
|
||||
self |= other;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOrAssign for Modifiers {
|
||||
fn bitor_assign(&mut self, other: Self) {
|
||||
self.control |= other.control;
|
||||
self.alt |= other.alt;
|
||||
self.shift |= other.shift;
|
||||
self.platform |= other.platform;
|
||||
self.function |= other.function;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitXor for Modifiers {
|
||||
type Output = Self;
|
||||
fn bitxor(mut self, rhs: Self) -> Self::Output {
|
||||
self ^= rhs;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitXorAssign for Modifiers {
|
||||
fn bitxor_assign(&mut self, other: Self) {
|
||||
self.control ^= other.control;
|
||||
self.alt ^= other.alt;
|
||||
self.shift ^= other.shift;
|
||||
self.platform ^= other.platform;
|
||||
self.function ^= other.function;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitAnd for Modifiers {
|
||||
type Output = Self;
|
||||
fn bitand(mut self, rhs: Self) -> Self::Output {
|
||||
self &= rhs;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitAndAssign for Modifiers {
|
||||
fn bitand_assign(&mut self, other: Self) {
|
||||
self.control &= other.control;
|
||||
self.alt &= other.alt;
|
||||
self.shift &= other.shift;
|
||||
self.platform &= other.platform;
|
||||
self.function &= other.function;
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of the capslock key at some point in time
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
|
||||
pub struct Capslock {
|
||||
/// The capslock key is on
|
||||
#[serde(default)]
|
||||
pub on: bool,
|
||||
}
|
||||
|
||||
impl AsKeystroke for Keystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsKeystroke for KeybindingKeystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsKeystroke + ?Sized> AsKeystroke for &T {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
(**self).as_keystroke()
|
||||
}
|
||||
}
|
||||
|
||||
fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if modifiers.control {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('^')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "ctrl-")?;
|
||||
}
|
||||
if modifiers.alt {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌥')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "alt-")?;
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌘')?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
f.write_char('❖')?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
f.write_char('⊞')?;
|
||||
}
|
||||
if modifiers.shift {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⇧')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "shift-")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let key = match key {
|
||||
#[cfg(target_os = "macos")]
|
||||
"backspace" => '⌫',
|
||||
#[cfg(target_os = "macos")]
|
||||
"up" => '↑',
|
||||
#[cfg(target_os = "macos")]
|
||||
"down" => '↓',
|
||||
#[cfg(target_os = "macos")]
|
||||
"left" => '←',
|
||||
#[cfg(target_os = "macos")]
|
||||
"right" => '→',
|
||||
#[cfg(target_os = "macos")]
|
||||
"tab" => '⇥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"escape" => '⎋',
|
||||
#[cfg(target_os = "macos")]
|
||||
"shift" => '⇧',
|
||||
#[cfg(target_os = "macos")]
|
||||
"control" => '⌃',
|
||||
#[cfg(target_os = "macos")]
|
||||
"alt" => '⌥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"platform" => '⌘',
|
||||
|
||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||
key => return f.write_str(key),
|
||||
};
|
||||
f.write_char(key)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn unparse(modifiers: &Modifiers, key: &str) -> String {
|
||||
let mut result = String::new();
|
||||
if modifiers.function {
|
||||
result.push_str("fn-");
|
||||
}
|
||||
if modifiers.control {
|
||||
result.push_str("ctrl-");
|
||||
}
|
||||
if modifiers.alt {
|
||||
result.push_str("alt-");
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
result.push_str("cmd-");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
result.push_str("super-");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
result.push_str("win-");
|
||||
}
|
||||
if modifiers.shift {
|
||||
result.push_str("shift-");
|
||||
}
|
||||
result.push_str(&key);
|
||||
result
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
use bitflags::bitflags;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::Pixels;
|
||||
|
||||
/// The layer the surface is rendered on. Multiple surfaces can share a layer, and ordering within
|
||||
/// a single layer is undefined.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Layer {
|
||||
/// The background layer, typically used for wallpapers.
|
||||
Background,
|
||||
|
||||
/// The bottom layer.
|
||||
Bottom,
|
||||
|
||||
/// The top layer, typically used for fullscreen windows.
|
||||
Top,
|
||||
|
||||
/// The overlay layer, used for surfaces that should always be on top.
|
||||
#[default]
|
||||
Overlay,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Screen anchor point for layer_shell surfaces. These can be used in any combination, e.g.
|
||||
/// specifying `Anchor::LEFT | Anchor::RIGHT` will stretch the surface across the width of the
|
||||
/// screen.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Anchor: u32 {
|
||||
/// Anchor to the top edge of the screen.
|
||||
const TOP = 1;
|
||||
/// Anchor to the bottom edge of the screen.
|
||||
const BOTTOM = 2;
|
||||
/// Anchor to the left edge of the screen.
|
||||
const LEFT = 4;
|
||||
/// Anchor to the right edge of the screen.
|
||||
const RIGHT = 8;
|
||||
}
|
||||
}
|
||||
|
||||
/// Keyboard interactivity mode for the layer_shell surfaces.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum KeyboardInteractivity {
|
||||
/// No keyboard inputs will be delivered to the surface and it won't be able to receive
|
||||
/// keyboard focus.
|
||||
None,
|
||||
|
||||
/// The surface will receive exclusive keyboard focus as long as it is above the shell surface
|
||||
/// layer, and no other layer_shell surfaces are above it.
|
||||
Exclusive,
|
||||
|
||||
/// The surface can be focused similarly to a normal window.
|
||||
#[default]
|
||||
OnDemand,
|
||||
}
|
||||
|
||||
/// Options for creating a layer_shell window.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct LayerShellOptions {
|
||||
/// The namespace for the surface, mostly used by compositors to apply rules, can not be
|
||||
/// changed after the surface is created.
|
||||
pub namespace: String,
|
||||
/// The layer the surface is rendered on.
|
||||
pub layer: Layer,
|
||||
/// The anchor point of the surface.
|
||||
pub anchor: Anchor,
|
||||
/// Requests that the compositor avoids occluding an area with other surfaces.
|
||||
pub exclusive_zone: Option<Pixels>,
|
||||
/// The anchor point of the exclusive zone, will be determined using the anchor if left
|
||||
/// unspecified.
|
||||
pub exclusive_edge: Option<Anchor>,
|
||||
/// Margins between the surface and its anchor point(s).
|
||||
/// Specified in CSS order: top, right, bottom, left.
|
||||
pub margin: Option<(Pixels, Pixels, Pixels, Pixels)>,
|
||||
/// How keyboard events should be delivered to the surface.
|
||||
pub keyboard_interactivity: KeyboardInteractivity,
|
||||
}
|
||||
|
||||
/// An error indicating that an action failed because the compositor doesn't support the required
|
||||
/// layer_shell protocol.
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Compositor doesn't support zwlr_layer_shell_v1")]
|
||||
pub struct LayerShellNotSupportedError;
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
use bitflags::bitflags;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{AnyWindowHandle, Bounds, Pixels, Point};
|
||||
|
||||
/// Options for a parent-anchored popup window such as a menu, dropdown, context menu or tooltip.
|
||||
///
|
||||
/// A popup is placed relative to an anchor rectangle on its parent window rather than at an
|
||||
/// absolute screen position. The platform resolves the final position, so this works both on
|
||||
/// systems where the compositor owns window placement (Wayland) and on platforms with absolute
|
||||
/// coordinates.
|
||||
///
|
||||
/// The popup's size comes from [`WindowOptions::window_bounds`](crate::WindowOptions), whose
|
||||
/// origin is ignored. All coordinates are in logical pixels.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PopupOptions {
|
||||
/// The window the popup is anchored to.
|
||||
pub parent: AnyWindowHandle,
|
||||
|
||||
/// The rectangle the popup is positioned relative to, in the parent window's coordinate
|
||||
/// space (the same space element bounds are in). For example, a dropdown menu uses the
|
||||
/// bounds of the button that opened it.
|
||||
pub anchor_rect: Bounds<Pixels>,
|
||||
|
||||
/// Which point of [`Self::anchor_rect`] the popup is anchored to.
|
||||
pub anchor: PopupAnchor,
|
||||
|
||||
/// The direction in which the popup extends away from the anchor point. A dropdown that
|
||||
/// drops below its button anchors to [`PopupAnchor::BottomLeft`] with a gravity of
|
||||
/// [`PopupGravity::BottomRight`] so it grows down and to the right.
|
||||
pub gravity: PopupGravity,
|
||||
|
||||
/// How the platform may adjust the popup if the requested placement would put it off-screen.
|
||||
pub constraint_adjustment: PopupConstraintAdjustment,
|
||||
|
||||
/// An additional offset applied to the popup after anchoring.
|
||||
pub offset: Point<Pixels>,
|
||||
|
||||
/// Whether the popup should take an explicit input grab.
|
||||
///
|
||||
/// Grabbing popups behave like menus: they take keyboard focus and are dismissed when the
|
||||
/// user clicks outside of them or presses a dismissing key. Use it for menus and comboboxes,
|
||||
/// not for tooltips or other passive popups.
|
||||
///
|
||||
/// A grab must be requested while the triggering input is still active, in practice the
|
||||
/// press of the mouse button that opens the popup. Open grabbing popups from a mouse-down
|
||||
/// handler rather than a click handler, otherwise the grab is refused.
|
||||
///
|
||||
/// Automatic dismissal only covers input aimed at other applications. A click elsewhere in
|
||||
/// your own application still reaches it as usual, so closing the popup in that case is up
|
||||
/// to you. Nested grabbing popups must be closed in the reverse order they were opened.
|
||||
pub grab: bool,
|
||||
}
|
||||
|
||||
/// The point of the anchor rectangle that a popup is anchored to.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum PopupAnchor {
|
||||
/// Anchor to the center of the anchor rectangle.
|
||||
#[default]
|
||||
Center,
|
||||
/// Anchor to the center of the top edge.
|
||||
Top,
|
||||
/// Anchor to the center of the bottom edge.
|
||||
Bottom,
|
||||
/// Anchor to the center of the left edge.
|
||||
Left,
|
||||
/// Anchor to the center of the right edge.
|
||||
Right,
|
||||
/// Anchor to the top-left corner.
|
||||
TopLeft,
|
||||
/// Anchor to the bottom-left corner.
|
||||
BottomLeft,
|
||||
/// Anchor to the top-right corner.
|
||||
TopRight,
|
||||
/// Anchor to the bottom-right corner.
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
/// The direction in which a popup extends away from its anchor point.
|
||||
///
|
||||
/// For instance, a gravity of [`PopupGravity::BottomRight`] places the popup below and to the
|
||||
/// right of the anchor point.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum PopupGravity {
|
||||
/// The popup is centered over the anchor point.
|
||||
#[default]
|
||||
Center,
|
||||
/// The popup extends upwards from the anchor point.
|
||||
Top,
|
||||
/// The popup extends downwards from the anchor point.
|
||||
Bottom,
|
||||
/// The popup extends to the left of the anchor point.
|
||||
Left,
|
||||
/// The popup extends to the right of the anchor point.
|
||||
Right,
|
||||
/// The popup extends up and to the left of the anchor point.
|
||||
TopLeft,
|
||||
/// The popup extends down and to the left of the anchor point.
|
||||
BottomLeft,
|
||||
/// The popup extends up and to the right of the anchor point.
|
||||
TopRight,
|
||||
/// The popup extends down and to the right of the anchor point.
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// How a popup may be adjusted by the platform if the requested placement would put it
|
||||
/// off-screen. If no flags are set, the popup is placed exactly as requested and may be
|
||||
/// clipped.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct PopupConstraintAdjustment: u32 {
|
||||
/// The popup may be slid horizontally to stay on-screen.
|
||||
const SLIDE_X = 1;
|
||||
/// The popup may be slid vertically to stay on-screen.
|
||||
const SLIDE_Y = 2;
|
||||
/// The popup's anchor and gravity may be flipped horizontally to stay on-screen.
|
||||
const FLIP_X = 4;
|
||||
/// The popup's anchor and gravity may be flipped vertically to stay on-screen.
|
||||
const FLIP_Y = 8;
|
||||
/// The popup may be shrunk horizontally to stay on-screen.
|
||||
const RESIZE_X = 16;
|
||||
/// The popup may be shrunk vertically to stay on-screen.
|
||||
const RESIZE_Y = 32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returned when the current platform has no native popup implementation yet.
|
||||
///
|
||||
/// Native popups are separate from gpui's in-window popovers, which are drawn as elements inside
|
||||
/// an existing window. A caller that wants a popup on every platform should treat this error as
|
||||
/// a cue to fall back to that in-window rendering.
|
||||
#[derive(Debug, Error)]
|
||||
#[error("popups are not supported on this platform")]
|
||||
pub struct PopupNotSupportedError;
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
//! Screen capture for Linux and Windows
|
||||
use crate::{
|
||||
DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
Size, SourceMetadata, size,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use futures::channel::oneshot;
|
||||
use scap::Target;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
|
||||
/// Populates the receiver with the screens that can be captured.
|
||||
///
|
||||
/// `scap_default_target_source` should be used instead on Wayland, since `scap_screen_sources`
|
||||
/// won't return any results.
|
||||
#[allow(dead_code)]
|
||||
pub fn scap_screen_sources(
|
||||
foreground_executor: &ForegroundExecutor,
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let (sources_tx, sources_rx) = oneshot::channel();
|
||||
get_screen_targets(sources_tx);
|
||||
to_dyn_screen_capture_sources(sources_rx, foreground_executor)
|
||||
}
|
||||
|
||||
/// Starts screen capture for the default target, and populates the receiver with a single source
|
||||
/// for it. The first frame of the screen capture is used to determine the size of the stream.
|
||||
///
|
||||
/// On Wayland (Linux), prompts the user to select a target, and populates the receiver with a
|
||||
/// single screen capture source for their selection.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn start_scap_default_target_source(
|
||||
foreground_executor: &ForegroundExecutor,
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let (sources_tx, sources_rx) = oneshot::channel();
|
||||
start_default_target_screen_capture(sources_tx);
|
||||
to_dyn_screen_capture_sources(sources_rx, foreground_executor)
|
||||
}
|
||||
|
||||
struct ScapCaptureSource {
|
||||
target: scap::Display,
|
||||
size: Size<DevicePixels>,
|
||||
}
|
||||
|
||||
/// Populates the sender with the screens available for capture.
|
||||
fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>>) {
|
||||
// Due to use of blocking APIs, a new thread is used.
|
||||
std::thread::spawn(|| {
|
||||
let targets = match scap::get_all_targets() {
|
||||
Ok(targets) => targets,
|
||||
Err(err) => {
|
||||
sources_tx.send(Err(err)).ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
let sources = targets
|
||||
.into_iter()
|
||||
.filter_map(|target| match target {
|
||||
scap::Target::Display(display) => {
|
||||
let size = Size {
|
||||
width: DevicePixels(display.width as i32),
|
||||
height: DevicePixels(display.height as i32),
|
||||
};
|
||||
Some(ScapCaptureSource {
|
||||
target: display,
|
||||
size,
|
||||
})
|
||||
}
|
||||
scap::Target::Window(_) => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
sources_tx.send(Ok(sources)).ok();
|
||||
});
|
||||
}
|
||||
|
||||
impl ScreenCaptureSource for ScapCaptureSource {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
Ok(SourceMetadata {
|
||||
resolution: self.size,
|
||||
label: Some(self.target.title.clone().into()),
|
||||
is_main: None,
|
||||
id: self.target.id as u64,
|
||||
})
|
||||
}
|
||||
|
||||
fn stream(
|
||||
&self,
|
||||
foreground_executor: &ForegroundExecutor,
|
||||
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
||||
) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
|
||||
let (stream_tx, stream_rx) = oneshot::channel();
|
||||
let target = self.target.clone();
|
||||
|
||||
// Due to use of blocking APIs, a dedicated thread is used.
|
||||
std::thread::spawn(move || {
|
||||
match new_scap_capturer(Some(scap::Target::Display(target.clone()))) {
|
||||
Ok(mut capturer) => {
|
||||
capturer.start_capture();
|
||||
run_capture(capturer, target.clone(), frame_callback, stream_tx);
|
||||
}
|
||||
Err(e) => {
|
||||
stream_tx.send(Err(e)).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
to_dyn_screen_capture_stream(stream_rx, foreground_executor)
|
||||
}
|
||||
}
|
||||
|
||||
struct ScapDefaultTargetCaptureSource {
|
||||
// Sender populated by single call to `ScreenCaptureSource::stream`.
|
||||
stream_call_tx: std::sync::mpsc::SyncSender<(
|
||||
// Provides the result of `ScreenCaptureSource::stream`.
|
||||
oneshot::Sender<Result<ScapStream>>,
|
||||
// Callback for frames.
|
||||
Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
||||
)>,
|
||||
target: scap::Display,
|
||||
size: Size<DevicePixels>,
|
||||
}
|
||||
|
||||
/// Starts screen capture on the default capture target, and populates the sender with the source.
|
||||
fn start_default_target_screen_capture(
|
||||
sources_tx: oneshot::Sender<Result<Vec<ScapDefaultTargetCaptureSource>>>,
|
||||
) {
|
||||
// Due to use of blocking APIs, a dedicated thread is used.
|
||||
std::thread::spawn(|| {
|
||||
let start_result = gpui_util::maybe!({
|
||||
let mut capturer = new_scap_capturer(None)?;
|
||||
capturer.start_capture();
|
||||
let first_frame = capturer
|
||||
.get_next_frame()
|
||||
.context("Failed to get first frame of screenshare to get the size.")?;
|
||||
let size = frame_size(&first_frame);
|
||||
let target = capturer
|
||||
.target()
|
||||
.context("Unable to determine the target display.")?;
|
||||
let target = target.clone();
|
||||
Ok((capturer, size, target))
|
||||
});
|
||||
|
||||
match start_result {
|
||||
Ok((capturer, size, Target::Display(display))) => {
|
||||
let (stream_call_tx, stream_rx) = std::sync::mpsc::sync_channel(1);
|
||||
sources_tx
|
||||
.send(Ok(vec![ScapDefaultTargetCaptureSource {
|
||||
stream_call_tx,
|
||||
size,
|
||||
target: display.clone(),
|
||||
}]))
|
||||
.ok();
|
||||
let Ok((stream_tx, frame_callback)) = stream_rx.recv() else {
|
||||
return;
|
||||
};
|
||||
run_capture(capturer, display, frame_callback, stream_tx);
|
||||
}
|
||||
Err(e) => {
|
||||
sources_tx.send(Err(e)).ok();
|
||||
}
|
||||
_ => {
|
||||
sources_tx
|
||||
.send(Err(anyhow!("The screen capture source is not a display")))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl ScreenCaptureSource for ScapDefaultTargetCaptureSource {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
Ok(SourceMetadata {
|
||||
resolution: self.size,
|
||||
label: None,
|
||||
is_main: None,
|
||||
id: self.target.id as u64,
|
||||
})
|
||||
}
|
||||
|
||||
fn stream(
|
||||
&self,
|
||||
foreground_executor: &ForegroundExecutor,
|
||||
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
||||
) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
match self.stream_call_tx.try_send((tx, frame_callback)) {
|
||||
Ok(()) => {}
|
||||
Err(std::sync::mpsc::TrySendError::Full((tx, _)))
|
||||
| Err(std::sync::mpsc::TrySendError::Disconnected((tx, _))) => {
|
||||
// Note: support could be added for being called again after end of prior stream.
|
||||
tx.send(Err(anyhow!(
|
||||
"Can't call ScapDefaultTargetCaptureSource::stream multiple times."
|
||||
)))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
to_dyn_screen_capture_stream(rx, foreground_executor)
|
||||
}
|
||||
}
|
||||
|
||||
fn new_scap_capturer(target: Option<scap::Target>) -> Result<scap::capturer::Capturer> {
|
||||
scap::capturer::Capturer::build(scap::capturer::Options {
|
||||
fps: 60,
|
||||
show_cursor: true,
|
||||
show_highlight: true,
|
||||
// Note that the actual frame output type may differ.
|
||||
output_type: scap::frame::FrameType::YUVFrame,
|
||||
output_resolution: scap::capturer::Resolution::Captured,
|
||||
crop_area: None,
|
||||
target,
|
||||
excluded_targets: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn run_capture(
|
||||
mut capturer: scap::capturer::Capturer,
|
||||
display: scap::Display,
|
||||
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
||||
stream_tx: oneshot::Sender<Result<ScapStream>>,
|
||||
) {
|
||||
let cancel_stream = Arc::new(AtomicBool::new(false));
|
||||
let size = Size {
|
||||
width: DevicePixels(display.width as i32),
|
||||
height: DevicePixels(display.height as i32),
|
||||
};
|
||||
let stream_send_result = stream_tx.send(Ok(ScapStream {
|
||||
cancel_stream: cancel_stream.clone(),
|
||||
display,
|
||||
size,
|
||||
}));
|
||||
if stream_send_result.is_err() {
|
||||
return;
|
||||
}
|
||||
while !cancel_stream.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
match capturer.get_next_frame() {
|
||||
Ok(frame) => frame_callback(ScreenCaptureFrame(frame)),
|
||||
Err(err) => {
|
||||
log::error!("Halting screen capture due to error: {err}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
capturer.stop_capture();
|
||||
}
|
||||
|
||||
struct ScapStream {
|
||||
cancel_stream: Arc<AtomicBool>,
|
||||
display: scap::Display,
|
||||
size: Size<DevicePixels>,
|
||||
}
|
||||
|
||||
impl ScreenCaptureStream for ScapStream {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
Ok(SourceMetadata {
|
||||
resolution: self.size,
|
||||
label: Some(self.display.title.clone().into()),
|
||||
is_main: None,
|
||||
id: self.display.id as u64,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ScapStream {
|
||||
fn drop(&mut self) {
|
||||
self.cancel_stream.store(true, atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
fn frame_size(frame: &scap::frame::Frame) -> Size<DevicePixels> {
|
||||
let (width, height) = match frame {
|
||||
scap::frame::Frame::YUVFrame(frame) => (frame.width, frame.height),
|
||||
scap::frame::Frame::RGB(frame) => (frame.width, frame.height),
|
||||
scap::frame::Frame::RGBx(frame) => (frame.width, frame.height),
|
||||
scap::frame::Frame::XBGR(frame) => (frame.width, frame.height),
|
||||
scap::frame::Frame::BGRx(frame) => (frame.width, frame.height),
|
||||
scap::frame::Frame::BGR0(frame) => (frame.width, frame.height),
|
||||
scap::frame::Frame::BGRA(frame) => (frame.width, frame.height),
|
||||
};
|
||||
size(DevicePixels(width), DevicePixels(height))
|
||||
}
|
||||
|
||||
/// This is used by `get_screen_targets` and `start_default_target_screen_capture` to turn their
|
||||
/// results into `Rc<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so
|
||||
/// the capture source structs are used as `Rc<dyn ScreenCaptureSource>` is not `Send`.
|
||||
fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
|
||||
sources_rx: oneshot::Receiver<Result<Vec<T>>>,
|
||||
foreground_executor: &ForegroundExecutor,
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel();
|
||||
foreground_executor
|
||||
.spawn(async move {
|
||||
match sources_rx.await {
|
||||
Ok(Ok(results)) => dyn_sources_tx
|
||||
.send(Ok(results
|
||||
.into_iter()
|
||||
.map(|source| Rc::new(source) as Rc<dyn ScreenCaptureSource>)
|
||||
.collect::<Vec<_>>()))
|
||||
.ok(),
|
||||
Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(),
|
||||
Err(oneshot::Canceled) => None,
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
dyn_sources_rx
|
||||
}
|
||||
|
||||
/// Same motivation as `to_dyn_screen_capture_sources` above.
|
||||
fn to_dyn_screen_capture_stream<T: ScreenCaptureStream + 'static>(
|
||||
sources_rx: oneshot::Receiver<Result<T>>,
|
||||
foreground_executor: &ForegroundExecutor,
|
||||
) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
|
||||
let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel();
|
||||
foreground_executor
|
||||
.spawn(async move {
|
||||
match sources_rx.await {
|
||||
Ok(Ok(stream)) => dyn_sources_tx
|
||||
.send(Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>))
|
||||
.ok(),
|
||||
Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(),
|
||||
Err(oneshot::Canceled) => None,
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
dyn_sources_rx
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
mod dispatcher;
|
||||
mod display;
|
||||
mod platform;
|
||||
mod window;
|
||||
|
||||
pub use dispatcher::*;
|
||||
pub(crate) use display::*;
|
||||
pub(crate) use platform::*;
|
||||
pub(crate) use window::*;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use platform::{TestScreenCaptureSource, TestScreenCaptureStream};
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
use crate::{PlatformDispatcher, Priority, RunnableVariant};
|
||||
use scheduler::Instant;
|
||||
use scheduler::{Clock, Scheduler, SessionId, TestScheduler, TestSchedulerConfig, Yield};
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// TestDispatcher provides deterministic async execution for tests.
|
||||
///
|
||||
/// This implementation delegates task scheduling to the scheduler crate's `TestScheduler`.
|
||||
/// Access the scheduler directly via `scheduler()` for clock, rng, and parking control.
|
||||
#[doc(hidden)]
|
||||
pub struct TestDispatcher {
|
||||
session_id: SessionId,
|
||||
scheduler: Arc<TestScheduler>,
|
||||
num_cpus_override: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl TestDispatcher {
|
||||
pub fn new(seed: u64) -> Self {
|
||||
let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig {
|
||||
seed,
|
||||
randomize_order: true,
|
||||
allow_parking: false,
|
||||
capture_pending_traces: std::env::var("PENDING_TRACES")
|
||||
.map_or(false, |var| var == "1" || var == "true"),
|
||||
timeout_ticks: 0..=1000,
|
||||
}));
|
||||
Self::from_scheduler(scheduler)
|
||||
}
|
||||
|
||||
pub fn from_scheduler(scheduler: Arc<TestScheduler>) -> Self {
|
||||
TestDispatcher {
|
||||
session_id: scheduler.allocate_session_id(),
|
||||
scheduler,
|
||||
num_cpus_override: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scheduler(&self) -> &Arc<TestScheduler> {
|
||||
&self.scheduler
|
||||
}
|
||||
|
||||
pub fn session_id(&self) -> SessionId {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
pub fn drain_tasks(&self) {
|
||||
self.scheduler.drain_tasks();
|
||||
}
|
||||
|
||||
pub fn advance_clock(&self, by: Duration) {
|
||||
self.scheduler.advance_clock(by);
|
||||
}
|
||||
|
||||
pub fn advance_clock_to_next_timer(&self) -> bool {
|
||||
self.scheduler.advance_clock_to_next_timer()
|
||||
}
|
||||
|
||||
pub fn simulate_random_delay(&self) -> Yield {
|
||||
self.scheduler.yield_random()
|
||||
}
|
||||
|
||||
pub fn tick(&self, background_only: bool) -> bool {
|
||||
if background_only {
|
||||
self.scheduler.tick_background_only()
|
||||
} else {
|
||||
self.scheduler.tick()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_until_parked(&self) {
|
||||
while self.tick(false) {}
|
||||
}
|
||||
|
||||
pub fn allow_parking(&self) {
|
||||
self.scheduler.allow_parking();
|
||||
}
|
||||
|
||||
pub fn forbid_parking(&self) {
|
||||
self.scheduler.forbid_parking();
|
||||
}
|
||||
|
||||
/// Override the value returned by `BackgroundExecutor::num_cpus()` in tests.
|
||||
/// A value of 0 means no override (the default of 4 is used).
|
||||
pub fn set_num_cpus(&self, count: usize) {
|
||||
self.num_cpus_override.store(count, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Returns the overridden CPU count, or `None` if no override is set.
|
||||
pub fn num_cpus_override(&self) -> Option<usize> {
|
||||
match self.num_cpus_override.load(Ordering::SeqCst) {
|
||||
0 => None,
|
||||
n => Some(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for TestDispatcher {
|
||||
fn clone(&self) -> Self {
|
||||
let session_id = self.scheduler.allocate_session_id();
|
||||
Self {
|
||||
session_id,
|
||||
scheduler: self.scheduler.clone(),
|
||||
num_cpus_override: self.num_cpus_override.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDispatcher for TestDispatcher {
|
||||
fn is_main_thread(&self) -> bool {
|
||||
self.scheduler.is_main_thread()
|
||||
}
|
||||
|
||||
fn now(&self) -> Instant {
|
||||
self.scheduler.clock().now()
|
||||
}
|
||||
|
||||
fn dispatch(&self, runnable: RunnableVariant, priority: Priority) {
|
||||
self.scheduler
|
||||
.schedule_background_with_priority(runnable, priority);
|
||||
}
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) {
|
||||
self.scheduler.schedule_local(self.session_id, runnable);
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) {
|
||||
panic!(
|
||||
"dispatch_after should not be called in tests. \
|
||||
Use BackgroundExecutor::timer() which uses the scheduler's native timer."
|
||||
);
|
||||
}
|
||||
|
||||
fn as_test(&self) -> Option<&TestDispatcher> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>) {
|
||||
std::thread::spawn(move || {
|
||||
f();
|
||||
});
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
use crate::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, px};
|
||||
use anyhow::{Ok, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TestDisplay {
|
||||
id: DisplayId,
|
||||
uuid: uuid::Uuid,
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
impl TestDisplay {
|
||||
pub fn new() -> Self {
|
||||
TestDisplay {
|
||||
id: DisplayId(1),
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
bounds: Bounds::from_corners(Point::default(), Point::new(px(1920.), px(1080.))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDisplay for TestDisplay {
|
||||
fn id(&self) -> crate::DisplayId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn uuid(&self) -> Result<uuid::Uuid> {
|
||||
Ok(self.uuid)
|
||||
}
|
||||
|
||||
fn bounds(&self) -> crate::Bounds<crate::Pixels> {
|
||||
self.bounds
|
||||
}
|
||||
}
|
||||
+742
@@ -0,0 +1,742 @@
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use crate::NoopTextSystem;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use crate::PathPromptOptions;
|
||||
use crate::{
|
||||
ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, OwnedMenu, Platform, PlatformDisplay,
|
||||
PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SharedString,
|
||||
SourceMetadata, SystemNotification, SystemNotificationResponse, Task, TestDisplay, TestWindow,
|
||||
ThermalState, WindowAppearance, WindowParams, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use collections::VecDeque;
|
||||
use futures::channel::oneshot;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
path::{Path, PathBuf},
|
||||
rc::{Rc, Weak},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// TestPlatform implements the Platform trait for use in tests.
|
||||
pub(crate) struct TestPlatform {
|
||||
background_executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
|
||||
pub(crate) active_window: RefCell<Option<TestWindow>>,
|
||||
active_display: Rc<dyn PlatformDisplay>,
|
||||
active_cursor: Mutex<CursorStyle>,
|
||||
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
current_primary_item: Mutex<Option<ClipboardItem>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
current_find_pasteboard_item: Mutex<Option<ClipboardItem>>,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) prompts: RefCell<TestPrompts>,
|
||||
screen_capture_sources: RefCell<Vec<TestScreenCaptureSource>>,
|
||||
pub opened_url: RefCell<Option<String>>,
|
||||
pub(crate) system_notifications: RefCell<TestSystemNotifications>,
|
||||
pub text_system: Arc<dyn PlatformTextSystem>,
|
||||
pub expect_restart:
|
||||
RefCell<Option<oneshot::Sender<(Option<PathBuf>, Vec<std::ffi::OsString>)>>>,
|
||||
idle_sleep_prevention_count: Arc<AtomicUsize>,
|
||||
idle_sleep_prevention_delay: Cell<Duration>,
|
||||
idle_sleep_prevention_fails: Cell<bool>,
|
||||
headless_renderer_factory:
|
||||
Option<Box<dyn Fn() -> anyhow::Result<Option<Box<dyn PlatformHeadlessRenderer>>>>>,
|
||||
weak: Weak<Self>,
|
||||
menus: RefCell<Vec<OwnedMenu>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// A fake screen capture source, used for testing.
|
||||
pub struct TestScreenCaptureSource {}
|
||||
|
||||
/// A fake screen capture stream, used for testing.
|
||||
pub struct TestScreenCaptureStream {}
|
||||
|
||||
impl ScreenCaptureSource for TestScreenCaptureSource {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
Ok(SourceMetadata {
|
||||
id: 0,
|
||||
is_main: None,
|
||||
label: None,
|
||||
resolution: size(DevicePixels(1), DevicePixels(1)),
|
||||
})
|
||||
}
|
||||
|
||||
fn stream(
|
||||
&self,
|
||||
_foreground_executor: &ForegroundExecutor,
|
||||
_frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
||||
) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
|
||||
let (mut tx, rx) = oneshot::channel();
|
||||
let stream = TestScreenCaptureStream {};
|
||||
tx.send(Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>))
|
||||
.ok();
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenCaptureStream for TestScreenCaptureStream {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
TestScreenCaptureSource {}.metadata()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
struct TestPrompt {
|
||||
msg: String,
|
||||
detail: Option<String>,
|
||||
answers: Vec<String>,
|
||||
tx: oneshot::Sender<usize>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TestSystemNotifications {
|
||||
pub(crate) app_identity: Option<(SharedString, SharedString)>,
|
||||
pub(crate) shown: Vec<SystemNotification>,
|
||||
pub(crate) delivered: Vec<SystemNotification>,
|
||||
pub(crate) dismissed: Vec<SharedString>,
|
||||
response_callback: Option<Box<dyn FnMut(SystemNotificationResponse)>>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TestPrompts {
|
||||
multiple_choice: VecDeque<TestPrompt>,
|
||||
new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
|
||||
paths: VecDeque<(
|
||||
PathPromptOptions,
|
||||
oneshot::Sender<Result<Option<Vec<PathBuf>>>>,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl TestPlatform {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc<Self> {
|
||||
Self::with_platform(
|
||||
executor,
|
||||
foreground_executor,
|
||||
Arc::new(NoopTextSystem),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn with_text_system(
|
||||
executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
text_system: Arc<dyn PlatformTextSystem>,
|
||||
) -> Rc<Self> {
|
||||
Self::with_platform(executor, foreground_executor, text_system, None)
|
||||
}
|
||||
|
||||
pub fn with_platform(
|
||||
executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
text_system: Arc<dyn PlatformTextSystem>,
|
||||
headless_renderer_factory: Option<
|
||||
Box<dyn Fn() -> anyhow::Result<Option<Box<dyn PlatformHeadlessRenderer>>>>,
|
||||
>,
|
||||
) -> Rc<Self> {
|
||||
Rc::new_cyclic(|weak| TestPlatform {
|
||||
background_executor: executor,
|
||||
foreground_executor,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
prompts: Default::default(),
|
||||
screen_capture_sources: Default::default(),
|
||||
active_cursor: Default::default(),
|
||||
active_display: Rc::new(TestDisplay::new()),
|
||||
active_window: Default::default(),
|
||||
expect_restart: Default::default(),
|
||||
current_clipboard_item: Mutex::new(None),
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
current_primary_item: Mutex::new(None),
|
||||
#[cfg(target_os = "macos")]
|
||||
current_find_pasteboard_item: Mutex::new(None),
|
||||
idle_sleep_prevention_count: Arc::new(AtomicUsize::new(0)),
|
||||
idle_sleep_prevention_delay: Cell::new(Duration::ZERO),
|
||||
idle_sleep_prevention_fails: Cell::new(false),
|
||||
weak: weak.clone(),
|
||||
opened_url: Default::default(),
|
||||
system_notifications: Default::default(),
|
||||
text_system,
|
||||
headless_renderer_factory,
|
||||
menus: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn simulate_new_path_selection(
|
||||
&self,
|
||||
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
|
||||
) {
|
||||
let (path, tx) = self
|
||||
.prompts
|
||||
.borrow_mut()
|
||||
.new_path
|
||||
.pop_front()
|
||||
.expect("no pending new path prompt");
|
||||
tx.send(Ok(select_path(&path))).ok();
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn simulate_path_prompt_response(
|
||||
&self,
|
||||
select_paths: impl FnOnce(&PathPromptOptions) -> Option<Vec<std::path::PathBuf>>,
|
||||
) {
|
||||
let (options, tx) = self
|
||||
.prompts
|
||||
.borrow_mut()
|
||||
.paths
|
||||
.pop_front()
|
||||
.expect("no pending paths prompt");
|
||||
let selection = select_paths(&options);
|
||||
if let Some(paths) = &selection
|
||||
&& !options.multiple
|
||||
&& paths.len() > 1
|
||||
{
|
||||
panic!(
|
||||
"selected {} paths for a prompt that does not allow multiple selection",
|
||||
paths.len()
|
||||
);
|
||||
}
|
||||
tx.send(Ok(selection)).ok();
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn did_prompt_for_paths(&self) -> bool {
|
||||
!self.prompts.borrow().paths.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub(crate) fn simulate_prompt_answer(&self, response: &str) {
|
||||
let prompt = self
|
||||
.prompts
|
||||
.borrow_mut()
|
||||
.multiple_choice
|
||||
.pop_front()
|
||||
.expect("no pending multiple choice prompt");
|
||||
let Some(ix) = prompt.answers.iter().position(|a| a == response) else {
|
||||
panic!(
|
||||
"PROMPT: {}\n{:?}\n{:?}\nCannot respond with {}",
|
||||
prompt.msg, prompt.detail, prompt.answers, response
|
||||
)
|
||||
};
|
||||
prompt.tx.send(ix).ok();
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn has_pending_prompt(&self) -> bool {
|
||||
!self.prompts.borrow().multiple_choice.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn pending_prompt(&self) -> Option<(String, String)> {
|
||||
let prompts = self.prompts.borrow();
|
||||
let prompt = prompts.multiple_choice.front()?;
|
||||
Some((
|
||||
prompt.msg.clone(),
|
||||
prompt.detail.clone().unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn set_screen_capture_sources(&self, sources: Vec<TestScreenCaptureSource>) {
|
||||
*self.screen_capture_sources.borrow_mut() = sources;
|
||||
}
|
||||
|
||||
/// Queues the prompt so a test can later inspect or answer it through
|
||||
/// [`Self::pending_prompt`] and [`Self::simulate_prompt_answer`].
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn prompt(
|
||||
&self,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[PromptButton],
|
||||
) -> oneshot::Receiver<usize> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let answers: Vec<String> = answers.iter().map(|s| s.label().to_string()).collect();
|
||||
self.prompts
|
||||
.borrow_mut()
|
||||
.multiple_choice
|
||||
.push_back(TestPrompt {
|
||||
msg: msg.to_string(),
|
||||
detail: detail.map(|s| s.to_string()),
|
||||
answers,
|
||||
tx,
|
||||
});
|
||||
rx
|
||||
}
|
||||
|
||||
/// Benchmarks have no API to answer a prompt, so this doesn't retain it
|
||||
/// for later inspection; dropping the sender immediately cancels the
|
||||
/// returned receiver instead of leaving it pending indefinitely.
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
pub(crate) fn prompt(
|
||||
&self,
|
||||
_msg: &str,
|
||||
_detail: Option<&str>,
|
||||
_answers: &[PromptButton],
|
||||
) -> oneshot::Receiver<usize> {
|
||||
oneshot::channel().1
|
||||
}
|
||||
|
||||
pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
|
||||
let executor = self.foreground_executor();
|
||||
let previous_window = self.active_window.borrow_mut().take();
|
||||
self.active_window.borrow_mut().clone_from(&window);
|
||||
|
||||
executor
|
||||
.spawn(async move {
|
||||
if let Some(previous_window) = previous_window {
|
||||
if let Some(window) = window.as_ref()
|
||||
&& Rc::ptr_eq(&previous_window.0, &window.0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
previous_window.simulate_active_status_change(false);
|
||||
}
|
||||
if let Some(window) = window {
|
||||
window.simulate_active_status_change(true);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn did_prompt_for_new_path(&self) -> bool {
|
||||
!self.prompts.borrow().new_path.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn active_idle_sleep_preventions(&self) -> usize {
|
||||
self.idle_sleep_prevention_count.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn set_idle_sleep_prevention_delay(&self, delay: Duration) {
|
||||
self.idle_sleep_prevention_delay.set(delay);
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn set_idle_sleep_prevention_fails(&self, fails: bool) {
|
||||
self.idle_sleep_prevention_fails.set(fails);
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn app_identity(&self) -> Option<(SharedString, SharedString)> {
|
||||
self.system_notifications.borrow().app_identity.clone()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn shown_system_notifications(&self) -> Vec<SystemNotification> {
|
||||
self.system_notifications.borrow().shown.clone()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn delivered_system_notifications(&self) -> Vec<SystemNotification> {
|
||||
self.system_notifications.borrow().delivered.clone()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn dismissed_system_notifications(&self) -> Vec<SharedString> {
|
||||
self.system_notifications.borrow().dismissed.clone()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn simulate_system_notification_response(
|
||||
&self,
|
||||
response: SystemNotificationResponse,
|
||||
) {
|
||||
let callback = self
|
||||
.system_notifications
|
||||
.borrow_mut()
|
||||
.response_callback
|
||||
.take();
|
||||
if let Some(mut callback) = callback {
|
||||
callback(response);
|
||||
self.system_notifications
|
||||
.borrow_mut()
|
||||
.response_callback
|
||||
.get_or_insert(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform for TestPlatform {
|
||||
fn background_executor(&self) -> BackgroundExecutor {
|
||||
self.background_executor.clone()
|
||||
}
|
||||
|
||||
fn foreground_executor(&self) -> ForegroundExecutor {
|
||||
self.foreground_executor.clone()
|
||||
}
|
||||
|
||||
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
|
||||
self.text_system.clone()
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||
Box::new(TestKeyboardLayout)
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(DummyKeyboardMapper)
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_thermal_state_change(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn thermal_state(&self) -> ThermalState {
|
||||
ThermalState::Nominal
|
||||
}
|
||||
|
||||
fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>> {
|
||||
let count = self.idle_sleep_prevention_count.clone();
|
||||
let fails = self.idle_sleep_prevention_fails.get();
|
||||
let reason = reason.to_owned();
|
||||
let acquire = move || {
|
||||
if fails {
|
||||
anyhow::bail!("Idle sleep prevention for {reason:?} is set to fail in this test");
|
||||
}
|
||||
count.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(ActivityGuard::new(move || {
|
||||
count.fetch_sub(1, Ordering::SeqCst);
|
||||
}))
|
||||
};
|
||||
|
||||
let delay = self.idle_sleep_prevention_delay.get();
|
||||
if delay.is_zero() {
|
||||
Task::ready(acquire())
|
||||
} else {
|
||||
let delay = self.background_executor.timer(delay);
|
||||
self.foreground_executor.spawn(async move {
|
||||
delay.await;
|
||||
acquire()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn quit(&self) {}
|
||||
|
||||
fn restart(&self, path: Option<PathBuf>, arguments: Vec<std::ffi::OsString>) {
|
||||
if let Some(tx) = self.expect_restart.take() {
|
||||
tx.send((path, arguments)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn activate(&self, _ignoring_other_apps: bool) {
|
||||
//
|
||||
}
|
||||
|
||||
fn hide(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn hide_other_apps(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn unhide_other_apps(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn displays(&self) -> Vec<std::rc::Rc<dyn crate::PlatformDisplay>> {
|
||||
vec![self.active_display.clone()]
|
||||
}
|
||||
|
||||
fn primary_display(&self) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
|
||||
Some(self.active_display.clone())
|
||||
}
|
||||
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
let (mut tx, rx) = oneshot::channel();
|
||||
tx.send(Ok(self
|
||||
.screen_capture_sources
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|source| Rc::new(source.clone()) as Rc<dyn ScreenCaptureSource>)
|
||||
.collect()))
|
||||
.ok();
|
||||
rx
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<crate::AnyWindowHandle> {
|
||||
self.active_window
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|window| window.0.lock().handle)
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
params: WindowParams,
|
||||
) -> anyhow::Result<Box<dyn crate::PlatformWindow>> {
|
||||
let renderer = match self.headless_renderer_factory.as_ref() {
|
||||
Some(factory) => factory()?,
|
||||
None => None,
|
||||
};
|
||||
let window = TestWindow::new(
|
||||
handle,
|
||||
params,
|
||||
self.weak.clone(),
|
||||
self.active_display.clone(),
|
||||
renderer,
|
||||
);
|
||||
Ok(Box::new(window))
|
||||
}
|
||||
|
||||
fn window_appearance(&self) -> WindowAppearance {
|
||||
WindowAppearance::Light
|
||||
}
|
||||
|
||||
fn open_url(&self, url: &str) {
|
||||
*self.opened_url.borrow_mut() = Some(url.to_string())
|
||||
}
|
||||
|
||||
fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Queues the prompt so a test can later answer it through
|
||||
/// [`Self::simulate_path_prompt_response`].
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
options: crate::PathPromptOptions,
|
||||
) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.prompts.borrow_mut().paths.push_back((options, tx));
|
||||
rx
|
||||
}
|
||||
|
||||
/// Benchmarks have no API to answer a path prompt, so this doesn't
|
||||
/// retain it for later inspection; dropping the sender immediately
|
||||
/// cancels the returned receiver instead of leaving it pending
|
||||
/// indefinitely.
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
_options: crate::PathPromptOptions,
|
||||
) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
|
||||
oneshot::channel().1
|
||||
}
|
||||
|
||||
/// Queues the prompt so a test can later answer it through
|
||||
/// [`Self::simulate_new_path_selection`].
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn prompt_for_new_path(
|
||||
&self,
|
||||
directory: &std::path::Path,
|
||||
_suggested_name: Option<&str>,
|
||||
) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.prompts
|
||||
.borrow_mut()
|
||||
.new_path
|
||||
.push_back((directory.to_path_buf(), tx));
|
||||
rx
|
||||
}
|
||||
|
||||
/// Benchmarks have no API to answer a new-path prompt, so this doesn't
|
||||
/// retain it for later inspection; dropping the sender immediately
|
||||
/// cancels the returned receiver instead of leaving it pending
|
||||
/// indefinitely.
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
fn prompt_for_new_path(
|
||||
&self,
|
||||
_directory: &std::path::Path,
|
||||
_suggested_name: Option<&str>,
|
||||
) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
|
||||
oneshot::channel().1
|
||||
}
|
||||
|
||||
fn can_select_mixed_files_and_dirs(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn reveal_path(&self, _path: &std::path::Path) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_quit(&self, _callback: Box<dyn FnMut() -> bool>) {}
|
||||
|
||||
fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_system_sleep(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn set_app_identity(&self, identifier: &str, name: &str) {
|
||||
self.system_notifications.borrow_mut().app_identity =
|
||||
Some((identifier.to_string().into(), name.to_string().into()));
|
||||
}
|
||||
|
||||
fn show_system_notification(&self, notification: SystemNotification) {
|
||||
let mut system_notifications = self.system_notifications.borrow_mut();
|
||||
if system_notifications.app_identity.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let delivered = system_notifications
|
||||
.delivered
|
||||
.iter_mut()
|
||||
.find(|delivered| delivered.tag == notification.tag);
|
||||
if let Some(delivered) = delivered {
|
||||
*delivered = notification.clone();
|
||||
} else {
|
||||
system_notifications.delivered.push(notification.clone());
|
||||
}
|
||||
system_notifications.shown.push(notification);
|
||||
}
|
||||
|
||||
fn dismiss_system_notification(&self, tag: &str) {
|
||||
let mut system_notifications = self.system_notifications.borrow_mut();
|
||||
system_notifications
|
||||
.delivered
|
||||
.retain(|notification| notification.tag != tag);
|
||||
system_notifications
|
||||
.dismissed
|
||||
.push(SharedString::from(tag.to_string()));
|
||||
}
|
||||
|
||||
fn on_system_notification_response(
|
||||
&self,
|
||||
callback: Box<dyn FnMut(SystemNotificationResponse)>,
|
||||
) {
|
||||
self.system_notifications.borrow_mut().response_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn set_menus(&self, menus: Vec<crate::Menu>, _keymap: &Keymap) {
|
||||
*self.menus.borrow_mut() = menus.into_iter().map(|menu| menu.owned()).collect()
|
||||
}
|
||||
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
Some(self.menus.borrow().clone())
|
||||
}
|
||||
|
||||
fn set_dock_menu(&self, _menu: Vec<crate::MenuItem>, _keymap: &Keymap) {}
|
||||
|
||||
fn add_recent_document(&self, _paths: &Path) {}
|
||||
|
||||
fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
|
||||
|
||||
fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
|
||||
|
||||
fn app_path(&self) -> Result<std::path::PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<std::path::PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_cursor_style(&self, style: crate::CursorStyle) {
|
||||
*self.active_cursor.lock() = style;
|
||||
}
|
||||
|
||||
fn hide_cursor_until_mouse_moves(&self) {}
|
||||
|
||||
fn is_cursor_visible(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn should_auto_hide_scrollbars(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.current_clipboard_item.lock().clone()
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
*self.current_clipboard_item.lock() = Some(item);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem> {
|
||||
self.current_primary_item.lock().clone()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn write_to_primary(&self, item: ClipboardItem) {
|
||||
*self.current_primary_item.lock() = Some(item);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
|
||||
self.current_find_pasteboard_item.lock().clone()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn write_to_find_pasteboard(&self, item: ClipboardItem) {
|
||||
*self.current_find_pasteboard_item.lock() = Some(item);
|
||||
}
|
||||
|
||||
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn open_with_system(&self, _path: &Path) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl TestScreenCaptureSource {
|
||||
/// Create a fake screen capture source, for testing.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
struct TestKeyboardLayout;
|
||||
|
||||
impl PlatformKeyboardLayout for TestKeyboardLayout {
|
||||
fn id(&self) -> &str {
|
||||
"zed.keyboard.example"
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"zed.keyboard.example"
|
||||
}
|
||||
}
|
||||
+591
@@ -0,0 +1,591 @@
|
||||
use crate::{
|
||||
AnyWindowHandle, Bounds, DevicePixels, DispatchEventResult, GpuSpecs, HeadlessAtlas, Pixels,
|
||||
PlatformAtlas, PlatformDisplay, PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler,
|
||||
PlatformWindow, Point, PromptButton, RequestFrameOptions, Scene, Size, TestPlatform,
|
||||
TextInputConfiguration, TextInputStateChange, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowControlArea, WindowInsets, WindowParams, WindowVisibility,
|
||||
};
|
||||
use gpui_util::ResultExt as _;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use image::RgbaImage;
|
||||
use parking_lot::Mutex;
|
||||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||||
use std::{
|
||||
cell::Cell,
|
||||
path::PathBuf,
|
||||
rc::{Rc, Weak},
|
||||
sync::{self, Arc},
|
||||
};
|
||||
|
||||
pub(crate) struct TestWindowState {
|
||||
pub(crate) bounds: Bounds<Pixels>,
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) edited: bool,
|
||||
pub(crate) document_path: Option<std::path::PathBuf>,
|
||||
platform: Weak<TestPlatform>,
|
||||
// TODO: Replace with `Rc`
|
||||
sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||
renderer: Option<Box<dyn PlatformHeadlessRenderer>>,
|
||||
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
|
||||
hit_test_window_control_callback: Option<Box<dyn FnMut() -> Option<WindowControlArea>>>,
|
||||
input_callback: Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
|
||||
active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
|
||||
visibility: WindowVisibility,
|
||||
visibility_callback: Option<Box<dyn FnMut(WindowVisibility)>>,
|
||||
hover_status_change_callback: Option<Box<dyn FnMut(bool)>>,
|
||||
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
visual_viewport: Option<Bounds<Pixels>>,
|
||||
visual_viewport_callback: Option<Box<dyn FnMut()>>,
|
||||
insets: WindowInsets,
|
||||
insets_callback: Option<Box<dyn FnMut(WindowInsets)>>,
|
||||
virtual_keyboard_requests: usize,
|
||||
virtual_keyboard_dismissals: usize,
|
||||
moved_callback: Option<Box<dyn FnMut()>>,
|
||||
appearance_change_callback: Option<Box<dyn FnMut()>>,
|
||||
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
|
||||
frame_wake_count: Rc<Cell<usize>>,
|
||||
frame_scheduled: bool,
|
||||
frame_callback_pending: bool,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
text_input_configurations: Vec<TextInputConfiguration>,
|
||||
text_input_state_changes: Vec<TextInputStateChange>,
|
||||
is_fullscreen: bool,
|
||||
scale_factor: f32,
|
||||
appearance: WindowAppearance,
|
||||
external_drag_files: Vec<(PathBuf, bool)>,
|
||||
start_external_drag_result: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestWindow(pub(crate) Rc<Mutex<TestWindowState>>);
|
||||
|
||||
// Test windows are not backed by a real platform window, so there is no raw
|
||||
// handle to report; `NotSupported` is `raw_window_handle`'s variant for exactly this.
|
||||
impl HasWindowHandle for TestWindow {
|
||||
fn window_handle(
|
||||
&self,
|
||||
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
|
||||
Err(raw_window_handle::HandleError::NotSupported)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasDisplayHandle for TestWindow {
|
||||
fn display_handle(
|
||||
&self,
|
||||
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
|
||||
Err(raw_window_handle::HandleError::NotSupported)
|
||||
}
|
||||
}
|
||||
|
||||
impl TestWindow {
|
||||
pub(crate) fn new(
|
||||
handle: AnyWindowHandle,
|
||||
params: WindowParams,
|
||||
platform: Weak<TestPlatform>,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
renderer: Option<Box<dyn PlatformHeadlessRenderer>>,
|
||||
) -> Self {
|
||||
let sprite_atlas: Arc<dyn PlatformAtlas> = match &renderer {
|
||||
Some(r) => r.sprite_atlas(),
|
||||
None => Arc::new(HeadlessAtlas::default()),
|
||||
};
|
||||
Self(Rc::new(Mutex::new(TestWindowState {
|
||||
bounds: params.bounds,
|
||||
display,
|
||||
platform,
|
||||
handle,
|
||||
sprite_atlas,
|
||||
renderer,
|
||||
title: Default::default(),
|
||||
edited: false,
|
||||
document_path: None,
|
||||
should_close_handler: None,
|
||||
hit_test_window_control_callback: None,
|
||||
input_callback: None,
|
||||
active_status_change_callback: None,
|
||||
visibility: WindowVisibility::Visible,
|
||||
visibility_callback: None,
|
||||
hover_status_change_callback: None,
|
||||
resize_callback: None,
|
||||
visual_viewport: None,
|
||||
visual_viewport_callback: None,
|
||||
insets: WindowInsets::default(),
|
||||
insets_callback: None,
|
||||
virtual_keyboard_requests: 0,
|
||||
virtual_keyboard_dismissals: 0,
|
||||
moved_callback: None,
|
||||
appearance_change_callback: None,
|
||||
request_frame_callback: None,
|
||||
frame_wake_count: Rc::new(Cell::new(0)),
|
||||
frame_scheduled: false,
|
||||
frame_callback_pending: false,
|
||||
input_handler: None,
|
||||
text_input_configurations: Vec::new(),
|
||||
text_input_state_changes: Vec::new(),
|
||||
is_fullscreen: false,
|
||||
// Preserve the test platform's historical 2x default.
|
||||
scale_factor: 2.0,
|
||||
appearance: WindowAppearance::Light,
|
||||
external_drag_files: Vec::new(),
|
||||
start_external_drag_result: false,
|
||||
})))
|
||||
}
|
||||
pub fn simulate_scheduled_frame(&self) -> bool {
|
||||
let callback = {
|
||||
let mut state = self.0.lock();
|
||||
if !std::mem::take(&mut state.frame_scheduled) {
|
||||
return false;
|
||||
}
|
||||
state.frame_callback_pending = false;
|
||||
state.request_frame_callback.take()
|
||||
};
|
||||
let Some(mut callback) = callback else {
|
||||
self.0.lock().frame_scheduled = true;
|
||||
return false;
|
||||
};
|
||||
|
||||
callback(RequestFrameOptions::default());
|
||||
self.0.lock().request_frame_callback = Some(callback);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn frame_scheduled(&self) -> bool {
|
||||
self.0.lock().frame_scheduled
|
||||
}
|
||||
|
||||
pub fn simulate_visibility_change(&self, visibility: WindowVisibility) {
|
||||
let callback = {
|
||||
let mut state = self.0.lock();
|
||||
state.visibility = visibility;
|
||||
state.visibility_callback.take()
|
||||
};
|
||||
if let Some(mut callback) = callback {
|
||||
callback(visibility);
|
||||
self.0.lock().visibility_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simulate_visual_viewport_change(&self, bounds: Bounds<Pixels>) {
|
||||
let callback = {
|
||||
let mut state = self.0.lock();
|
||||
state.visual_viewport = Some(bounds);
|
||||
state.visual_viewport_callback.take()
|
||||
};
|
||||
if let Some(mut callback) = callback {
|
||||
callback();
|
||||
self.0.lock().visual_viewport_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simulate_insets_change(&self, insets: WindowInsets) {
|
||||
let callback = {
|
||||
let mut state = self.0.lock();
|
||||
state.insets = insets.clone();
|
||||
state.insets_callback.take()
|
||||
};
|
||||
if let Some(mut callback) = callback {
|
||||
callback(insets);
|
||||
self.0.lock().insets_callback = Some(callback);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn virtual_keyboard_requests(&self) -> usize {
|
||||
self.0.lock().virtual_keyboard_requests
|
||||
}
|
||||
|
||||
pub fn virtual_keyboard_dismissals(&self) -> usize {
|
||||
self.0.lock().virtual_keyboard_dismissals
|
||||
}
|
||||
|
||||
/// Every [`TextInputConfiguration`] forwarded to this window, in order.
|
||||
pub fn text_input_configurations(&self) -> Vec<TextInputConfiguration> {
|
||||
self.0.lock().text_input_configurations.clone()
|
||||
}
|
||||
|
||||
pub fn text_input_state_changes(&self) -> Vec<TextInputStateChange> {
|
||||
self.0.lock().text_input_state_changes.clone()
|
||||
}
|
||||
|
||||
pub fn simulate_resize(&mut self, size: Size<Pixels>) {
|
||||
let scale_factor = self.scale_factor();
|
||||
let mut lock = self.0.lock();
|
||||
// Always update bounds, even if no callback is registered
|
||||
lock.bounds.size = size;
|
||||
let Some(mut callback) = lock.resize_callback.take() else {
|
||||
return;
|
||||
};
|
||||
drop(lock);
|
||||
callback(size, scale_factor);
|
||||
self.0.lock().resize_callback = Some(callback);
|
||||
}
|
||||
|
||||
/// Simulates a display scale change through the resize callback, preserving logical bounds.
|
||||
pub fn simulate_scale_factor_change(&mut self, scale_factor: f32) {
|
||||
let size = {
|
||||
let mut lock = self.0.lock();
|
||||
lock.scale_factor = scale_factor;
|
||||
lock.bounds.size
|
||||
};
|
||||
self.simulate_resize(size);
|
||||
}
|
||||
|
||||
pub(crate) fn simulate_active_status_change(&self, active: bool) {
|
||||
let mut lock = self.0.lock();
|
||||
let Some(mut callback) = lock.active_status_change_callback.take() else {
|
||||
return;
|
||||
};
|
||||
drop(lock);
|
||||
callback(active);
|
||||
self.0.lock().active_status_change_callback = Some(callback);
|
||||
}
|
||||
|
||||
pub fn simulate_appearance_change(&self, appearance: WindowAppearance) {
|
||||
let mut lock = self.0.lock();
|
||||
lock.appearance = appearance;
|
||||
let Some(mut callback) = lock.appearance_change_callback.take() else {
|
||||
return;
|
||||
};
|
||||
drop(lock);
|
||||
callback();
|
||||
self.0.lock().appearance_change_callback = Some(callback);
|
||||
}
|
||||
|
||||
/// Returns how many times this window's frame waker has been invoked.
|
||||
pub fn frame_wake_count(&self) -> usize {
|
||||
self.0.lock().frame_wake_count.get()
|
||||
}
|
||||
|
||||
/// Delivers a frame request to the window, as the platform's frame source
|
||||
/// would.
|
||||
pub fn simulate_frame_request(&self, options: RequestFrameOptions) {
|
||||
let mut lock = self.0.lock();
|
||||
let Some(mut callback) = lock.request_frame_callback.take() else {
|
||||
return;
|
||||
};
|
||||
drop(lock);
|
||||
callback(options);
|
||||
self.0.lock().request_frame_callback = Some(callback);
|
||||
}
|
||||
|
||||
pub fn simulate_input(&mut self, event: PlatformInput) -> bool {
|
||||
let mut lock = self.0.lock();
|
||||
let Some(mut callback) = lock.input_callback.take() else {
|
||||
return false;
|
||||
};
|
||||
drop(lock);
|
||||
let result = callback(event);
|
||||
self.0.lock().input_callback = Some(callback);
|
||||
!result.propagate
|
||||
}
|
||||
|
||||
pub fn external_drag_files(&self) -> Vec<(PathBuf, bool)> {
|
||||
self.0.lock().external_drag_files.clone()
|
||||
}
|
||||
|
||||
pub fn set_start_external_drag_result(&self, result: bool) {
|
||||
self.0.lock().start_external_drag_result = result;
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformWindow for TestWindow {
|
||||
fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
|
||||
let state = self.0.lock();
|
||||
state
|
||||
.visual_viewport
|
||||
.unwrap_or_else(|| Bounds::new(Point::default(), state.bounds.size))
|
||||
}
|
||||
|
||||
fn on_visual_viewport_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.lock().visual_viewport_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn insets(&self) -> WindowInsets {
|
||||
self.0.lock().insets.clone()
|
||||
}
|
||||
|
||||
fn on_insets_changed(&self, callback: Box<dyn FnMut(WindowInsets)>) {
|
||||
self.0.lock().insets_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn show_soft_keyboard(&self) {
|
||||
self.0.lock().virtual_keyboard_requests += 1;
|
||||
}
|
||||
|
||||
fn hide_soft_keyboard(&self) {
|
||||
self.0.lock().virtual_keyboard_dismissals += 1;
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.0.lock().bounds
|
||||
}
|
||||
|
||||
fn window_bounds(&self) -> WindowBounds {
|
||||
WindowBounds::Windowed(self.bounds())
|
||||
}
|
||||
|
||||
fn is_maximized(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.bounds().size
|
||||
}
|
||||
|
||||
fn resize(&mut self, size: Size<Pixels>) {
|
||||
let mut lock = self.0.lock();
|
||||
lock.bounds.size = size;
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.0.lock().scale_factor
|
||||
}
|
||||
|
||||
fn appearance(&self) -> WindowAppearance {
|
||||
self.0.lock().appearance
|
||||
}
|
||||
|
||||
fn display(&self) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
|
||||
Some(self.0.lock().display.clone())
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
Point::default()
|
||||
}
|
||||
|
||||
fn modifiers(&self) -> crate::Modifiers {
|
||||
crate::Modifiers::default()
|
||||
}
|
||||
|
||||
fn capslock(&self) -> crate::Capslock {
|
||||
crate::Capslock::default()
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||
self.0.lock().input_handler = Some(input_handler);
|
||||
}
|
||||
|
||||
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
|
||||
self.0.lock().input_handler.take()
|
||||
}
|
||||
|
||||
fn set_text_input_configuration(&mut self, configuration: TextInputConfiguration) {
|
||||
self.0.lock().text_input_configurations.push(configuration);
|
||||
}
|
||||
|
||||
fn text_input_state_changed(&self, change: TextInputStateChange) {
|
||||
self.0.lock().text_input_state_changes.push(change);
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_level: crate::PromptLevel,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[PromptButton],
|
||||
) -> Option<futures::channel::oneshot::Receiver<usize>> {
|
||||
Some(
|
||||
self.0
|
||||
.lock()
|
||||
.platform
|
||||
.upgrade()
|
||||
.expect("platform dropped")
|
||||
.prompt(msg, detail, answers),
|
||||
)
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
self.0
|
||||
.lock()
|
||||
.platform
|
||||
.upgrade()
|
||||
.unwrap()
|
||||
.set_active_window(Some(self.clone()))
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn visibility(&self) -> WindowVisibility {
|
||||
self.0.lock().visibility
|
||||
}
|
||||
|
||||
fn is_hovered(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn background_appearance(&self) -> WindowBackgroundAppearance {
|
||||
WindowBackgroundAppearance::Opaque
|
||||
}
|
||||
|
||||
fn is_subpixel_rendering_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.0.lock().title = Some(title.to_owned());
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {}
|
||||
|
||||
fn set_edited(&mut self, edited: bool) {
|
||||
self.0.lock().edited = edited;
|
||||
}
|
||||
|
||||
fn set_document_path(&self, path: Option<&std::path::Path>) {
|
||||
self.0.lock().document_path = path.map(|p| p.to_path_buf());
|
||||
}
|
||||
|
||||
fn show_character_palette(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn zoom(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn toggle_fullscreen(&self) {
|
||||
let mut lock = self.0.lock();
|
||||
lock.is_fullscreen = !lock.is_fullscreen;
|
||||
}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
self.0.lock().is_fullscreen
|
||||
}
|
||||
|
||||
fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
|
||||
// Tests can inspect wakes without delivering a frame synchronously.
|
||||
let frame_wake_count = self.0.lock().frame_wake_count.clone();
|
||||
#[cfg(feature = "bench-support")]
|
||||
let window = Rc::downgrade(&self.0);
|
||||
Some(Rc::new(move || {
|
||||
frame_wake_count.set(frame_wake_count.get() + 1);
|
||||
#[cfg(feature = "bench-support")]
|
||||
if let Some(window) = window.upgrade() {
|
||||
TestWindow(window).schedule_frame();
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>) {
|
||||
self.0.lock().request_frame_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn schedule_frame(&self) {
|
||||
let mut state = self.0.lock();
|
||||
if !state.frame_callback_pending {
|
||||
state.frame_scheduled = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn on_input(&self, callback: Box<dyn FnMut(crate::PlatformInput) -> DispatchEventResult>) {
|
||||
self.0.lock().input_callback = Some(callback)
|
||||
}
|
||||
|
||||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.lock().active_status_change_callback = Some(callback)
|
||||
}
|
||||
|
||||
fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>) {
|
||||
self.0.lock().visibility_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.lock().hover_status_change_callback = Some(callback)
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.0.lock().resize_callback = Some(callback)
|
||||
}
|
||||
|
||||
fn on_moved(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.lock().moved_callback = Some(callback)
|
||||
}
|
||||
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||||
self.0.lock().should_close_handler = Some(callback);
|
||||
}
|
||||
|
||||
fn on_close(&self, _callback: Box<dyn FnOnce()>) {}
|
||||
|
||||
fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
|
||||
self.0.lock().hit_test_window_control_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.lock().appearance_change_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &Scene) {
|
||||
let scale_factor = self.scale_factor();
|
||||
let mut state = self.0.lock();
|
||||
state.frame_callback_pending = true;
|
||||
state.frame_scheduled = true;
|
||||
let device_size: Size<DevicePixels> = state.bounds.size.to_device_pixels(scale_factor);
|
||||
if let Some(renderer) = &mut state.renderer {
|
||||
renderer.render_scene(scene, device_size).warn_on_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
|
||||
self.0.lock().sprite_atlas.clone()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn render_to_image(&self, scene: &Scene) -> anyhow::Result<RgbaImage> {
|
||||
let scale_factor = self.scale_factor();
|
||||
let mut state = self.0.lock();
|
||||
let size = state.bounds.size;
|
||||
if let Some(renderer) = &mut state.renderer {
|
||||
let device_size: Size<DevicePixels> = size.to_device_pixels(scale_factor);
|
||||
renderer.render_scene_to_image(scene, device_size)
|
||||
} else {
|
||||
anyhow::bail!("render_to_image not available: no HeadlessRenderer configured")
|
||||
}
|
||||
}
|
||||
|
||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn show_window_menu(&self, _position: Point<Pixels>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn start_window_move(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn can_start_external_drag(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn start_external_drag(&self, payload: &crate::ExternalDragPayload) -> bool {
|
||||
let mut state = self.0.lock();
|
||||
match payload {
|
||||
crate::ExternalDragPayload::Files(paths) => {
|
||||
state.external_drag_files.extend_from_slice(paths.entries());
|
||||
}
|
||||
}
|
||||
state.start_external_drag_result
|
||||
}
|
||||
|
||||
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {}
|
||||
|
||||
fn gpu_specs(&self) -> Option<GpuSpecs> {
|
||||
None
|
||||
}
|
||||
}
|
||||
+744
@@ -0,0 +1,744 @@
|
||||
use std::{
|
||||
collections::BinaryHeap,
|
||||
sync::Arc,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use parking_lot::{Condvar, Mutex};
|
||||
|
||||
use crate::{
|
||||
PlatformDispatcher, Priority, RunnableVariant, profiler,
|
||||
queue::{PriorityQueueReceiver, PriorityQueueSender},
|
||||
};
|
||||
|
||||
const MIN_THREADS: usize = 2;
|
||||
|
||||
/// A multithreaded [`PlatformDispatcher`] for tests and benchmarks.
|
||||
///
|
||||
/// Background tasks run in parallel on a pool of worker threads and timers fire
|
||||
/// in real time on a dedicated timer thread, mirroring the production
|
||||
/// dispatchers (see `LinuxDispatcher`). Main-thread tasks are queued until the
|
||||
/// creating thread drains them via [`Self::run_until_idle`], since there is no
|
||||
/// platform run loop pumping them.
|
||||
///
|
||||
/// Unlike [`TestDispatcher`](crate::TestDispatcher), which runs everything on a
|
||||
/// single thread with a virtual clock, work dispatched through this dispatcher
|
||||
/// executes with production concurrency.
|
||||
pub struct ThreadedDispatcher {
|
||||
background_sender: PriorityQueueSender<RunnableVariant>,
|
||||
main_sender: PriorityQueueSender<RunnableVariant>,
|
||||
main_receiver: Mutex<PriorityQueueReceiver<RunnableVariant>>,
|
||||
timers: Arc<TimerQueue>,
|
||||
idle: Arc<IdleTracker>,
|
||||
main_thread_id: thread::ThreadId,
|
||||
}
|
||||
|
||||
/// Tracks how many background and timer runnables are queued or running so
|
||||
/// [`ThreadedDispatcher::run_until_idle`] knows when to stop waiting.
|
||||
#[derive(Default)]
|
||||
struct IdleTracker {
|
||||
inflight: Mutex<usize>,
|
||||
condvar: Condvar,
|
||||
}
|
||||
|
||||
impl IdleTracker {
|
||||
fn increment(&self) {
|
||||
*self.inflight.lock() += 1;
|
||||
}
|
||||
|
||||
fn decrement(&self) {
|
||||
let mut inflight = self.inflight.lock();
|
||||
*inflight -= 1;
|
||||
if *inflight == 0 {
|
||||
self.condvar.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a guard that decrements the in-flight count when dropped, so
|
||||
/// the count stays correct even if the runnable being executed panics.
|
||||
fn decrement_on_drop(&self) -> impl Drop + '_ {
|
||||
gpui_util::defer(|| self.decrement())
|
||||
}
|
||||
|
||||
/// Notifies waiters while holding the in-flight lock. `run_until_idle`
|
||||
/// re-checks its wake conditions under this lock before waiting, so the
|
||||
/// notification can't slip between its check and its wait and be lost.
|
||||
fn notify_under_lock(&self) {
|
||||
let _inflight = self.inflight.lock();
|
||||
self.condvar.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
struct TimerQueue {
|
||||
state: Mutex<TimerQueueState>,
|
||||
condvar: Condvar,
|
||||
}
|
||||
|
||||
struct TimerQueueState {
|
||||
heap: BinaryHeap<TimerEntry>,
|
||||
next_seq: u64,
|
||||
}
|
||||
|
||||
struct TimerEntry {
|
||||
due: Instant,
|
||||
seq: u64,
|
||||
runnable: RunnableVariant,
|
||||
}
|
||||
|
||||
impl PartialEq for TimerEntry {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.due == other.due && self.seq == other.seq
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for TimerEntry {}
|
||||
|
||||
impl PartialOrd for TimerEntry {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for TimerEntry {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
// Reversed so that the entry with the earliest due time (breaking ties
|
||||
// by insertion order) is at the top of the max-heap.
|
||||
other
|
||||
.due
|
||||
.cmp(&self.due)
|
||||
.then_with(|| other.seq.cmp(&self.seq))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ThreadedDispatcher {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadedDispatcher {
|
||||
/// Creates a dispatcher whose main thread is the calling thread.
|
||||
///
|
||||
/// Worker and timer threads live for the lifetime of the process; the
|
||||
/// dispatcher is expected to be created once and reused.
|
||||
pub fn new() -> Self {
|
||||
let (background_sender, background_receiver) = PriorityQueueReceiver::new();
|
||||
let (main_sender, main_receiver) = PriorityQueueReceiver::new();
|
||||
let idle = Arc::new(IdleTracker::default());
|
||||
|
||||
let thread_count =
|
||||
thread::available_parallelism().map_or(MIN_THREADS, |i| i.get().max(MIN_THREADS));
|
||||
for i in 0..thread_count {
|
||||
let mut receiver: PriorityQueueReceiver<RunnableVariant> = background_receiver.clone();
|
||||
let idle = idle.clone();
|
||||
thread::Builder::new()
|
||||
.name(format!("ThreadedDispatcherWorker-{i}"))
|
||||
.spawn(move || {
|
||||
while let Ok(runnable) = receiver.pop() {
|
||||
let _decrement = idle.decrement_on_drop();
|
||||
let location = runnable.metadata().location;
|
||||
let spawned = runnable.metadata().spawned;
|
||||
profiler::update_running_task(spawned, location);
|
||||
runnable.run();
|
||||
profiler::save_task_timing();
|
||||
}
|
||||
})
|
||||
.expect("failed to spawn threaded dispatcher worker");
|
||||
}
|
||||
drop(background_receiver);
|
||||
|
||||
let timers = Arc::new(TimerQueue {
|
||||
state: Mutex::new(TimerQueueState {
|
||||
heap: BinaryHeap::new(),
|
||||
next_seq: 0,
|
||||
}),
|
||||
condvar: Condvar::new(),
|
||||
});
|
||||
{
|
||||
let timers = timers.clone();
|
||||
let idle = idle.clone();
|
||||
thread::Builder::new()
|
||||
.name("ThreadedDispatcherTimer".to_owned())
|
||||
.spawn(move || {
|
||||
let mut state = timers.state.lock();
|
||||
loop {
|
||||
let Some(entry) = state.heap.peek() else {
|
||||
timers.condvar.wait(&mut state);
|
||||
continue;
|
||||
};
|
||||
let due = entry.due;
|
||||
if due > Instant::now() {
|
||||
timers.condvar.wait_until(&mut state, due);
|
||||
continue;
|
||||
}
|
||||
let Some(entry) = state.heap.pop() else {
|
||||
continue;
|
||||
};
|
||||
// Count the firing timer as in-flight before releasing
|
||||
// the lock so it can spawn follow-up work that
|
||||
// `run_until_idle` will wait for. Lock order is always
|
||||
// timer state, then in-flight count; `run_until_idle`
|
||||
// never takes them in the opposite order.
|
||||
idle.increment();
|
||||
drop(state);
|
||||
|
||||
{
|
||||
let _decrement = idle.decrement_on_drop();
|
||||
let location = entry.runnable.metadata().location;
|
||||
let spawned = entry.runnable.metadata().spawned;
|
||||
profiler::update_running_task(spawned, location);
|
||||
entry.runnable.run();
|
||||
profiler::save_task_timing();
|
||||
}
|
||||
|
||||
state = timers.state.lock();
|
||||
}
|
||||
})
|
||||
.expect("failed to spawn threaded dispatcher timer");
|
||||
}
|
||||
|
||||
Self {
|
||||
background_sender,
|
||||
main_sender,
|
||||
main_receiver: Mutex::new(main_receiver),
|
||||
timers,
|
||||
idle,
|
||||
main_thread_id: thread::current().id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs queued main thread tasks and waits until no background or timer
|
||||
/// work is queued, running, or already due.
|
||||
///
|
||||
/// Timers that haven't reached their due time yet are *not* waited for:
|
||||
/// the dispatcher runs in real time and cannot skip ahead like the
|
||||
/// `TestDispatcher`'s virtual clock, so waiting on a future timer would
|
||||
/// block for its full real duration. Tasks sleeping on such timers are
|
||||
/// considered idle. Must be called on the thread that created this
|
||||
/// dispatcher.
|
||||
pub fn run_until_idle(&self) {
|
||||
self.run_until_idle_with(|| self.drain_main_queue());
|
||||
}
|
||||
|
||||
pub(crate) fn run_until_idle_with(&self, mut run_turn: impl FnMut() -> bool) {
|
||||
assert!(
|
||||
self.is_main_thread(),
|
||||
"run_until_idle must be called on the threaded dispatcher's main thread"
|
||||
);
|
||||
loop {
|
||||
if run_turn() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Checked before taking the in-flight lock; the timer thread
|
||||
// locks them in the opposite order, so nesting would deadlock.
|
||||
if self.has_due_timer() {
|
||||
// Poll briefly: a firing timer leaves the heap just before it
|
||||
// registers as in-flight.
|
||||
let mut inflight = self.idle.inflight.lock();
|
||||
self.idle
|
||||
.condvar
|
||||
.wait_for(&mut inflight, Duration::from_millis(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut inflight = self.idle.inflight.lock();
|
||||
// Re-checked under the lock that `dispatch_on_main_thread`
|
||||
// notifies under, so the notification can't be lost.
|
||||
if self.main_queue_has_work() {
|
||||
continue;
|
||||
}
|
||||
if *inflight == 0 {
|
||||
// Main-thread sends happen before in-flight decrements, and
|
||||
// decrements happen under this lock, so the check above
|
||||
// observed all completed work.
|
||||
return;
|
||||
}
|
||||
// Woken when main-thread work arrives or the in-flight count
|
||||
// reaches zero; both notify under this lock.
|
||||
self.idle.condvar.wait(&mut inflight);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drives main-thread work until `ready` returns a value.
|
||||
///
|
||||
/// Unlike [`Self::run_until_idle`], this waits across temporary quiescence.
|
||||
/// This is required when completion can arrive from an external worker that
|
||||
/// is not represented in the dispatcher's in-flight count.
|
||||
///
|
||||
/// Readiness is checked before every main-thread runnable, so this returns
|
||||
/// as soon as `ready` observes completion rather than after the queue
|
||||
/// drains — deferred work that re-queues itself (idle sweeps, pollers)
|
||||
/// must not extend a benchmark's measured interval past the completion it
|
||||
/// awaits.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn run_until<R>(&self, ready: impl FnMut() -> Option<R>) -> R {
|
||||
self.run_until_with_frames(ready, || false)
|
||||
}
|
||||
|
||||
/// Runs at most one queued main-thread task, returning whether one ran.
|
||||
///
|
||||
/// Stepping tasks individually lets callers check completion between polls
|
||||
/// even when work continually re-queues itself.
|
||||
fn run_one_main_task(&self) -> bool {
|
||||
assert!(
|
||||
self.is_main_thread(),
|
||||
"main tasks must run on the main thread"
|
||||
);
|
||||
let runnable = self.main_receiver.lock().try_pop();
|
||||
match runnable {
|
||||
Ok(Some(runnable)) => {
|
||||
let location = runnable.metadata().location;
|
||||
let spawned = runnable.metadata().spawned;
|
||||
profiler::update_running_task(spawned, location);
|
||||
runnable.run();
|
||||
profiler::save_task_timing();
|
||||
true
|
||||
}
|
||||
Ok(None) | Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs up to the initial queue length of main-thread tasks.
|
||||
///
|
||||
/// Returns whether any ran. This snapshots the count, not task identities:
|
||||
/// newly dispatched higher-priority tasks can overtake the original tasks.
|
||||
pub fn run_ready_main_tasks(&self) -> bool {
|
||||
self.run_ready_main_tasks_with(|| true, || {})
|
||||
}
|
||||
|
||||
/// Runs a count-bounded ready turn, checking `should_continue` before every poll.
|
||||
/// Calls `after_poll` after each runnable returns, outside the queue lock.
|
||||
pub(crate) fn run_ready_main_tasks_with(
|
||||
&self,
|
||||
mut should_continue: impl FnMut() -> bool,
|
||||
mut after_poll: impl FnMut(),
|
||||
) -> bool {
|
||||
assert!(
|
||||
self.is_main_thread(),
|
||||
"run_ready_main_tasks must be called on the threaded dispatcher's main thread"
|
||||
);
|
||||
let pending = self.main_receiver.lock().len();
|
||||
let mut ran_any = false;
|
||||
for _ in 0..pending {
|
||||
if !should_continue() || !self.run_one_main_task() {
|
||||
break;
|
||||
}
|
||||
ran_any = true;
|
||||
after_poll();
|
||||
}
|
||||
ran_any
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "bench-support"))]
|
||||
pub(crate) fn run_until_with_frames<R>(
|
||||
&self,
|
||||
mut ready: impl FnMut() -> Option<R>,
|
||||
mut dispatch_frames: impl FnMut() -> bool,
|
||||
) -> R {
|
||||
assert!(
|
||||
self.is_main_thread(),
|
||||
"main tasks must run on the main thread"
|
||||
);
|
||||
loop {
|
||||
if let Some(result) = ready() {
|
||||
return result;
|
||||
}
|
||||
let ran_task = self.run_one_main_task();
|
||||
// A completion poll must not pull an unrelated animation frame
|
||||
// into the measured operation.
|
||||
if let Some(result) = ready() {
|
||||
return result;
|
||||
}
|
||||
let dispatched_frames = dispatch_frames();
|
||||
if ran_task || dispatched_frames {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut inflight = self.idle.inflight.lock();
|
||||
if self.main_queue_has_work() {
|
||||
continue;
|
||||
}
|
||||
self.idle.condvar.wait(&mut inflight);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancels all pending timers so timers armed by one workload can't fire
|
||||
/// during a later workload sharing this process-lifetime dispatcher.
|
||||
///
|
||||
/// Dropping a timer runnable drops its completion sender, waking the task
|
||||
/// awaiting the timer. Call [`Self::run_until_idle`] after this method to
|
||||
/// drain any work that cancellation unblocks.
|
||||
pub fn cancel_pending_timers(&self) -> usize {
|
||||
let timers = {
|
||||
let mut state = self.timers.state.lock();
|
||||
let timers: Vec<_> = state.heap.drain().collect();
|
||||
self.timers.condvar.notify_all();
|
||||
timers
|
||||
};
|
||||
let canceled = timers.len();
|
||||
drop(timers);
|
||||
canceled
|
||||
}
|
||||
|
||||
/// Describes the dispatcher's idle-tracking state, for diagnosing
|
||||
/// workloads that fail to reach quiescence.
|
||||
pub fn debug_state(&self) -> String {
|
||||
let inflight = *self.idle.inflight.lock();
|
||||
let timers = self.timers.state.lock().heap.len();
|
||||
let main_queue_has_work = self.main_queue_has_work();
|
||||
format!(
|
||||
"ThreadedDispatcher {{ inflight: {inflight}, pending_timers: {timers}, \
|
||||
main_queue_has_work: {main_queue_has_work} }}"
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether no main-thread work is queued, no background or timer
|
||||
/// runnables are queued or running, and no armed timer is due. Timers
|
||||
/// that aren't due yet are ignored, as in [`Self::run_until_idle`].
|
||||
#[cfg(any(test, feature = "bench-support"))]
|
||||
pub(crate) fn is_idle(&self) -> bool {
|
||||
!self.main_queue_has_work() && !self.has_due_timer() && *self.idle.inflight.lock() == 0
|
||||
}
|
||||
|
||||
fn has_due_timer(&self) -> bool {
|
||||
let state = self.timers.state.lock();
|
||||
state
|
||||
.heap
|
||||
.peek()
|
||||
.is_some_and(|entry| entry.due <= Instant::now())
|
||||
}
|
||||
|
||||
fn main_queue_has_work(&self) -> bool {
|
||||
!self.main_receiver.lock().is_empty()
|
||||
}
|
||||
|
||||
fn drain_main_queue(&self) -> bool {
|
||||
let mut ran_any = false;
|
||||
loop {
|
||||
// Lock only around the pop so runnables can re-entrantly dispatch
|
||||
// more main-thread work through the sender while they run.
|
||||
let runnable = self.main_receiver.lock().try_pop();
|
||||
match runnable {
|
||||
Ok(Some(runnable)) => {
|
||||
let location = runnable.metadata().location;
|
||||
let spawned = runnable.metadata().spawned;
|
||||
profiler::update_running_task(spawned, location);
|
||||
runnable.run();
|
||||
profiler::save_task_timing();
|
||||
ran_any = true;
|
||||
}
|
||||
Ok(None) | Err(_) => return ran_any,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDispatcher for ThreadedDispatcher {
|
||||
fn is_main_thread(&self) -> bool {
|
||||
thread::current().id() == self.main_thread_id
|
||||
}
|
||||
|
||||
fn dispatch(&self, runnable: RunnableVariant, priority: Priority) {
|
||||
self.idle.increment();
|
||||
self.background_sender
|
||||
.send(priority, runnable)
|
||||
.unwrap_or_else(|_| panic!("threaded dispatcher workers are no longer running"));
|
||||
}
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) {
|
||||
if let Err(error) = self.main_sender.send(priority, runnable) {
|
||||
// The main receiver lives as long as this dispatcher, so a failed
|
||||
// send means we're mid-teardown. The runnable may wrap a !Send
|
||||
// future, so forget it rather than dropping it on this thread
|
||||
// (mirrors LinuxDispatcher).
|
||||
std::mem::forget(error);
|
||||
return;
|
||||
}
|
||||
// Wake `run_until_idle` if it's waiting for main-thread work.
|
||||
self.idle.notify_under_lock();
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) {
|
||||
let mut state = self.timers.state.lock();
|
||||
let seq = state.next_seq;
|
||||
state.next_seq += 1;
|
||||
state.heap.push(TimerEntry {
|
||||
due: Instant::now() + duration,
|
||||
seq,
|
||||
runnable,
|
||||
});
|
||||
self.timers.condvar.notify_one();
|
||||
}
|
||||
|
||||
fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>) {
|
||||
// This dispatcher does not need realtime scheduling priority; a plain
|
||||
// thread keeps it portable.
|
||||
thread::Builder::new()
|
||||
.name("ThreadedDispatcherRealtime".to_owned())
|
||||
.spawn(f)
|
||||
.expect("failed to spawn threaded dispatcher realtime thread");
|
||||
}
|
||||
|
||||
fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::future::Future;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
|
||||
use super::*;
|
||||
use crate::{BackgroundExecutor, ForegroundExecutor};
|
||||
|
||||
#[test]
|
||||
fn is_idle_tracks_queued_work_but_ignores_undue_timers() {
|
||||
let dispatcher = Arc::new(ThreadedDispatcher::new());
|
||||
let foreground = ForegroundExecutor::new(dispatcher.clone());
|
||||
assert!(dispatcher.is_idle());
|
||||
|
||||
foreground.spawn(async {}).detach();
|
||||
assert!(!dispatcher.is_idle());
|
||||
dispatcher.run_until_idle();
|
||||
assert!(dispatcher.is_idle());
|
||||
|
||||
let background = BackgroundExecutor::new(dispatcher.clone());
|
||||
let timer = background.timer(Duration::from_secs(60));
|
||||
// The timer future's initial poll runs on a worker thread; wait for
|
||||
// it so only the armed, not-yet-due timer remains.
|
||||
dispatcher.run_until_idle();
|
||||
assert!(
|
||||
dispatcher.is_idle(),
|
||||
"a timer that is not due yet should not count as pending work"
|
||||
);
|
||||
drop(timer);
|
||||
dispatcher.cancel_pending_timers();
|
||||
dispatcher.run_until_idle();
|
||||
assert!(dispatcher.is_idle());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_ready_main_tasks_does_not_wait_for_background_handoffs() {
|
||||
let dispatcher = Arc::new(ThreadedDispatcher::new());
|
||||
let background = BackgroundExecutor::new(dispatcher.clone());
|
||||
let foreground = ForegroundExecutor::new(dispatcher.clone());
|
||||
|
||||
let (sender, receiver) = futures::channel::oneshot::channel();
|
||||
background
|
||||
.spawn(async move {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
sender.send(()).ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
let completed = Arc::new(AtomicBool::new(false));
|
||||
foreground
|
||||
.spawn({
|
||||
let completed = completed.clone();
|
||||
async move {
|
||||
receiver.await.ok();
|
||||
completed.store(true, Ordering::SeqCst);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
assert!(dispatcher.run_ready_main_tasks());
|
||||
assert!(!completed.load(Ordering::SeqCst));
|
||||
|
||||
dispatcher.run_until_idle();
|
||||
assert!(completed.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_until_idle_completes_background_to_main_handoffs() {
|
||||
let dispatcher = Arc::new(ThreadedDispatcher::new());
|
||||
let background = BackgroundExecutor::new(dispatcher.clone());
|
||||
let foreground = ForegroundExecutor::new(dispatcher.clone());
|
||||
|
||||
let (sender, receiver) = futures::channel::oneshot::channel();
|
||||
background
|
||||
.spawn(async move {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
sender.send(()).ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
let completed = Arc::new(AtomicBool::new(false));
|
||||
foreground
|
||||
.spawn({
|
||||
let completed = completed.clone();
|
||||
async move {
|
||||
receiver.await.ok();
|
||||
completed.store(true, Ordering::SeqCst);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
dispatcher.run_until_idle();
|
||||
assert!(completed.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_until_waits_for_untracked_external_wakes() {
|
||||
let dispatcher = Arc::new(ThreadedDispatcher::new());
|
||||
let foreground = ForegroundExecutor::new(dispatcher.clone());
|
||||
let (sender, receiver) = futures::channel::oneshot::channel();
|
||||
let sender_thread = thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
sender
|
||||
.send(())
|
||||
.expect("foreground receiver should remain alive");
|
||||
});
|
||||
|
||||
let completed = Arc::new(AtomicBool::new(false));
|
||||
foreground
|
||||
.spawn({
|
||||
let completed = completed.clone();
|
||||
async move {
|
||||
receiver
|
||||
.await
|
||||
.expect("external sender should deliver its wake");
|
||||
completed.store(true, Ordering::SeqCst);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
dispatcher.run_until(|| completed.load(Ordering::SeqCst).then_some(()));
|
||||
sender_thread.join().expect("sender thread should finish");
|
||||
assert!(completed.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_until_returns_at_readiness_despite_requeuing_main_work() {
|
||||
const REQUEUE_LIMIT: usize = 10_000;
|
||||
|
||||
let dispatcher = Arc::new(ThreadedDispatcher::new());
|
||||
let foreground = ForegroundExecutor::new(dispatcher.clone());
|
||||
|
||||
// Mirrors main-thread work that yields and immediately re-queues
|
||||
// itself (e.g. an idle-time sweep): the main queue never drains until
|
||||
// such work finishes every iteration, so readiness must be observed
|
||||
// between runnables rather than only at quiescence.
|
||||
let iterations = Arc::new(AtomicUsize::new(0));
|
||||
foreground
|
||||
.spawn({
|
||||
let iterations = iterations.clone();
|
||||
async move {
|
||||
for _ in 0..REQUEUE_LIMIT {
|
||||
iterations.fetch_add(1, Ordering::SeqCst);
|
||||
yield_once().await;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let completed = Arc::new(AtomicBool::new(false));
|
||||
foreground
|
||||
.spawn({
|
||||
let completed = completed.clone();
|
||||
async move {
|
||||
completed.store(true, Ordering::SeqCst);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
dispatcher.run_until(|| completed.load(Ordering::SeqCst).then_some(()));
|
||||
assert!(
|
||||
iterations.load(Ordering::SeqCst) < REQUEUE_LIMIT,
|
||||
"run_until should return at readiness instead of draining re-queued main work"
|
||||
);
|
||||
}
|
||||
|
||||
/// Completes after one re-schedule: the poll returns `Pending` and wakes
|
||||
/// immediately, so the runnable re-enters the main queue.
|
||||
fn yield_once() -> impl Future<Output = ()> {
|
||||
let mut yielded = false;
|
||||
std::future::poll_fn(move |poll_context| {
|
||||
if yielded {
|
||||
std::task::Poll::Ready(())
|
||||
} else {
|
||||
yielded = true;
|
||||
poll_context.waker().wake_by_ref();
|
||||
std::task::Poll::Pending
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_ready_main_tasks_advances_requeuing_work_one_batch_per_call() {
|
||||
const REQUEUE_LIMIT: usize = 10_000;
|
||||
|
||||
let dispatcher = Arc::new(ThreadedDispatcher::new());
|
||||
let foreground = ForegroundExecutor::new(dispatcher.clone());
|
||||
|
||||
let iterations = Arc::new(AtomicUsize::new(0));
|
||||
foreground
|
||||
.spawn({
|
||||
let iterations = iterations.clone();
|
||||
async move {
|
||||
for _ in 0..REQUEUE_LIMIT {
|
||||
iterations.fetch_add(1, Ordering::SeqCst);
|
||||
yield_once().await;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
assert!(dispatcher.run_ready_main_tasks());
|
||||
assert_eq!(iterations.load(Ordering::SeqCst), 1);
|
||||
assert!(dispatcher.run_ready_main_tasks());
|
||||
assert_eq!(iterations.load(Ordering::SeqCst), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timers_fire_in_real_time() {
|
||||
let dispatcher = Arc::new(ThreadedDispatcher::new());
|
||||
let background = BackgroundExecutor::new(dispatcher);
|
||||
|
||||
let fired = Arc::new(AtomicBool::new(false));
|
||||
let timer = background.timer(Duration::from_millis(10));
|
||||
background
|
||||
.spawn({
|
||||
let fired = fired.clone();
|
||||
async move {
|
||||
timer.await;
|
||||
fired.store(true, Ordering::SeqCst);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while !fired.load(Ordering::SeqCst) && Instant::now() < deadline {
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
assert!(fired.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_pending_timers_wakes_waiters_without_waiting_for_deadline() {
|
||||
let dispatcher = Arc::new(ThreadedDispatcher::new());
|
||||
let background = BackgroundExecutor::new(dispatcher.clone());
|
||||
|
||||
let fired = Arc::new(AtomicBool::new(false));
|
||||
let timer = background.timer(Duration::from_secs(10));
|
||||
background
|
||||
.spawn({
|
||||
let fired = fired.clone();
|
||||
async move {
|
||||
timer.await;
|
||||
fired.store(true, Ordering::SeqCst);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
dispatcher.run_until_idle();
|
||||
assert_eq!(dispatcher.cancel_pending_timers(), 1);
|
||||
dispatcher.run_until_idle();
|
||||
|
||||
assert!(fired.load(Ordering::SeqCst));
|
||||
assert_eq!(dispatcher.cancel_pending_timers(), 0);
|
||||
}
|
||||
}
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
//! Visual test platform that combines real rendering (macOs-only for now) with controllable TestDispatcher.
|
||||
//!
|
||||
//! This platform is used for visual tests that need:
|
||||
//! - Real rendering (e.g. Metal/compositor) for accurate screenshots
|
||||
//! - Deterministic task scheduling via TestDispatcher
|
||||
//! - Controllable time via `advance_clock`
|
||||
|
||||
use crate::ScreenCaptureSource;
|
||||
use crate::{
|
||||
ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle,
|
||||
ForegroundExecutor, Keymap, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PlatformWindow, Task, TestDispatcher, WindowAppearance, WindowParams,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use futures::channel::oneshot;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// A platform that combines real Mac rendering with controllable TestDispatcher.
|
||||
///
|
||||
/// This allows visual tests to:
|
||||
/// - Render real UI via Metal for accurate screenshots
|
||||
/// - Control task scheduling deterministically via TestDispatcher
|
||||
/// - Advance simulated time for testing time-based behaviors (tooltips, animations, etc.)
|
||||
pub struct VisualTestPlatform {
|
||||
dispatcher: TestDispatcher,
|
||||
background_executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
platform: Rc<dyn Platform>,
|
||||
clipboard: Mutex<Option<ClipboardItem>>,
|
||||
find_pasteboard: Mutex<Option<ClipboardItem>>,
|
||||
}
|
||||
|
||||
impl VisualTestPlatform {
|
||||
/// Creates a new VisualTestPlatform with the given random seed.
|
||||
///
|
||||
/// The seed is used for deterministic random number generation in the TestDispatcher.
|
||||
pub fn new(platform: Rc<dyn Platform>, seed: u64) -> Self {
|
||||
let dispatcher = TestDispatcher::new(seed);
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
|
||||
Self {
|
||||
dispatcher,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
platform,
|
||||
clipboard: Mutex::new(None),
|
||||
find_pasteboard: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the TestDispatcher for controlling task scheduling and time.
|
||||
pub fn dispatcher(&self) -> &TestDispatcher {
|
||||
&self.dispatcher
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform for VisualTestPlatform {
|
||||
fn background_executor(&self) -> BackgroundExecutor {
|
||||
self.background_executor.clone()
|
||||
}
|
||||
|
||||
fn foreground_executor(&self) -> ForegroundExecutor {
|
||||
self.foreground_executor.clone()
|
||||
}
|
||||
|
||||
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
|
||||
self.platform.text_system()
|
||||
}
|
||||
|
||||
fn run(&self, _on_finish_launching: Box<dyn 'static + FnOnce()>) {
|
||||
panic!("VisualTestPlatform::run should not be called in tests")
|
||||
}
|
||||
|
||||
fn quit(&self) {}
|
||||
|
||||
fn restart(&self, _binary_path: Option<PathBuf>, _arguments: Vec<std::ffi::OsString>) {}
|
||||
|
||||
fn activate(&self, _ignoring_other_apps: bool) {}
|
||||
|
||||
fn hide(&self) {}
|
||||
|
||||
fn hide_other_apps(&self) {}
|
||||
|
||||
fn unhide_other_apps(&self) {}
|
||||
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
self.platform.displays()
|
||||
}
|
||||
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
self.platform.primary_display()
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
self.platform.active_window()
|
||||
}
|
||||
|
||||
fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
|
||||
self.platform.window_stack()
|
||||
}
|
||||
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
self.platform.is_screen_capture_supported()
|
||||
}
|
||||
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||
self.platform.screen_capture_sources()
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowParams,
|
||||
) -> Result<Box<dyn PlatformWindow>> {
|
||||
self.platform.open_window(handle, options)
|
||||
}
|
||||
|
||||
fn window_appearance(&self) -> WindowAppearance {
|
||||
self.platform.window_appearance()
|
||||
}
|
||||
|
||||
fn open_url(&self, url: &str) {
|
||||
self.platform.open_url(url)
|
||||
}
|
||||
|
||||
fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {}
|
||||
|
||||
fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
_options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
tx.send(Ok(None)).ok();
|
||||
rx
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(
|
||||
&self,
|
||||
_directory: &Path,
|
||||
_suggested_name: Option<&str>,
|
||||
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
tx.send(Ok(None)).ok();
|
||||
rx
|
||||
}
|
||||
|
||||
fn can_select_mixed_files_and_dirs(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: &Path) {
|
||||
self.platform.reveal_path(path)
|
||||
}
|
||||
|
||||
fn open_with_system(&self, path: &Path) {
|
||||
self.platform.open_with_system(path)
|
||||
}
|
||||
|
||||
fn on_quit(&self, _callback: Box<dyn FnMut() -> bool>) {}
|
||||
|
||||
fn on_reopen(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_system_sleep(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
|
||||
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
|
||||
|
||||
fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
|
||||
|
||||
fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
|
||||
|
||||
fn app_path(&self) -> Result<PathBuf> {
|
||||
self.platform.app_path()
|
||||
}
|
||||
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||
self.platform.path_for_auxiliary_executable(name)
|
||||
}
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
self.platform.set_cursor_style(style)
|
||||
}
|
||||
|
||||
fn hide_cursor_until_mouse_moves(&self) {
|
||||
self.platform.hide_cursor_until_mouse_moves();
|
||||
}
|
||||
|
||||
fn is_cursor_visible(&self) -> bool {
|
||||
self.platform.is_cursor_visible()
|
||||
}
|
||||
|
||||
fn should_auto_hide_scrollbars(&self) -> bool {
|
||||
self.platform.should_auto_hide_scrollbars()
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.clipboard.lock().clone()
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
*self.clipboard.lock() = Some(item);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
|
||||
self.find_pasteboard.lock().clone()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn write_to_find_pasteboard(&self, item: ClipboardItem) {
|
||||
*self.find_pasteboard.lock() = Some(item);
|
||||
}
|
||||
|
||||
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||
self.platform.keyboard_layout()
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
self.platform.keyboard_mapper()
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn thermal_state(&self) -> super::ThermalState {
|
||||
super::ThermalState::Nominal
|
||||
}
|
||||
|
||||
fn on_thermal_state_change(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>> {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"Idle sleep prevention for {reason:?} is not supported in visual tests"
|
||||
)))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user