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:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-11 00:25:10 -04:00
1 parent ba5c0798cb
commit 48a66ca6c1
162 files changed
+104826 -2

No files matched your search

+426
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
)))
}
}