Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use futures_util::future::LocalBoxFuture;
|
||||
|
||||
use crate::modals::ModalId;
|
||||
use crate::windowing::state::ApplicationStage;
|
||||
use crate::windowing::{WindowCallbackDispatcher, WindowManager};
|
||||
use crate::{
|
||||
keymap::Keystroke, notification, AppContext, ClosedWindowData, SingletonEntity, WindowId,
|
||||
};
|
||||
|
||||
use super::menu::MenuItemPropertyChanges;
|
||||
|
||||
pub type AppInitCallbackFn =
|
||||
Box<dyn FnOnce(&mut crate::AppContext, LocalBoxFuture<'static, crate::App>)>;
|
||||
|
||||
pub type TerminationResult = anyhow::Result<()>;
|
||||
|
||||
/// A collection of callbacks which application developers can provide
|
||||
/// to hook into important events that are observed by the UI framework.
|
||||
#[derive(Default)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct AppCallbacks {
|
||||
pub on_become_active: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
pub on_notification_clicked:
|
||||
Option<Box<dyn FnMut(notification::NotificationResponse, &mut AppContext)>>,
|
||||
pub on_resigned_active: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
pub on_will_terminate: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
/// Callback on whether the app will proceed with termination.
|
||||
pub on_should_terminate_app: Option<Box<dyn FnMut(&mut AppContext) -> ApproveTerminateResult>>,
|
||||
/// Callback on whether the window will proceed with closing.
|
||||
pub on_should_close_window:
|
||||
Option<Box<dyn FnMut(WindowId, &mut AppContext) -> ApproveTerminateResult>>,
|
||||
/// Callback for when the user clicks "don't show again" on the warning modal.
|
||||
pub on_disable_warning_modal: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
/// Callback on when the internet reachability to a specific host has changed.
|
||||
/// The host name here could be a string for an IP address or domain (e.g. www.warp.dev).
|
||||
pub on_internet_reachability_changed: Option<Box<dyn FnMut(bool, &mut AppContext)>>,
|
||||
pub on_active_window_changed: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
pub on_new_window_requested: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
pub on_window_moved: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
pub on_window_resized: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
pub on_window_will_close: Option<Box<dyn FnMut(Option<ClosedWindowData>, &mut AppContext)>>,
|
||||
/// Callback for screen parameter changes. For example, user connecting/
|
||||
/// disconnecting external monitor, changes screen arrangement, or adjusts screen
|
||||
/// resolution.
|
||||
pub on_screen_changed: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
pub on_open_files: Option<Box<dyn FnMut(Vec<PathBuf>, &mut AppContext)>>,
|
||||
pub on_open_urls: Option<Box<dyn FnMut(Vec<String>, &mut AppContext)>>,
|
||||
pub on_os_appearance_changed: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
/// Callback to hook into a notification for when the cpu was awakened after sleeping.
|
||||
pub on_cpu_awakened: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
/// Callback to hook into a notification for when the cpu is about to go to sleep.
|
||||
pub on_cpu_will_sleep: Option<Box<dyn FnMut(&mut AppContext)>>,
|
||||
}
|
||||
|
||||
/// A helper structure to simplify and standardize the act of making calls from
|
||||
/// platform code into user application code.
|
||||
pub struct AppCallbackDispatcher {
|
||||
callbacks: AppCallbacks,
|
||||
ui_app: crate::App,
|
||||
}
|
||||
|
||||
pub enum ApproveTerminateResult {
|
||||
/// The window or app should be closed.
|
||||
Terminate,
|
||||
/// Do not close the window or app.
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl AppCallbackDispatcher {
|
||||
pub fn new(callbacks: AppCallbacks, ui_app: crate::App) -> Self {
|
||||
Self { callbacks, ui_app }
|
||||
}
|
||||
|
||||
pub fn initialize_app(&mut self, init_fn: AppInitCallbackFn) {
|
||||
let app_clone = self.ui_app.clone();
|
||||
self.ui_app.update(|ctx| {
|
||||
use futures_util::FutureExt;
|
||||
|
||||
// Provide the init function with access to the UI app,
|
||||
// but only from a future that is running on the main
|
||||
// thread (to prevent double-borrow issues).
|
||||
init_fn(ctx, async move { app_clone }.boxed_local());
|
||||
|
||||
// Validate all of the registered bindings now that the app is initialized.
|
||||
ctx.validate_bindings();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn app_became_active(&mut self) {
|
||||
log::info!("application did become active");
|
||||
self.ui_app.update(|ctx| {
|
||||
WindowManager::handle(ctx).update(ctx, |windowing_state, ctx| {
|
||||
windowing_state.set_stage(ApplicationStage::Active, ctx);
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(callback) = &mut self.callbacks.on_become_active {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
// This is not called on Linux or wasm, as there isn't any generic way to
|
||||
// click on/interact with a notification.
|
||||
// TODO(CORE-2322): implement desktop notifications on Windows
|
||||
#[cfg_attr(
|
||||
any(target_os = "linux", target_os = "windows", target_family = "wasm"),
|
||||
allow(dead_code)
|
||||
)]
|
||||
pub fn notification_clicked(&mut self, response: notification::NotificationResponse) {
|
||||
if let Some(callback) = &mut self.callbacks.on_notification_clicked {
|
||||
self.ui_app.update(|ctx| callback(response, ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_resigned_active(&mut self) {
|
||||
self.ui_app.update(|ctx| {
|
||||
WindowManager::handle(ctx).update(ctx, |state, ctx| {
|
||||
state.set_stage(ApplicationStage::Inactive, ctx);
|
||||
});
|
||||
});
|
||||
if let Some(callback) = &mut self.callbacks.on_resigned_active {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_will_terminate(&mut self) {
|
||||
log::info!("application will terminate");
|
||||
self.ui_app.update(|ctx| {
|
||||
WindowManager::handle(ctx).update(ctx, |state, ctx| {
|
||||
state.set_stage(ApplicationStage::Terminating, ctx);
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(callback) = &mut self.callbacks.on_will_terminate {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_terminate_app(&mut self) -> ApproveTerminateResult {
|
||||
if let Some(callback) = &mut self.callbacks.on_should_terminate_app {
|
||||
self.ui_app.update(|ctx| callback(ctx))
|
||||
} else {
|
||||
ApproveTerminateResult::Terminate
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_close_window(&mut self, window_id: WindowId) -> ApproveTerminateResult {
|
||||
if let Some(callback) = &mut self.callbacks.on_should_close_window {
|
||||
self.ui_app.update(|ctx| callback(window_id, ctx))
|
||||
} else {
|
||||
ApproveTerminateResult::Terminate
|
||||
}
|
||||
}
|
||||
|
||||
// Dead code is allowed on wasm as when we register the network connection
|
||||
// listener on wasm, we don't yet have access to an `AppCallbackDispatcher`,
|
||||
// so we directly check the `Callbacks` object instead.
|
||||
// TODO(CORE-2683): implement events for internet reachability changes
|
||||
#[cfg_attr(any(target_family = "wasm", target_os = "windows"), allow(dead_code))]
|
||||
pub fn has_internet_reachability_changed_callback(&self) -> bool {
|
||||
self.callbacks.on_internet_reachability_changed.is_some()
|
||||
}
|
||||
|
||||
pub fn internet_reachability_changed(&mut self, is_reachable: bool) {
|
||||
if is_reachable {
|
||||
log::info!("application can reach internet");
|
||||
} else {
|
||||
log::info!("application can not reach internet");
|
||||
}
|
||||
if let Some(callback) = &mut self.callbacks.on_internet_reachability_changed {
|
||||
self.ui_app.update(|ctx| callback(is_reachable, ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_window_changed(&mut self, active_window_id: Option<WindowId>) {
|
||||
log::info!("active window changed: {active_window_id:?}");
|
||||
self.ui_app.update(|ctx| {
|
||||
WindowManager::handle(ctx).update(ctx, |state, ctx| {
|
||||
state.set_active_window(active_window_id, ctx);
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(callback) = &mut self.callbacks.on_active_window_changed {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
pub fn open_new_window(&mut self) {
|
||||
if let Some(callback) = &mut self.callbacks.on_new_window_requested {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_moved(&mut self) {
|
||||
if let Some(callback) = &mut self.callbacks.on_window_moved {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_resized(&mut self) {
|
||||
log::info!("window resized");
|
||||
if let Some(callback) = &mut self.callbacks.on_window_resized {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_will_close(&mut self, window_id: WindowId) {
|
||||
log::info!("{window_id:?} will close");
|
||||
if let Some(callback) = &mut self.callbacks.on_window_will_close {
|
||||
self.ui_app.update(|ctx| {
|
||||
let closed_window_data = ctx.handle_window_closed(window_id);
|
||||
callback(closed_window_data, ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
pub fn screen_changed(&mut self) {
|
||||
if let Some(callback) = &mut self.callbacks.on_screen_changed {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
|
||||
// Update the window fullscreen state, which in turn triggers a re-render of the workspace view.
|
||||
self.ui_app.update(|ctx| {
|
||||
WindowManager::handle(ctx).update(ctx, |state, model_ctx| {
|
||||
state.update_is_active_window_fullscreen(model_ctx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
pub fn open_files(&mut self, file_paths: Vec<PathBuf>) {
|
||||
if let Some(callback) = &mut self.callbacks.on_open_files {
|
||||
self.ui_app.update(|ctx| callback(file_paths, ctx));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
pub fn open_urls(&mut self, urls: Vec<String>) {
|
||||
if let Some(callback) = &mut self.callbacks.on_open_urls {
|
||||
self.ui_app.update(|ctx| callback(urls, ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn os_appearance_changed(&mut self) {
|
||||
if let Some(callback) = &mut self.callbacks.on_os_appearance_changed {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpu_awakened(&mut self) {
|
||||
if let Some(callback) = &mut self.callbacks.on_cpu_awakened {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpu_will_sleep(&mut self) {
|
||||
if let Some(callback) = &mut self.callbacks.on_cpu_will_sleep {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn global_shortcut_triggered(&mut self, shortcut: Keystroke) {
|
||||
self.ui_app
|
||||
.update(|ctx| ctx.on_global_shortcut_triggered(shortcut))
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "macos"), allow(unused))]
|
||||
pub fn can_borrow_mut(&self) -> bool {
|
||||
self.ui_app.can_borrow_mut()
|
||||
}
|
||||
|
||||
pub fn with_mutable_app_context<T>(
|
||||
&mut self,
|
||||
callback: impl FnOnce(&mut AppContext) -> T,
|
||||
) -> T {
|
||||
self.ui_app.update(|ctx| callback(ctx))
|
||||
}
|
||||
|
||||
pub fn for_window<'a>(
|
||||
&'a mut self,
|
||||
window: &'a dyn super::Window,
|
||||
) -> WindowCallbackDispatcher<'a> {
|
||||
WindowCallbackDispatcher::new(window.callbacks(), self.ui_app.as_mut())
|
||||
}
|
||||
}
|
||||
|
||||
// Functions in AppCallbackDispatcher that relate to application menus.
|
||||
//
|
||||
// This is marked as `allow(dead_code)` on Linux, as it doesn't support
|
||||
// application menus, so these never get called.
|
||||
// TODO(CORE-2691): implement native Windows OS app menus
|
||||
#[cfg_attr(
|
||||
any(target_os = "linux", target_os = "windows", target_family = "wasm"),
|
||||
allow(dead_code)
|
||||
)]
|
||||
impl AppCallbackDispatcher {
|
||||
pub fn menu_item_triggered(&mut self, callback: impl FnOnce(&mut AppContext)) {
|
||||
self.ui_app.update(callback);
|
||||
}
|
||||
|
||||
pub fn update_menu_item(
|
||||
&mut self,
|
||||
callback: impl FnOnce(&mut AppContext) -> MenuItemPropertyChanges,
|
||||
) -> MenuItemPropertyChanges {
|
||||
self.ui_app.update(callback)
|
||||
}
|
||||
}
|
||||
|
||||
// Functions in AppCallbackDispatcher that relate to native platform modals.
|
||||
//
|
||||
// This is marked as `allow(dead_code)` on Linux and WASM, as we do not support
|
||||
// native platform modals on these platforms, so these never get called.
|
||||
// TODO(CORE-2323): implement native Windows OS modal
|
||||
#[cfg_attr(
|
||||
any(target_os = "linux", target_os = "windows", target_family = "wasm"),
|
||||
allow(dead_code)
|
||||
)]
|
||||
impl AppCallbackDispatcher {
|
||||
pub fn process_platform_modal_response(
|
||||
&mut self,
|
||||
modal_id: ModalId,
|
||||
response_button_index: usize,
|
||||
disable_modal: bool,
|
||||
) {
|
||||
self.ui_app.update(|ctx| {
|
||||
ctx.process_platform_modal_response(modal_id, response_button_index, disable_modal)
|
||||
});
|
||||
}
|
||||
|
||||
pub fn warning_modal_disabled(&mut self) {
|
||||
if let Some(callback) = &mut self.callbacks.on_disable_warning_modal {
|
||||
self.ui_app.update(|ctx| callback(ctx));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use std::{fmt, path::PathBuf, sync::Arc};
|
||||
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum FilePickerError {
|
||||
#[error("Failed to spawn file picker thread: {0}")]
|
||||
ThreadSpawnFailed(Arc<std::io::Error>),
|
||||
|
||||
#[error("File dialog failed: {0}")]
|
||||
DialogFailed(String),
|
||||
}
|
||||
|
||||
// Define complex type here for the file picker callback.
|
||||
pub type FilePickerCallback =
|
||||
Box<dyn FnOnce(Result<Vec<String>, FilePickerError>, &mut crate::AppContext) + Send + Sync>;
|
||||
|
||||
// Define callback type for save file picker - returns single path or None if cancelled
|
||||
pub type SaveFilePickerCallback =
|
||||
Box<dyn FnOnce(Option<String>, &mut crate::AppContext) + Send + Sync>;
|
||||
|
||||
pub enum FileType {
|
||||
Image,
|
||||
Yaml,
|
||||
Markdown,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
/// List of supported file extensions for this file type.
|
||||
pub fn extensions(&self) -> &[&str] {
|
||||
match self {
|
||||
FileType::Image => &["png", "jpg", "jpeg"],
|
||||
FileType::Yaml => &["yaml"],
|
||||
FileType::Markdown => &["md"],
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable name for this general category of files.
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self {
|
||||
FileType::Image => "Image",
|
||||
FileType::Yaml => "Yaml",
|
||||
FileType::Markdown => "Markdown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FileType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.display_name())
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the file picker.
|
||||
///
|
||||
/// Not all configurations are supported on all platforms:
|
||||
/// * Linux can only show a single-file picker, a multi-file picker, or a single-directory picker.
|
||||
/// If choosing a folder is allowed ([`Self::allow_folder`] or [`Self::folders_only`]), a
|
||||
/// single-directory picker is shown, regardless of the other settings.
|
||||
/// * macOS supports any combination of allowing files, allowing folders, and allowing
|
||||
/// multi-select.
|
||||
pub struct FilePickerConfiguration {
|
||||
allows_files: bool,
|
||||
allows_folder: bool,
|
||||
file_types: Vec<FileType>,
|
||||
can_multi_select: bool,
|
||||
}
|
||||
|
||||
impl FilePickerConfiguration {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
allows_files: true,
|
||||
allows_folder: false,
|
||||
file_types: Default::default(),
|
||||
can_multi_select: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure the file picker to allow choosing folders, in addition to files.
|
||||
pub fn allow_folder(mut self) -> Self {
|
||||
self.allows_folder = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the file picker to *only* allow choosing folders.
|
||||
pub fn folders_only(mut self) -> Self {
|
||||
self.allows_folder = true;
|
||||
self.allows_files = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the file picker to allow selecting multiple files.
|
||||
pub fn allow_multi_select(mut self) -> Self {
|
||||
self.can_multi_select = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_allowed_file_types(mut self, file_types: Vec<FileType>) -> Self {
|
||||
self.file_types = file_types;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn allows_files(&self) -> bool {
|
||||
self.allows_files
|
||||
}
|
||||
|
||||
// TODO(CORE-2324): open file picker on Windows
|
||||
|
||||
pub fn allows_folder(&self) -> bool {
|
||||
self.allows_folder
|
||||
}
|
||||
|
||||
pub fn allows_multi_select(&self) -> bool {
|
||||
self.can_multi_select
|
||||
}
|
||||
|
||||
pub fn file_types(&self) -> &Vec<FileType> {
|
||||
&self.file_types
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FilePickerConfiguration {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SaveFilePickerConfiguration {
|
||||
/// Pre-fill the editable filename editor with this.
|
||||
pub default_filename: Option<String>,
|
||||
/// Open the picker into this directory location to start.
|
||||
pub default_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl SaveFilePickerConfiguration {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_default_filename(mut self, filename: String) -> Self {
|
||||
self.default_filename = Some(filename);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_default_directory(mut self, directory: PathBuf) -> Self {
|
||||
self.default_directory = Some(directory);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
//! This module defines types used in the context of keyboard events across platforms. The types
|
||||
//! are based on winit types, however, we include them in this module to avoid needing to include
|
||||
//! the entirety of winit as a dependency for MacOS.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// The following types and functions are taken from winit's implementation.
|
||||
// We redefine them here to avoid needing to include the entirety of winit as a dependency for MacOS.
|
||||
// --------------------------------------------------------------------------------------------------------
|
||||
|
||||
/// Contains the platform-native physical key identifier
|
||||
///
|
||||
/// The exact values vary from platform to platform (which is part of why this is a per-platform
|
||||
/// enum), but the values are primarily tied to the key's physical location on the keyboard.
|
||||
///
|
||||
/// This enum is primarily used to store raw keycodes when Winit doesn't map a given native
|
||||
/// physical key identifier to a meaningful [`KeyCode`] variant. In the presence of identifiers we
|
||||
/// haven't mapped for you yet, this lets you use use [`KeyCode`] to:
|
||||
///
|
||||
/// - Correctly match key press and release events.
|
||||
/// - On non-Web platforms, support assigning keybinds to virtually any key through a UI.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub enum NativeKeyCode {
|
||||
Unidentified,
|
||||
/// A macOS "scancode".
|
||||
MacOS(u16),
|
||||
/// A Windows "scancode".
|
||||
Windows(u16),
|
||||
/// An XKB "keycode".
|
||||
Xkb(u32),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for NativeKeyCode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use NativeKeyCode::{MacOS, Unidentified, Windows, Xkb};
|
||||
let mut debug_tuple;
|
||||
match self {
|
||||
Unidentified => {
|
||||
debug_tuple = f.debug_tuple("Unidentified");
|
||||
}
|
||||
MacOS(code) => {
|
||||
debug_tuple = f.debug_tuple("MacOS");
|
||||
debug_tuple.field(&format_args!("0x{code:04X}"));
|
||||
}
|
||||
Windows(code) => {
|
||||
debug_tuple = f.debug_tuple("Windows");
|
||||
debug_tuple.field(&format_args!("0x{code:04X}"));
|
||||
}
|
||||
Xkb(code) => {
|
||||
debug_tuple = f.debug_tuple("Xkb");
|
||||
debug_tuple.field(&format_args!("0x{code:04X}"));
|
||||
}
|
||||
}
|
||||
debug_tuple.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the location of a physical key.
|
||||
///
|
||||
/// This type is a superset of [`KeyCode`], including an [`Unidentified`][Self::Unidentified]
|
||||
/// variant.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub enum PhysicalKey {
|
||||
/// A known key code
|
||||
Code(KeyCode),
|
||||
/// This variant is used when the key cannot be translated to a [`KeyCode`]
|
||||
///
|
||||
/// The native keycode is provided (if available) so you're able to more reliably match
|
||||
/// key-press and key-release events by hashing the [`PhysicalKey`]. It is also possible to use
|
||||
/// this for keybinds for non-standard keys, but such keybinds are tied to a given platform.
|
||||
Unidentified(NativeKeyCode),
|
||||
}
|
||||
|
||||
/// Code representing the location of a physical key
|
||||
///
|
||||
/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few
|
||||
/// exceptions:
|
||||
/// - The keys that the specification calls "MetaLeft" and "MetaRight" are named "SuperLeft" and
|
||||
/// "SuperRight" here.
|
||||
/// - The key that the specification calls "Super" is reported as `Unidentified` here.
|
||||
///
|
||||
/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub enum KeyCode {
|
||||
/// <kbd>`</kbd> on a US keyboard. This is also called a backtick or grave.
|
||||
/// This is the <kbd>半角</kbd>/<kbd>全角</kbd>/<kbd>漢字</kbd>
|
||||
/// (hankaku/zenkaku/kanji) key on Japanese keyboards
|
||||
Backquote,
|
||||
/// Used for both the US <kbd>\\</kbd> (on the 101-key layout) and also for the key
|
||||
/// located between the <kbd>"</kbd> and <kbd>Enter</kbd> keys on row C of the 102-,
|
||||
/// 104- and 106-key layouts.
|
||||
/// Labeled <kbd>#</kbd> on a UK (102) keyboard.
|
||||
Backslash,
|
||||
/// <kbd>[</kbd> on a US keyboard.
|
||||
BracketLeft,
|
||||
/// <kbd>]</kbd> on a US keyboard.
|
||||
BracketRight,
|
||||
/// <kbd>,</kbd> on a US keyboard.
|
||||
Comma,
|
||||
/// <kbd>0</kbd> on a US keyboard.
|
||||
Digit0,
|
||||
/// <kbd>1</kbd> on a US keyboard.
|
||||
Digit1,
|
||||
/// <kbd>2</kbd> on a US keyboard.
|
||||
Digit2,
|
||||
/// <kbd>3</kbd> on a US keyboard.
|
||||
Digit3,
|
||||
/// <kbd>4</kbd> on a US keyboard.
|
||||
Digit4,
|
||||
/// <kbd>5</kbd> on a US keyboard.
|
||||
Digit5,
|
||||
/// <kbd>6</kbd> on a US keyboard.
|
||||
Digit6,
|
||||
/// <kbd>7</kbd> on a US keyboard.
|
||||
Digit7,
|
||||
/// <kbd>8</kbd> on a US keyboard.
|
||||
Digit8,
|
||||
/// <kbd>9</kbd> on a US keyboard.
|
||||
Digit9,
|
||||
/// <kbd>=</kbd> on a US keyboard.
|
||||
Equal,
|
||||
/// Located between the left <kbd>Shift</kbd> and <kbd>Z</kbd> keys.
|
||||
/// Labeled <kbd>\\</kbd> on a UK keyboard.
|
||||
IntlBackslash,
|
||||
/// Located between the <kbd>/</kbd> and right <kbd>Shift</kbd> keys.
|
||||
/// Labeled <kbd>\\</kbd> (ro) on a Japanese keyboard.
|
||||
IntlRo,
|
||||
/// Located between the <kbd>=</kbd> and <kbd>Backspace</kbd> keys.
|
||||
/// Labeled <kbd>¥</kbd> (yen) on a Japanese keyboard. <kbd>\\</kbd> on a
|
||||
/// Russian keyboard.
|
||||
IntlYen,
|
||||
/// <kbd>a</kbd> on a US keyboard.
|
||||
/// Labeled <kbd>q</kbd> on an AZERTY (e.g., French) keyboard.
|
||||
KeyA,
|
||||
/// <kbd>b</kbd> on a US keyboard.
|
||||
KeyB,
|
||||
/// <kbd>c</kbd> on a US keyboard.
|
||||
KeyC,
|
||||
/// <kbd>d</kbd> on a US keyboard.
|
||||
KeyD,
|
||||
/// <kbd>e</kbd> on a US keyboard.
|
||||
KeyE,
|
||||
/// <kbd>f</kbd> on a US keyboard.
|
||||
KeyF,
|
||||
/// <kbd>g</kbd> on a US keyboard.
|
||||
KeyG,
|
||||
/// <kbd>h</kbd> on a US keyboard.
|
||||
KeyH,
|
||||
/// <kbd>i</kbd> on a US keyboard.
|
||||
KeyI,
|
||||
/// <kbd>j</kbd> on a US keyboard.
|
||||
KeyJ,
|
||||
/// <kbd>k</kbd> on a US keyboard.
|
||||
KeyK,
|
||||
/// <kbd>l</kbd> on a US keyboard.
|
||||
KeyL,
|
||||
/// <kbd>m</kbd> on a US keyboard.
|
||||
KeyM,
|
||||
/// <kbd>n</kbd> on a US keyboard.
|
||||
KeyN,
|
||||
/// <kbd>o</kbd> on a US keyboard.
|
||||
KeyO,
|
||||
/// <kbd>p</kbd> on a US keyboard.
|
||||
KeyP,
|
||||
/// <kbd>q</kbd> on a US keyboard.
|
||||
/// Labeled <kbd>a</kbd> on an AZERTY (e.g., French) keyboard.
|
||||
KeyQ,
|
||||
/// <kbd>r</kbd> on a US keyboard.
|
||||
KeyR,
|
||||
/// <kbd>s</kbd> on a US keyboard.
|
||||
KeyS,
|
||||
/// <kbd>t</kbd> on a US keyboard.
|
||||
KeyT,
|
||||
/// <kbd>u</kbd> on a US keyboard.
|
||||
KeyU,
|
||||
/// <kbd>v</kbd> on a US keyboard.
|
||||
KeyV,
|
||||
/// <kbd>w</kbd> on a US keyboard.
|
||||
/// Labeled <kbd>z</kbd> on an AZERTY (e.g., French) keyboard.
|
||||
KeyW,
|
||||
/// <kbd>x</kbd> on a US keyboard.
|
||||
KeyX,
|
||||
/// <kbd>y</kbd> on a US keyboard.
|
||||
/// Labeled <kbd>z</kbd> on a QWERTZ (e.g., German) keyboard.
|
||||
KeyY,
|
||||
/// <kbd>z</kbd> on a US keyboard.
|
||||
/// Labeled <kbd>w</kbd> on an AZERTY (e.g., French) keyboard, and <kbd>y</kbd> on a
|
||||
/// QWERTZ (e.g., German) keyboard.
|
||||
KeyZ,
|
||||
/// <kbd>-</kbd> on a US keyboard.
|
||||
Minus,
|
||||
/// <kbd>.</kbd> on a US keyboard.
|
||||
Period,
|
||||
/// <kbd>'</kbd> on a US keyboard.
|
||||
Quote,
|
||||
/// <kbd>;</kbd> on a US keyboard.
|
||||
Semicolon,
|
||||
/// <kbd>/</kbd> on a US keyboard.
|
||||
Slash,
|
||||
/// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
|
||||
AltLeft,
|
||||
/// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
|
||||
/// This is labeled <kbd>AltGr</kbd> on many keyboard layouts.
|
||||
AltRight,
|
||||
/// <kbd>Backspace</kbd> or <kbd>⌫</kbd>.
|
||||
/// Labeled <kbd>Delete</kbd> on Apple keyboards.
|
||||
Backspace,
|
||||
/// <kbd>CapsLock</kbd> or <kbd>⇪</kbd>
|
||||
CapsLock,
|
||||
/// The application context menu key, which is typically found between the right
|
||||
/// <kbd>Super</kbd> key and the right <kbd>Control</kbd> key.
|
||||
ContextMenu,
|
||||
/// <kbd>Control</kbd> or <kbd>⌃</kbd>
|
||||
ControlLeft,
|
||||
/// <kbd>Control</kbd> or <kbd>⌃</kbd>
|
||||
ControlRight,
|
||||
/// <kbd>Enter</kbd> or <kbd>↵</kbd>. Labeled <kbd>Return</kbd> on Apple keyboards.
|
||||
Enter,
|
||||
/// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
|
||||
SuperLeft,
|
||||
/// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
|
||||
SuperRight,
|
||||
/// <kbd>Shift</kbd> or <kbd>⇧</kbd>
|
||||
ShiftLeft,
|
||||
/// <kbd>Shift</kbd> or <kbd>⇧</kbd>
|
||||
ShiftRight,
|
||||
/// <kbd> </kbd> (space)
|
||||
Space,
|
||||
/// <kbd>Tab</kbd> or <kbd>⇥</kbd>
|
||||
Tab,
|
||||
/// Japanese: <kbd>変</kbd> (henkan)
|
||||
Convert,
|
||||
/// Japanese: <kbd>カタカナ</kbd>/<kbd>ひらがな</kbd>/<kbd>ローマ字</kbd>
|
||||
/// (katakana/hiragana/romaji)
|
||||
KanaMode,
|
||||
/// Korean: HangulMode <kbd>한/영</kbd> (han/yeong)
|
||||
///
|
||||
/// Japanese (Mac keyboard): <kbd>か</kbd> (kana)
|
||||
Lang1,
|
||||
/// Korean: Hanja <kbd>한</kbd> (hanja)
|
||||
///
|
||||
/// Japanese (Mac keyboard): <kbd>英</kbd> (eisu)
|
||||
Lang2,
|
||||
/// Japanese (word-processing keyboard): Katakana
|
||||
Lang3,
|
||||
/// Japanese (word-processing keyboard): Hiragana
|
||||
Lang4,
|
||||
/// Japanese (word-processing keyboard): Zenkaku/Hankaku
|
||||
Lang5,
|
||||
/// Japanese: <kbd>無変換</kbd> (muhenkan)
|
||||
NonConvert,
|
||||
/// <kbd>⌦</kbd>. The forward delete key.
|
||||
/// Note that on Apple keyboards, the key labelled <kbd>Delete</kbd> on the main part of
|
||||
/// the keyboard is encoded as [`Backspace`].
|
||||
///
|
||||
/// [`Backspace`]: Self::Backspace
|
||||
Delete,
|
||||
/// <kbd>Page Down</kbd>, <kbd>End</kbd>, or <kbd>↘</kbd>
|
||||
End,
|
||||
/// <kbd>Help</kbd>. Not present on standard PC keyboards.
|
||||
Help,
|
||||
/// <kbd>Home</kbd> or <kbd>↖</kbd>
|
||||
Home,
|
||||
/// <kbd>Insert</kbd> or <kbd>Ins</kbd>. Not present on Apple keyboards.
|
||||
Insert,
|
||||
/// <kbd>Page Down</kbd>, <kbd>PgDn</kbd>, or <kbd>⇟</kbd>
|
||||
PageDown,
|
||||
/// <kbd>Page Up</kbd>, <kbd>PgUp</kbd>, or <kbd>⇞</kbd>
|
||||
PageUp,
|
||||
/// <kbd>↓</kbd>
|
||||
ArrowDown,
|
||||
/// <kbd>←</kbd>
|
||||
ArrowLeft,
|
||||
/// <kbd>→</kbd>
|
||||
ArrowRight,
|
||||
/// <kbd>↑</kbd>
|
||||
ArrowUp,
|
||||
/// On the Mac, this is used for the numpad <kbd>Clear</kbd> key.
|
||||
NumLock,
|
||||
/// <kbd>0 Ins</kbd> on a keyboard. <kbd>0</kbd> on a phone or remote control
|
||||
Numpad0,
|
||||
/// <kbd>1 End</kbd> on a keyboard. <kbd>1</kbd> or <kbd>1 QZ</kbd> on a phone or remote
|
||||
/// control
|
||||
Numpad1,
|
||||
/// <kbd>2 ↓</kbd> on a keyboard. <kbd>2 ABC</kbd> on a phone or remote control
|
||||
Numpad2,
|
||||
/// <kbd>3 PgDn</kbd> on a keyboard. <kbd>3 DEF</kbd> on a phone or remote control
|
||||
Numpad3,
|
||||
/// <kbd>4 ←</kbd> on a keyboard. <kbd>4 GHI</kbd> on a phone or remote control
|
||||
Numpad4,
|
||||
/// <kbd>5</kbd> on a keyboard. <kbd>5 JKL</kbd> on a phone or remote control
|
||||
Numpad5,
|
||||
/// <kbd>6 →</kbd> on a keyboard. <kbd>6 MNO</kbd> on a phone or remote control
|
||||
Numpad6,
|
||||
/// <kbd>7 Home</kbd> on a keyboard. <kbd>7 PQRS</kbd> or <kbd>7 PRS</kbd> on a phone
|
||||
/// or remote control
|
||||
Numpad7,
|
||||
/// <kbd>8 ↑</kbd> on a keyboard. <kbd>8 TUV</kbd> on a phone or remote control
|
||||
Numpad8,
|
||||
/// <kbd>9 PgUp</kbd> on a keyboard. <kbd>9 WXYZ</kbd> or <kbd>9 WXY</kbd> on a phone
|
||||
/// or remote control
|
||||
Numpad9,
|
||||
/// <kbd>+</kbd>
|
||||
NumpadAdd,
|
||||
/// Found on the Microsoft Natural Keyboard.
|
||||
NumpadBackspace,
|
||||
/// <kbd>C</kbd> or <kbd>A</kbd> (All Clear). Also for use with numpads that have a
|
||||
/// <kbd>Clear</kbd> key that is separate from the <kbd>NumLock</kbd> key. On the Mac, the
|
||||
/// numpad <kbd>Clear</kbd> key is encoded as [`NumLock`].
|
||||
///
|
||||
/// [`NumLock`]: Self::NumLock
|
||||
NumpadClear,
|
||||
/// <kbd>C</kbd> (Clear Entry)
|
||||
NumpadClearEntry,
|
||||
/// <kbd>,</kbd> (thousands separator). For locales where the thousands separator
|
||||
/// is a "." (e.g., Brazil), this key may generate a <kbd>.</kbd>.
|
||||
NumpadComma,
|
||||
/// <kbd>. Del</kbd>. For locales where the decimal separator is "," (e.g.,
|
||||
/// Brazil), this key may generate a <kbd>,</kbd>.
|
||||
NumpadDecimal,
|
||||
/// <kbd>/</kbd>
|
||||
NumpadDivide,
|
||||
NumpadEnter,
|
||||
/// <kbd>=</kbd>
|
||||
NumpadEqual,
|
||||
/// <kbd>#</kbd> on a phone or remote control device. This key is typically found
|
||||
/// below the <kbd>9</kbd> key and to the right of the <kbd>0</kbd> key.
|
||||
NumpadHash,
|
||||
/// <kbd>M</kbd> Add current entry to the value stored in memory.
|
||||
NumpadMemoryAdd,
|
||||
/// <kbd>M</kbd> Clear the value stored in memory.
|
||||
NumpadMemoryClear,
|
||||
/// <kbd>M</kbd> Replace the current entry with the value stored in memory.
|
||||
NumpadMemoryRecall,
|
||||
/// <kbd>M</kbd> Replace the value stored in memory with the current entry.
|
||||
NumpadMemoryStore,
|
||||
/// <kbd>M</kbd> Subtract current entry from the value stored in memory.
|
||||
NumpadMemorySubtract,
|
||||
/// <kbd>*</kbd> on a keyboard. For use with numpads that provide mathematical
|
||||
/// operations (<kbd>+</kbd>, <kbd>-</kbd> <kbd>*</kbd> and <kbd>/</kbd>).
|
||||
///
|
||||
/// Use `NumpadStar` for the <kbd>*</kbd> key on phones and remote controls.
|
||||
NumpadMultiply,
|
||||
/// <kbd>(</kbd> Found on the Microsoft Natural Keyboard.
|
||||
NumpadParenLeft,
|
||||
/// <kbd>)</kbd> Found on the Microsoft Natural Keyboard.
|
||||
NumpadParenRight,
|
||||
/// <kbd>*</kbd> on a phone or remote control device.
|
||||
///
|
||||
/// This key is typically found below the <kbd>7</kbd> key and to the left of
|
||||
/// the <kbd>0</kbd> key.
|
||||
///
|
||||
/// Use <kbd>"NumpadMultiply"</kbd> for the <kbd>*</kbd> key on
|
||||
/// numeric keypads.
|
||||
NumpadStar,
|
||||
/// <kbd>-</kbd>
|
||||
NumpadSubtract,
|
||||
/// <kbd>Esc</kbd> or <kbd>⎋</kbd>
|
||||
Escape,
|
||||
/// <kbd>Fn</kbd> This is typically a hardware key that does not generate a separate code.
|
||||
Fn,
|
||||
/// <kbd>FLock</kbd> or <kbd>FnLock</kbd>. Function Lock key. Found on the Microsoft
|
||||
/// Natural Keyboard.
|
||||
FnLock,
|
||||
/// <kbd>PrtScr SysRq</kbd> or <kbd>Print Screen</kbd>
|
||||
PrintScreen,
|
||||
/// <kbd>Scroll Lock</kbd>
|
||||
ScrollLock,
|
||||
/// <kbd>Pause Break</kbd>
|
||||
Pause,
|
||||
/// Some laptops place this key to the left of the <kbd>↑</kbd> key.
|
||||
///
|
||||
/// This also the "back" button (triangle) on Android.
|
||||
BrowserBack,
|
||||
BrowserFavorites,
|
||||
/// Some laptops place this key to the right of the <kbd>↑</kbd> key.
|
||||
BrowserForward,
|
||||
/// The "home" button on Android.
|
||||
BrowserHome,
|
||||
BrowserRefresh,
|
||||
BrowserSearch,
|
||||
BrowserStop,
|
||||
/// <kbd>Eject</kbd> or <kbd>⏏</kbd>. This key is placed in the function section on some Apple
|
||||
/// keyboards.
|
||||
Eject,
|
||||
/// Sometimes labelled <kbd>My Computer</kbd> on the keyboard
|
||||
LaunchApp1,
|
||||
/// Sometimes labelled <kbd>Calculator</kbd> on the keyboard
|
||||
LaunchApp2,
|
||||
LaunchMail,
|
||||
MediaPlayPause,
|
||||
MediaSelect,
|
||||
MediaStop,
|
||||
MediaTrackNext,
|
||||
MediaTrackPrevious,
|
||||
/// This key is placed in the function section on some Apple keyboards, replacing the
|
||||
/// <kbd>Eject</kbd> key.
|
||||
Power,
|
||||
Sleep,
|
||||
AudioVolumeDown,
|
||||
AudioVolumeMute,
|
||||
AudioVolumeUp,
|
||||
WakeUp,
|
||||
// Legacy modifier key. Also called "Super" in certain places.
|
||||
Meta,
|
||||
// Legacy modifier key.
|
||||
Hyper,
|
||||
Turbo,
|
||||
Abort,
|
||||
Resume,
|
||||
Suspend,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Again,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Copy,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Cut,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Find,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Open,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Paste,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Props,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Select,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Undo,
|
||||
/// Use for dedicated <kbd>ひらがな</kbd> key found on some Japanese word processing keyboards.
|
||||
Hiragana,
|
||||
/// Use for dedicated <kbd>カタカナ</kbd> key found on some Japanese word processing keyboards.
|
||||
Katakana,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F1,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F2,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F3,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F4,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F5,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F6,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F7,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F8,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F9,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F10,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F11,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F12,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F13,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F14,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F15,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F16,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F17,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F18,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F19,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F20,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F21,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F22,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F23,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F24,
|
||||
/// General-purpose function key.
|
||||
F25,
|
||||
/// General-purpose function key.
|
||||
F26,
|
||||
/// General-purpose function key.
|
||||
F27,
|
||||
/// General-purpose function key.
|
||||
F28,
|
||||
/// General-purpose function key.
|
||||
F29,
|
||||
/// General-purpose function key.
|
||||
F30,
|
||||
/// General-purpose function key.
|
||||
F31,
|
||||
/// General-purpose function key.
|
||||
F32,
|
||||
/// General-purpose function key.
|
||||
F33,
|
||||
/// General-purpose function key.
|
||||
F34,
|
||||
/// General-purpose function key.
|
||||
F35,
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
use crate::actions::StandardAction;
|
||||
use crate::keymap::Keystroke;
|
||||
use crate::AppContext;
|
||||
|
||||
pub enum MenuItem {
|
||||
Custom(CustomMenuItem),
|
||||
Separator,
|
||||
Standard(StandardAction),
|
||||
|
||||
/// Services is a system-defined standard menu on macOS.
|
||||
#[cfg(target_os = "macos")]
|
||||
Services,
|
||||
}
|
||||
|
||||
// We allow dead_code here because the title is only read when compiling the
|
||||
// Mac bits.
|
||||
#[allow(dead_code)]
|
||||
pub struct Menu {
|
||||
pub title: String,
|
||||
pub menu_items: Vec<MenuItem>,
|
||||
}
|
||||
|
||||
impl Menu {
|
||||
pub fn new<S: Into<String>>(title: S, menu_items: Vec<MenuItem>) -> Self {
|
||||
Menu {
|
||||
title: title.into(),
|
||||
menu_items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_window_menu(&self) -> bool {
|
||||
&self.title == "Window"
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct MenuBar {
|
||||
pub menus: Vec<Menu>,
|
||||
}
|
||||
|
||||
impl MenuBar {
|
||||
pub fn new(menus: Vec<Menu>) -> Self {
|
||||
MenuBar { menus }
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties of a menu item.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MenuItemProperties {
|
||||
pub name: String,
|
||||
pub keystroke: Option<Keystroke>,
|
||||
pub disabled: bool,
|
||||
/// If set, the item gets a checkmark.
|
||||
pub checked: bool,
|
||||
}
|
||||
|
||||
impl MenuItemProperties {
|
||||
pub fn apply(&mut self, changes: &MenuItemPropertyChanges) {
|
||||
if let Some(name) = &changes.name {
|
||||
self.name.clone_from(name);
|
||||
}
|
||||
if let Some(keystroke) = changes.keystroke.as_ref() {
|
||||
self.keystroke.clone_from(keystroke);
|
||||
}
|
||||
if let Some(disabled) = changes.disabled {
|
||||
self.disabled = disabled;
|
||||
}
|
||||
if let Some(checked) = changes.checked {
|
||||
self.checked = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes to properties of a menu item.
|
||||
#[derive(Default)]
|
||||
pub struct MenuItemPropertyChanges {
|
||||
pub name: Option<String>,
|
||||
pub keystroke: Option<Option<Keystroke>>,
|
||||
pub disabled: Option<bool>,
|
||||
pub checked: Option<bool>,
|
||||
pub submenu: Option<Submenu>,
|
||||
}
|
||||
|
||||
impl MenuItemPropertyChanges {
|
||||
/// Returns a struct that unconditionally sets all properties, to be used
|
||||
/// when initializing a menu item for the first time.
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub fn for_new_item(props: MenuItemProperties, submenu: Submenu) -> Self {
|
||||
Self {
|
||||
name: Some(props.name),
|
||||
keystroke: Some(props.keystroke),
|
||||
disabled: Some(props.disabled),
|
||||
checked: Some(props.checked),
|
||||
submenu: Some(submenu),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ItemTriggeredCallback = Box<dyn Fn(&mut AppContext)>;
|
||||
|
||||
/// A callback function that is invoked when we may want to update
|
||||
/// a menu item.
|
||||
///
|
||||
/// It receives a reference to the current set of properties, and
|
||||
/// returns a structure indicating which properties should be updated
|
||||
/// and what the new values should be.
|
||||
pub type UpdateMenuItemCallback =
|
||||
Box<dyn Fn(&MenuItemProperties, &mut AppContext) -> MenuItemPropertyChanges>;
|
||||
|
||||
pub type Submenu = Option<Vec<MenuItem>>;
|
||||
|
||||
pub struct CustomMenuItem {
|
||||
pub properties: MenuItemProperties,
|
||||
pub callback: ItemTriggeredCallback,
|
||||
pub updater: UpdateMenuItemCallback,
|
||||
pub submenu: Submenu,
|
||||
}
|
||||
|
||||
impl CustomMenuItem {
|
||||
/// Construct a new CustomMenuItem with the given \p name.
|
||||
/// \p callback will be invoked when the user triggers the menu item.
|
||||
/// \p updater is invoked when the menu is opened or otherwise needs to be updated.
|
||||
/// The function receives a bag of properties and may mutate it.
|
||||
/// Any properties that are changed will be reflected in the menu item.
|
||||
pub fn new<
|
||||
Callback: 'static + Fn(&mut AppContext),
|
||||
Updater: 'static + Fn(&MenuItemProperties, &mut AppContext) -> MenuItemPropertyChanges,
|
||||
>(
|
||||
name: &str,
|
||||
callback: Callback,
|
||||
updater: Updater,
|
||||
keystroke: Option<Keystroke>,
|
||||
) -> Self {
|
||||
Self {
|
||||
properties: MenuItemProperties {
|
||||
name: name.to_string(),
|
||||
keystroke,
|
||||
..Default::default()
|
||||
},
|
||||
callback: Box::new(callback),
|
||||
updater: Box::new(updater),
|
||||
submenu: None,
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor that takes in additional submenu argument.
|
||||
pub fn new_with_submenu<
|
||||
Callback: 'static + Fn(&mut AppContext),
|
||||
Updater: 'static + Fn(&MenuItemProperties, &mut AppContext) -> MenuItemPropertyChanges,
|
||||
>(
|
||||
name: &str,
|
||||
callback: Callback,
|
||||
updater: Updater,
|
||||
keystroke: Option<Keystroke>,
|
||||
submenu: Vec<MenuItem>,
|
||||
) -> Self {
|
||||
Self {
|
||||
properties: MenuItemProperties {
|
||||
name: name.to_string(),
|
||||
keystroke,
|
||||
..Default::default()
|
||||
},
|
||||
callback: Box::new(callback),
|
||||
updater: Box::new(updater),
|
||||
submenu: Some(submenu),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,749 @@
|
||||
pub mod app;
|
||||
pub mod file_picker;
|
||||
pub mod keyboard;
|
||||
pub mod menu;
|
||||
|
||||
pub mod test;
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub mod wasm;
|
||||
|
||||
pub use app::AppCallbacks;
|
||||
use derivative::Derivative;
|
||||
pub use file_picker::{
|
||||
FilePickerCallback, FilePickerConfiguration, FileType, SaveFilePickerCallback,
|
||||
SaveFilePickerConfiguration,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use galaxy_util::path::ShellFamily;
|
||||
|
||||
use crate::fonts::SubpixelAlignment;
|
||||
use crate::keymap::Keystroke;
|
||||
use crate::modals::{AlertDialog, ModalId};
|
||||
use crate::notification::{NotificationSendError, RequestPermissionsOutcome};
|
||||
|
||||
use crate::rendering::{GPUPowerPreference, OnGPUDeviceSelected};
|
||||
use crate::text_layout::{ClipConfig, StyleAndFont, TextAlignment, TextFrame};
|
||||
use crate::{
|
||||
accessibility::AccessibilityContent,
|
||||
fonts::{
|
||||
canvas::RasterFormat, FamilyId, FontId, GlyphId, Metrics, Properties, RasterizedGlyph,
|
||||
},
|
||||
notification::UserNotification,
|
||||
text_layout::Line,
|
||||
windowing::WindowCallbacks,
|
||||
Scene, WindowId,
|
||||
};
|
||||
use crate::{
|
||||
geometry, rendering, AppContext, ApplicationBundleInfo, Clipboard, DisplayId, DisplayIdx,
|
||||
OptionalPlatformWindow,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_task::Runnable;
|
||||
use lazy_static::lazy_static;
|
||||
use pathfinder_geometry::vector::Vector2I;
|
||||
use pathfinder_geometry::{
|
||||
rect::{RectF, RectI},
|
||||
vector::Vector2F,
|
||||
};
|
||||
use std::any::Any;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::{ops::Range, rc::Rc, sync::Arc};
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
lazy_static! {
|
||||
pub static ref KEYS_TO_IGNORE: HashSet<Keystroke> = HashSet::new();
|
||||
}
|
||||
#[cfg(target_family = "wasm")]
|
||||
lazy_static! {
|
||||
pub static ref KEYS_TO_IGNORE: HashSet<Keystroke> =
|
||||
HashSet::from([Keystroke::parse("cmdorctrl-v").unwrap()]);
|
||||
}
|
||||
|
||||
/// Type of the callback function that provides the result of requesting
|
||||
/// desktop notification permissions.
|
||||
pub type RequestNotificationPermissionsCallback =
|
||||
Box<dyn FnOnce(RequestPermissionsOutcome, &mut AppContext) + Send + Sync>;
|
||||
/// Type of the callback function invoked when an error occurs while sending
|
||||
/// a desktop notification.
|
||||
pub type SendNotificationErrorCallback =
|
||||
Box<dyn FnOnce(NotificationSendError, &mut AppContext) + Send + Sync>;
|
||||
|
||||
/// The information needed to send a notification.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug)]
|
||||
pub struct NotificationInfo {
|
||||
pub notification_content: UserNotification,
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub on_error: SendNotificationErrorCallback,
|
||||
}
|
||||
|
||||
// TODO(advait): revisit this to check if there's a better approach.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct LineStyle {
|
||||
pub font_size: f32,
|
||||
pub line_height_ratio: f32,
|
||||
pub baseline_ratio: f32,
|
||||
/// Size of tab stops in spaces for fully fixed-width (monospace) text.
|
||||
///
|
||||
/// `Some(n)` means `\t` advances to the next stop every `n` spaces. This is intended only for
|
||||
/// paragraphs where all runs share the same fixed-width font metrics.
|
||||
///
|
||||
/// `None` leaves tab stop behavior up to the backend defaults.
|
||||
pub fixed_width_tab_size: Option<u8>,
|
||||
}
|
||||
|
||||
pub struct WindowOptions {
|
||||
pub bounds: WindowBounds,
|
||||
pub fullscreen_state: FullscreenState,
|
||||
pub hide_title_bar: bool,
|
||||
pub title: Option<String>,
|
||||
pub style: WindowStyle,
|
||||
pub background_blur_radius_pixels: Option<u8>,
|
||||
pub background_blur_texture: bool,
|
||||
pub gpu_power_preference: GPUPowerPreference,
|
||||
pub backend_preference: Option<GraphicsBackend>,
|
||||
pub on_gpu_device_info_reported: Box<OnGPUDeviceSelected>,
|
||||
/// This is an identifier to distinguish different windows among one application. It is a no-op
|
||||
/// on all platforms except X11 Linux.
|
||||
/// See docs on the "WM_CLASS" property:
|
||||
/// https://www.x.org/docs/ICCCM/icccm.pdf
|
||||
pub window_instance: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WindowOptions {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("WindowOptions")
|
||||
.field("bounds", &self.bounds)
|
||||
.field("hide_title_bar", &self.hide_title_bar)
|
||||
.field("title", &self.title)
|
||||
.field("style", &self.style)
|
||||
.field(
|
||||
"background_blur_radius_pixels",
|
||||
&self.background_blur_radius_pixels,
|
||||
)
|
||||
.field("background_blur_texture", &self.background_blur_texture)
|
||||
.field("gpu_power_preference", &self.gpu_power_preference)
|
||||
.field("backend_preference", &self.backend_preference)
|
||||
.field("window_instance", &self.window_instance)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub enum WindowStyle {
|
||||
#[default]
|
||||
Normal,
|
||||
|
||||
/// If the window does not steal focus, the passed bounds won't be applied and the
|
||||
/// the window will be set to default size.
|
||||
NotStealFocus,
|
||||
|
||||
/// If a window is pinned, it will be positioned above all other apps and steals focus
|
||||
/// by default.
|
||||
Pin,
|
||||
|
||||
/// A window that needs to cascade in case of opening a new window with ExactPosition
|
||||
Cascade,
|
||||
|
||||
/// Position the window at exact bounds and show it, but don't make it key (no focus steal).
|
||||
/// Used for drag preview windows that should appear but not interrupt the drag.
|
||||
PositionedNoFocus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Default)]
|
||||
pub enum WindowBounds {
|
||||
/// The platform chooses the window size and origin.
|
||||
#[default]
|
||||
Default,
|
||||
/// Use an exact size for the window, but let the platform choose its origin.
|
||||
ExactSize(Vector2F),
|
||||
/// Use an exact size and origin for the window.
|
||||
ExactPosition(RectF),
|
||||
}
|
||||
|
||||
impl WindowBounds {
|
||||
pub fn new(bounds: Option<RectF>) -> Self {
|
||||
match bounds {
|
||||
// Make sure the bounds are valid before passing down to platform call.
|
||||
Some(bound) if bound.height() > 0. && bound.width() > 0. => {
|
||||
WindowBounds::ExactPosition(bound)
|
||||
}
|
||||
_ => WindowBounds::Default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bounds(&self) -> Option<RectF> {
|
||||
match &self {
|
||||
Self::Default => None,
|
||||
Self::ExactSize(_) => None,
|
||||
Self::ExactPosition(bound) => Some(*bound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum MicrophoneAccessState {
|
||||
NotDetermined,
|
||||
Denied,
|
||||
Restricted,
|
||||
Authorized,
|
||||
}
|
||||
|
||||
pub trait Delegate: 'static {
|
||||
/// Returns a handle to the platform dispatch delegate.
|
||||
fn dispatch_delegate(&self) -> Arc<dyn DispatchDelegate>;
|
||||
|
||||
fn request_user_attention(&self, window_id: WindowId);
|
||||
|
||||
fn clipboard(&mut self) -> &mut dyn Clipboard;
|
||||
|
||||
fn system_theme(&self) -> SystemTheme;
|
||||
|
||||
fn open_url(&self, url: &str);
|
||||
|
||||
/// Opens an absolute file path with native system API.
|
||||
fn open_file_path(&self, path: &Path);
|
||||
|
||||
/// Opens an absolute file path in the file explorer with native system API.
|
||||
fn open_file_path_in_explorer(&self, path: &Path);
|
||||
|
||||
fn open_file_picker(
|
||||
&self,
|
||||
callback: FilePickerCallback,
|
||||
file_picker_config: FilePickerConfiguration,
|
||||
);
|
||||
|
||||
fn open_save_file_picker(
|
||||
&self,
|
||||
callback: file_picker::SaveFilePickerCallback,
|
||||
config: file_picker::SaveFilePickerConfiguration,
|
||||
);
|
||||
|
||||
/// Retrieve the absolute path of given application's bundle and its executable.
|
||||
fn application_bundle_info(&self, bundle_identifier: &str)
|
||||
-> Option<ApplicationBundleInfo<'_>>;
|
||||
|
||||
/// Create a window showing a modal dialog native to the platform. The modal will synchronously
|
||||
/// block all other interactions with the app until dismissed. The [`ModalId`] is a handle to
|
||||
/// map the modal response to the right callback for the [`AppContext`].
|
||||
fn show_native_platform_modal(&self, id: ModalId, modal: AlertDialog);
|
||||
|
||||
/// Requests OS permissions for sending desktop notifications.
|
||||
fn request_desktop_notification_permissions(
|
||||
&self,
|
||||
on_completion: RequestNotificationPermissionsCallback,
|
||||
);
|
||||
|
||||
/// Sends a desktop notification.
|
||||
fn send_desktop_notification(
|
||||
&self,
|
||||
notification_content: UserNotification,
|
||||
window_id: WindowId,
|
||||
on_error: SendNotificationErrorCallback,
|
||||
);
|
||||
|
||||
/// Sets the cursor pointer
|
||||
fn set_cursor_shape(&self, cursor: Cursor);
|
||||
|
||||
/// Returns the current cursor pointer
|
||||
#[cfg(feature = "test-util")]
|
||||
fn get_cursor_shape(&self) -> Cursor;
|
||||
fn close_ime_async(&self, window_id: WindowId);
|
||||
fn is_ime_open(&self) -> bool;
|
||||
|
||||
/// Requests that the system character palette (usually an emoji picker)
|
||||
/// be shown.
|
||||
fn open_character_palette(&self);
|
||||
|
||||
/// Sets the passed string as the content available for a11y tools (such as screen readers).
|
||||
fn set_accessibility_contents(&self, content: AccessibilityContent);
|
||||
|
||||
fn register_global_shortcut(&self, shortcut: Keystroke);
|
||||
fn unregister_global_shortcut(&self, shortcut: &Keystroke);
|
||||
|
||||
fn terminate_app(&self, termination_mode: TerminationMode);
|
||||
|
||||
/// Returns whether or not a screen reader is enabled, or None if we do not
|
||||
/// know for sure.
|
||||
fn is_screen_reader_enabled(&self) -> Option<bool>;
|
||||
|
||||
/// Returns the current microphone access state.
|
||||
fn microphone_access_state(&self) -> MicrophoneAccessState;
|
||||
|
||||
/// Returns whether the app is running with a headless rendering backend
|
||||
/// (no GUI or visible output).
|
||||
fn is_headless(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TerminationMode {
|
||||
/// The termination can be interrupted. This is the default, and should be used most
|
||||
/// of the time.
|
||||
Cancellable,
|
||||
/// The termination cannot be interrupted. This can be useful when we have received
|
||||
/// confirmation from the user that it is ok to terminate, for example.
|
||||
ForceTerminate,
|
||||
/// The window's content (tab) has been transferred to another window, so the
|
||||
/// now-empty source window should close without any confirmation dialogs.
|
||||
ContentTransferred,
|
||||
}
|
||||
|
||||
/// A trait for interacting with the main thread.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub trait DispatchDelegate: 'static + Send + Sync {
|
||||
fn is_main_thread(&self) -> bool;
|
||||
fn run_on_main_thread(&self, task: Runnable);
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub trait DispatchDelegate: 'static {
|
||||
fn is_main_thread(&self) -> bool;
|
||||
fn run_on_main_thread(&self, task: Runnable);
|
||||
}
|
||||
|
||||
/// A marker trait for the types that [`FontDB`] implementations return from
|
||||
/// [`FontDB::load_all_system_fonts`].
|
||||
pub trait LoadedSystemFonts: 'static + Any + Send + Sync {
|
||||
fn as_any(self: Box<Self>) -> Box<dyn Any>;
|
||||
}
|
||||
|
||||
/// Trait that implements text layout. Implementors must be [`Send`] and
|
||||
/// [`Sync`] so that text can be laid out in a background thread.
|
||||
pub trait TextLayoutSystem: 'static + Send + Sync {
|
||||
/// Lays out a single line of text.
|
||||
fn layout_line(
|
||||
&self,
|
||||
text: &str,
|
||||
line_style: LineStyle,
|
||||
style_runs: &[(Range<usize>, StyleAndFont)],
|
||||
max_width: f32,
|
||||
clip_config: ClipConfig,
|
||||
) -> Line;
|
||||
|
||||
/// Lays out text into a series of lines that fit within the bounding box
|
||||
/// defined by `max_width` and `max_height`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_text(
|
||||
&self,
|
||||
text: &str,
|
||||
line_style: LineStyle,
|
||||
style_runs: &[(Range<usize>, StyleAndFont)],
|
||||
max_width: f32,
|
||||
max_height: f32,
|
||||
alignment: TextAlignment,
|
||||
first_line_head_indent: Option<f32>,
|
||||
) -> TextFrame;
|
||||
}
|
||||
|
||||
/// A trait for working with fonts.
|
||||
///
|
||||
/// This interface provides a platform-agnostic API for loading fonts,
|
||||
/// retrieving font-related metrics, performing text shaping/layout, and
|
||||
/// rasterizing glyphs.
|
||||
///
|
||||
/// Implementations of this trait can rely on callers to cache returned values
|
||||
/// where appropriate.
|
||||
pub trait FontDB: 'static {
|
||||
/// Loads a font family from the provided set of font data.
|
||||
///
|
||||
/// Each bytestring should be decodable as a single font.
|
||||
fn load_from_bytes(&mut self, name: &str, bytes: Vec<Vec<u8>>) -> Result<FamilyId>;
|
||||
|
||||
/// Loads a font from the system by family name.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn load_from_system(&mut self, font_family: &str) -> Result<FamilyId>;
|
||||
|
||||
/// Returns a background task that produces the set of data the font DB
|
||||
/// needs to make all system fonts available to the application.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn load_all_system_fonts(
|
||||
&self,
|
||||
) -> futures::future::BoxFuture<'static, Box<dyn LoadedSystemFonts>>;
|
||||
|
||||
/// Processes the data produced by [`FontDB::load_all_system_fonts`],
|
||||
/// returning the list of system fonts that can be used by the application.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn process_loaded_system_fonts(
|
||||
&mut self,
|
||||
loaded_system_fonts: Box<dyn LoadedSystemFonts>,
|
||||
) -> Vec<(Option<FamilyId>, crate::fonts::FontInfo)>;
|
||||
|
||||
/// Returns the [`FamilyId`] identified by `name`, or [`None`] if no font
|
||||
/// with `name` has been inserted into the cache.
|
||||
fn family_id_for_name(&self, name: &str) -> Option<FamilyId>;
|
||||
|
||||
/// Gets the name of a font family by ID.
|
||||
fn load_family_name_from_id(&self, id: FamilyId) -> Option<String>;
|
||||
|
||||
/// Determines which font from a family should be used to display text with
|
||||
/// the given properties.
|
||||
fn select_font(&self, family_id: FamilyId, properties: Properties) -> FontId;
|
||||
|
||||
/// Returns the ordered list of fonts which should be checked when the given
|
||||
/// font is lacking a glyph for a character.
|
||||
fn fallback_fonts(&self, character: char, font_id: FontId) -> Vec<FontId>;
|
||||
|
||||
/// Returns a set of metrics about the font that aren't glyph-dependent.
|
||||
fn font_metrics(&self, font_id: FontId) -> Metrics;
|
||||
|
||||
/// Computes the position of a glyph that occurs after this one, relative to
|
||||
/// this glyph.
|
||||
///
|
||||
/// The `x` position within the resulting `Vector2F` is the horizontal distance to
|
||||
/// increment (or decrement, for RTL text) the position after a glyph has been rendered. It is
|
||||
/// always positive for horizontal layouts, and 0 for fonts that only support being
|
||||
/// rendered vertically.
|
||||
///
|
||||
/// The `y` position within the resulting `Vector2F` is the vertical distance to decrement (or
|
||||
/// increment for bottom to top writing) the position after a glyph has been rendered. It is
|
||||
/// always positive for vertical layouts, and 0 for fonts that only support being rendered
|
||||
/// horizontally.
|
||||
fn glyph_advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Vector2I>;
|
||||
|
||||
/// Computes the size of the canvas needed to rasterize the glyph.
|
||||
fn glyph_raster_bounds(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
size: f32,
|
||||
glyph_id: GlyphId,
|
||||
scale: Vector2F,
|
||||
glyph_config: &rendering::GlyphConfig,
|
||||
) -> Result<RectI>;
|
||||
|
||||
/// Computes the bounding box of a glyph with respect to surrounding glyphs.
|
||||
fn glyph_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<RectI>;
|
||||
|
||||
/// Rasterizes a single glyph so it can be rendered to the screen.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
size: f32,
|
||||
glyph_id: GlyphId,
|
||||
scale: Vector2F,
|
||||
subpixel_alignment: SubpixelAlignment,
|
||||
glyph_config: &rendering::GlyphConfig,
|
||||
format: RasterFormat,
|
||||
) -> Result<RasterizedGlyph>;
|
||||
|
||||
/// Returns the ID of the glyph which represents the given character in the
|
||||
/// given font.
|
||||
fn glyph_for_char(&self, font_id: FontId, char: char) -> Option<GlyphId>;
|
||||
|
||||
fn text_layout_system(&self) -> &dyn TextLayoutSystem;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, num_derive::FromPrimitive, PartialEq, Eq)]
|
||||
pub enum FullscreenState {
|
||||
#[default]
|
||||
Normal = 0,
|
||||
Fullscreen = 1,
|
||||
Maximized = 2,
|
||||
}
|
||||
|
||||
pub trait Window: 'static + WindowContext + std::any::Any {
|
||||
fn minimize(&self);
|
||||
fn toggle_maximized(&self);
|
||||
fn toggle_fullscreen(&self);
|
||||
fn fullscreen_state(&self) -> FullscreenState;
|
||||
/// Whether the window has the native OS window frame (title bar and buttons).
|
||||
fn uses_native_window_decorations(&self) -> bool;
|
||||
fn set_titlebar_height(&self, height: f64);
|
||||
|
||||
/// Whether any hardware supports window transparency
|
||||
fn supports_transparency(&self) -> bool;
|
||||
fn graphics_backend(&self) -> GraphicsBackend;
|
||||
fn supported_backends(&self) -> Vec<GraphicsBackend>;
|
||||
|
||||
fn as_ctx(&self) -> &dyn WindowContext;
|
||||
fn callbacks(&self) -> &WindowCallbacks;
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
}
|
||||
|
||||
pub trait WindowContext {
|
||||
/// Returns the current inner (content) size of the window, in logical
|
||||
/// pixels.
|
||||
fn size(&self) -> Vector2F;
|
||||
|
||||
/// Returns the position of the window origin (top-left corner) within the
|
||||
/// screen, in logical pixels.
|
||||
fn origin(&self) -> Vector2F;
|
||||
|
||||
/// Returns the scale factor for the window surface.
|
||||
fn backing_scale_factor(&self) -> f32;
|
||||
|
||||
/// The maximum dimension size in pixels, either width or height, for a 2D-texture. `None`
|
||||
/// will be treated as unbounded.
|
||||
fn max_texture_dimension_2d(&self) -> Option<u32>;
|
||||
|
||||
/// Provides the window the next scene to render and asks it to schedule a
|
||||
/// redraw.
|
||||
fn render_scene(&self, scene: Rc<Scene>);
|
||||
|
||||
/// Schedules a redraw of the window.
|
||||
fn request_redraw(&self);
|
||||
|
||||
/// Requests a frame capture on the next render.
|
||||
///
|
||||
/// When the frame is captured, the provided callback will be invoked with the
|
||||
/// captured frame data.
|
||||
fn request_frame_capture(&self, callback: Box<dyn FnOnce(CapturedFrame) + Send + 'static>);
|
||||
}
|
||||
|
||||
/// Pixel format of the data in a `CapturedFrame`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CapturedFrameFormat {
|
||||
Rgba,
|
||||
Bgra,
|
||||
}
|
||||
|
||||
/// A captured frame containing pixel data in the format indicated by `format`.
|
||||
#[derive(Clone)]
|
||||
pub struct CapturedFrame {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub data: Vec<u8>,
|
||||
pub format: CapturedFrameFormat,
|
||||
}
|
||||
|
||||
impl CapturedFrame {
|
||||
pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
format: CapturedFrameFormat::Rgba,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_bgra(width: u32, height: u32, data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
format: CapturedFrameFormat::Bgra,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_rgba(&mut self) {
|
||||
if self.format == CapturedFrameFormat::Bgra {
|
||||
for chunk in self.data.chunks_exact_mut(4) {
|
||||
chunk.swap(0, 2);
|
||||
}
|
||||
self.format = CapturedFrameFormat::Rgba;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
|
||||
pub enum WindowFocusBehavior {
|
||||
/// Brings the window to the front when focusing the app.
|
||||
#[default]
|
||||
BringToFront,
|
||||
/// Retain the window's current position in the z-index when
|
||||
/// focusing the app. May not be supported on all platforms.
|
||||
RetainZIndex,
|
||||
}
|
||||
|
||||
/// Common interface for abstracting platform-specific windowing logic.
|
||||
pub trait WindowManager {
|
||||
fn open_window(
|
||||
&mut self,
|
||||
window_id: WindowId,
|
||||
window_options: WindowOptions,
|
||||
callbacks: WindowCallbacks,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Returns a platform-independent trait-object for the window with the given ID.
|
||||
fn platform_window(&self, window_id: WindowId) -> OptionalPlatformWindow;
|
||||
|
||||
/// Drop a window. Note that other pieces of state pointing to this window ID must also be
|
||||
/// removed from [`AppContext`].
|
||||
fn remove_window(&mut self, window_id: WindowId);
|
||||
|
||||
/// \return the window ID of the window that is active (has typing focus), or None if none.
|
||||
fn active_window_id(&self) -> Option<WindowId>;
|
||||
|
||||
/// \return the active window is an alert modal
|
||||
fn key_window_is_modal_panel(&self) -> bool;
|
||||
|
||||
/// \return if the app is currently active.
|
||||
fn app_is_active(&self) -> bool;
|
||||
|
||||
/// Makes all the app's windows visible, and transfer focus to whichever window most recently
|
||||
/// had focus.
|
||||
/// \return the window ID of the window that will become active, which may be None if we are on
|
||||
/// a platform that allows the app to run without any open windows.
|
||||
fn activate_app(&self, last_active_window: Option<WindowId>) -> Option<WindowId>;
|
||||
fn show_window_and_focus_app(&self, window_id: WindowId, behavior: WindowFocusBehavior);
|
||||
fn hide_app(&self);
|
||||
fn hide_window(&self, window_id: WindowId);
|
||||
fn set_window_bounds(&self, window_id: WindowId, bound: RectF);
|
||||
|
||||
/// Sets the background blur radius for all windows to the given `blur_radius_pixels` value.
|
||||
fn set_all_windows_background_blur_radius(&self, blur_radius_pixels: u8);
|
||||
|
||||
/// [Windows only] Sets the background blur texture (Acrylic) for all windows.
|
||||
fn set_all_windows_background_blur_texture(&self, use_blur_texture: bool);
|
||||
|
||||
fn set_window_title(&self, window_id: WindowId, title: &str);
|
||||
|
||||
/// Closes a window asynchronously. This is done asynchronously solely because the UI framework
|
||||
/// incorrectly assumes that a call to platform code cannot synchronously trigger a callback
|
||||
/// back to the UI framework. For example, closing window will also synchronously trigger a
|
||||
/// `window_will_close`, which will crash the app with a BorrowMut error. To avoid this error,
|
||||
/// we do this asynchronously.
|
||||
fn close_window_async(&self, window_id: WindowId, termination_mode: TerminationMode);
|
||||
|
||||
/// Returns the display bound for the current active display.
|
||||
fn active_display_bounds(&self) -> geometry::rect::RectF;
|
||||
/// Returns the unique identifier for the current active display.
|
||||
fn active_display_id(&self) -> DisplayId;
|
||||
fn display_count(&self) -> usize;
|
||||
fn bounds_for_display_idx(&self, idx: DisplayIdx) -> Option<RectF>;
|
||||
|
||||
fn active_cursor_position_updated(&self);
|
||||
|
||||
fn windowing_system(&self) -> Option<crate::windowing::System>;
|
||||
|
||||
/// The name of the operating system's window server/manager/compositor.
|
||||
fn os_window_manager_name(&self) -> Option<String>;
|
||||
/// Whether or not this is a tiling window manager.
|
||||
fn is_tiling_window_manager(&self) -> bool;
|
||||
|
||||
/// Returns the IDs of all application windows in front-to-back z-order.
|
||||
/// An empty vector indicates that z-ordering information is not available
|
||||
/// on this platform.
|
||||
fn ordered_window_ids(&self) -> Vec<WindowId> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn cancel_synthetic_drag(&self, _window_id: WindowId) {}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum SystemTheme {
|
||||
#[default]
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Cursor {
|
||||
Arrow,
|
||||
IBeam,
|
||||
Crosshair,
|
||||
OpenHand,
|
||||
ClosedHand,
|
||||
NotAllowed,
|
||||
PointingHand,
|
||||
ResizeLeftRight,
|
||||
ResizeUpDown,
|
||||
/// The drag copy cursor, indicating the currently will result in a copy action.
|
||||
DragCopy,
|
||||
}
|
||||
|
||||
/// The current operating system in which this library is running. If on the web, this reads the
|
||||
/// user agent to determine the backing OS, otherwise this is determined at compile time based on
|
||||
/// the value of `target_arch` (<https://doc.rust-lang.org/reference/conditional-compilation.html#target_arch>).
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum OperatingSystem {
|
||||
/// Any distribution of Linux.
|
||||
Linux,
|
||||
/// MacOS.
|
||||
Mac,
|
||||
/// Windows.
|
||||
Windows,
|
||||
/// The operating system is unknown or not one of the ones specified.
|
||||
/// Contains the name of the operating system if it is known.
|
||||
Other(Option<&'static str>),
|
||||
}
|
||||
|
||||
impl OperatingSystem {
|
||||
pub fn get() -> Self {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_family = "wasm")] {
|
||||
wasm::current_platform()
|
||||
} else if #[cfg(target_os = "linux")] {
|
||||
OperatingSystem::Linux
|
||||
} else if #[cfg(target_os = "macos")] {
|
||||
OperatingSystem::Mac
|
||||
} else if #[cfg(windows)] {
|
||||
OperatingSystem::Windows
|
||||
} else {
|
||||
OperatingSystem::Other(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the current [`OperatingSystem`] is Mac.
|
||||
pub fn is_mac(&self) -> bool {
|
||||
*self == OperatingSystem::Mac
|
||||
}
|
||||
|
||||
/// Returns true if the current [`OperatingSystem`] is Linux.
|
||||
pub fn is_linux(&self) -> bool {
|
||||
*self == OperatingSystem::Linux
|
||||
}
|
||||
|
||||
/// Returns true if the current [`OperatingSystem`] is Windows.
|
||||
pub fn is_windows(&self) -> bool {
|
||||
*self == OperatingSystem::Windows
|
||||
}
|
||||
|
||||
pub fn default_shell_family(&self) -> ShellFamily {
|
||||
match self {
|
||||
OperatingSystem::Linux | OperatingSystem::Mac | OperatingSystem::Other(_) => {
|
||||
ShellFamily::Posix
|
||||
}
|
||||
OperatingSystem::Windows => ShellFamily::PowerShell,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema_gen", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(
|
||||
feature = "schema_gen",
|
||||
schemars(
|
||||
description = "Graphics rendering backend used for display output.",
|
||||
rename_all = "snake_case"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(feature = "settings_value", derive(settings_value::SettingsValue))]
|
||||
pub enum GraphicsBackend {
|
||||
/// This maps to [`wgpu::Backend::Empty`].
|
||||
#[cfg_attr(
|
||||
feature = "schema_gen",
|
||||
schemars(description = "No-op backend for testing.")
|
||||
)]
|
||||
Empty,
|
||||
#[cfg_attr(feature = "schema_gen", schemars(description = "DirectX 12."))]
|
||||
Dx12,
|
||||
#[cfg_attr(feature = "schema_gen", schemars(description = "Vulkan."))]
|
||||
Vulkan,
|
||||
#[cfg_attr(feature = "schema_gen", schemars(description = "OpenGL."))]
|
||||
Gl,
|
||||
#[cfg_attr(feature = "schema_gen", schemars(description = "Metal."))]
|
||||
Metal,
|
||||
#[cfg_attr(feature = "schema_gen", schemars(description = "WebGPU (browser)."))]
|
||||
BrowserWebGpu,
|
||||
}
|
||||
|
||||
impl GraphicsBackend {
|
||||
pub fn to_label(&self) -> &'static str {
|
||||
match self {
|
||||
GraphicsBackend::Empty => "",
|
||||
GraphicsBackend::Dx12 => "DirectX 12",
|
||||
GraphicsBackend::Vulkan => "Vulkan",
|
||||
GraphicsBackend::Gl => "OpenGL",
|
||||
GraphicsBackend::Metal => "Metal",
|
||||
GraphicsBackend::BrowserWebGpu => "WebGPU",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use futures_util::future::LocalBoxFuture;
|
||||
|
||||
use crate::{assets::AssetProvider, integration::TestDriver, platform, AppContext};
|
||||
|
||||
pub struct App;
|
||||
|
||||
impl App {
|
||||
#[allow(dead_code)]
|
||||
pub(in crate::platform) fn new(
|
||||
_callbacks: platform::app::AppCallbacks,
|
||||
_assets: Box<dyn AssetProvider>,
|
||||
_test_driver: Option<&TestDriver>,
|
||||
) -> Self {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(in crate::platform) fn run(
|
||||
self,
|
||||
_init_fn: impl FnOnce(&mut AppContext, LocalBoxFuture<'static, crate::App>),
|
||||
) {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,666 @@
|
||||
use crate::clipboard::InMemoryClipboard;
|
||||
use crate::fonts::FamilyId;
|
||||
use crate::geometry;
|
||||
use crate::keymap::Keystroke;
|
||||
use crate::modals::{AlertDialog, ModalId};
|
||||
use crate::platform::{
|
||||
self,
|
||||
file_picker::{FilePickerCallback, FilePickerConfiguration},
|
||||
Cursor, RequestNotificationPermissionsCallback, SendNotificationErrorCallback,
|
||||
WindowFocusBehavior, WindowOptions,
|
||||
};
|
||||
use crate::platform::{MicrophoneAccessState, TerminationMode, TextLayoutSystem};
|
||||
use crate::text_layout::TextAlignment;
|
||||
use crate::windowing::WindowCallbacks;
|
||||
use crate::{accessibility::AccessibilityContent, notification::UserNotification, Scene, WindowId};
|
||||
use crate::{ApplicationBundleInfo, DisplayId, DisplayIdx, OptionalPlatformWindow};
|
||||
use anyhow::Result;
|
||||
use parking_lot::Mutex;
|
||||
use pathfinder_geometry::rect::RectI;
|
||||
use pathfinder_geometry::vector::{vec2i, Vector2I};
|
||||
use pathfinder_geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct AppDelegate {
|
||||
clipboard: InMemoryClipboard,
|
||||
cursor_shape: Mutex<Cursor>,
|
||||
}
|
||||
|
||||
// Dummy IntegrationTestDelegate implementation so the integration test code
|
||||
// builds on non-mac platforms (even though running them there is a no-op for now).
|
||||
// This is relevant to build on Linux for GitHub Actions.
|
||||
pub struct IntegrationTestDelegate {
|
||||
clipboard: InMemoryClipboard,
|
||||
cursor_shape: Mutex<Cursor>,
|
||||
}
|
||||
|
||||
pub struct Window {
|
||||
callbacks: WindowCallbacks,
|
||||
}
|
||||
|
||||
impl AppDelegate {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
clipboard: InMemoryClipboard::default(),
|
||||
cursor_shape: Mutex::new(Cursor::Arrow),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntegrationTestDelegate {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
clipboard: InMemoryClipboard::default(),
|
||||
cursor_shape: Mutex::new(Cursor::Arrow),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct WindowManager {
|
||||
windows: HashMap<WindowId, Rc<Window>>,
|
||||
}
|
||||
|
||||
impl WindowManager {
|
||||
pub(crate) fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::WindowManager for WindowManager {
|
||||
fn open_window(
|
||||
&mut self,
|
||||
window_id: WindowId,
|
||||
_window_options: WindowOptions,
|
||||
callbacks: WindowCallbacks,
|
||||
) -> Result<()> {
|
||||
self.windows
|
||||
.insert(window_id, Rc::new(Window { callbacks }));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn platform_window(&self, window_id: WindowId) -> OptionalPlatformWindow {
|
||||
self.windows
|
||||
.get(&window_id)
|
||||
.map(Rc::clone)
|
||||
.map(|window| window as Rc<dyn platform::Window>)
|
||||
}
|
||||
|
||||
fn remove_window(&mut self, window_id: WindowId) {
|
||||
self.windows.remove(&window_id);
|
||||
}
|
||||
|
||||
fn active_window_id(&self) -> Option<WindowId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn key_window_is_modal_panel(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn app_is_active(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn activate_app(&self, _last_active_window: Option<WindowId>) -> Option<WindowId> {
|
||||
// no-op for tests
|
||||
None
|
||||
}
|
||||
|
||||
fn show_window_and_focus_app(&self, _window_id: WindowId, _behavior: WindowFocusBehavior) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn hide_app(&self) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn hide_window(&self, _window_id: WindowId) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn set_window_bounds(&self, _window_id: WindowId, _bound: RectF) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn set_all_windows_background_blur_radius(&self, _blur_radius_pixels: u8) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn set_all_windows_background_blur_texture(&self, _use_blur_texture: bool) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn set_window_title(&self, _window_id: WindowId, _title: &str) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn close_window_async(&self, _window_id: WindowId, _termination_mode: TerminationMode) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn active_display_bounds(&self) -> geometry::rect::RectF {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn active_display_id(&self) -> DisplayId {
|
||||
DisplayId::from(0)
|
||||
}
|
||||
|
||||
fn display_count(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn bounds_for_display_idx(&self, _idx: DisplayIdx) -> Option<RectF> {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn active_cursor_position_updated(&self) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn windowing_system(&self) -> Option<crate::windowing::System> {
|
||||
None
|
||||
}
|
||||
|
||||
fn os_window_manager_name(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_tiling_window_manager(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::Delegate for AppDelegate {
|
||||
#[cfg(feature = "test-util")]
|
||||
fn get_cursor_shape(&self) -> Cursor {
|
||||
*self.cursor_shape.lock()
|
||||
}
|
||||
|
||||
fn set_cursor_shape(&self, cursor: Cursor) {
|
||||
*self.cursor_shape.lock() = cursor;
|
||||
}
|
||||
|
||||
fn open_url(&self, _: &str) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn close_ime_async(&self, _window_id: WindowId) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn open_character_palette(&self) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn open_file_path(&self, _: &Path) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn open_file_path_in_explorer(&self, _: &Path) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn open_file_picker(
|
||||
&self,
|
||||
_callback: FilePickerCallback,
|
||||
_file_picker_config: FilePickerConfiguration,
|
||||
) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn open_save_file_picker(
|
||||
&self,
|
||||
_callback: platform::SaveFilePickerCallback,
|
||||
_config: platform::SaveFilePickerConfiguration,
|
||||
) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn application_bundle_info(&self, _: &str) -> Option<ApplicationBundleInfo<'_>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_ime_open(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_accessibility_contents(&self, _: AccessibilityContent) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn request_user_attention(&self, _window_id: WindowId) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn request_desktop_notification_permissions(
|
||||
&self,
|
||||
_on_completion: RequestNotificationPermissionsCallback,
|
||||
) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn send_desktop_notification(
|
||||
&self,
|
||||
_notification_content: UserNotification,
|
||||
_window_id: WindowId,
|
||||
_on_error: SendNotificationErrorCallback,
|
||||
) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn clipboard(&mut self) -> &mut dyn crate::Clipboard {
|
||||
&mut self.clipboard
|
||||
}
|
||||
|
||||
fn system_theme(&self) -> platform::SystemTheme {
|
||||
platform::SystemTheme::Light
|
||||
}
|
||||
|
||||
fn dispatch_delegate(&self) -> Arc<dyn platform::DispatchDelegate> {
|
||||
Arc::new(DispatchDelegate)
|
||||
}
|
||||
|
||||
fn register_global_shortcut(&self, _: Keystroke) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn unregister_global_shortcut(&self, _: &Keystroke) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn terminate_app(&self, _termination_mode: TerminationMode) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn is_screen_reader_enabled(&self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
||||
fn microphone_access_state(&self) -> MicrophoneAccessState {
|
||||
MicrophoneAccessState::NotDetermined
|
||||
}
|
||||
|
||||
fn show_native_platform_modal(&self, _id: ModalId, _modal: AlertDialog) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::Delegate for IntegrationTestDelegate {
|
||||
#[cfg(feature = "test-util")]
|
||||
fn get_cursor_shape(&self) -> Cursor {
|
||||
*self.cursor_shape.lock()
|
||||
}
|
||||
|
||||
fn set_cursor_shape(&self, cursor: Cursor) {
|
||||
*self.cursor_shape.lock() = cursor;
|
||||
}
|
||||
|
||||
fn open_url(&self, _: &str) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn close_ime_async(&self, _window_id: WindowId) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn open_character_palette(&self) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn open_file_path(&self, _: &Path) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn open_file_path_in_explorer(&self, _: &Path) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn open_file_picker(
|
||||
&self,
|
||||
_callback: FilePickerCallback,
|
||||
_file_picker_config: FilePickerConfiguration,
|
||||
) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn open_save_file_picker(
|
||||
&self,
|
||||
_callback: platform::SaveFilePickerCallback,
|
||||
_config: platform::SaveFilePickerConfiguration,
|
||||
) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn application_bundle_info(&self, _: &str) -> Option<ApplicationBundleInfo<'_>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_ime_open(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_accessibility_contents(&self, _: AccessibilityContent) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn request_user_attention(&self, _window_id: WindowId) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn request_desktop_notification_permissions(
|
||||
&self,
|
||||
_on_completion: RequestNotificationPermissionsCallback,
|
||||
) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn send_desktop_notification(
|
||||
&self,
|
||||
_notification_content: UserNotification,
|
||||
_window_id: WindowId,
|
||||
_on_error: SendNotificationErrorCallback,
|
||||
) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn clipboard(&mut self) -> &mut dyn crate::Clipboard {
|
||||
&mut self.clipboard
|
||||
}
|
||||
|
||||
fn system_theme(&self) -> platform::SystemTheme {
|
||||
platform::SystemTheme::Light
|
||||
}
|
||||
|
||||
fn dispatch_delegate(&self) -> Arc<dyn platform::DispatchDelegate> {
|
||||
Arc::new(DispatchDelegate)
|
||||
}
|
||||
|
||||
fn register_global_shortcut(&self, _: Keystroke) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn unregister_global_shortcut(&self, _: &Keystroke) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn terminate_app(&self, _termination_mode: TerminationMode) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
fn is_screen_reader_enabled(&self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
||||
fn microphone_access_state(&self) -> MicrophoneAccessState {
|
||||
MicrophoneAccessState::NotDetermined
|
||||
}
|
||||
|
||||
fn show_native_platform_modal(&self, _id: ModalId, _modal: AlertDialog) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::Window for Window {
|
||||
fn callbacks(&self) -> &crate::windowing::WindowCallbacks {
|
||||
&self.callbacks
|
||||
}
|
||||
|
||||
fn minimize(&self) {}
|
||||
|
||||
fn toggle_maximized(&self) {}
|
||||
|
||||
fn toggle_fullscreen(&self) {}
|
||||
|
||||
fn fullscreen_state(&self) -> platform::FullscreenState {
|
||||
platform::FullscreenState::Normal
|
||||
}
|
||||
|
||||
fn set_titlebar_height(&self, _height: f64) {}
|
||||
|
||||
fn as_ctx(&self) -> &dyn platform::WindowContext {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn supports_transparency(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn graphics_backend(&self) -> platform::GraphicsBackend {
|
||||
platform::GraphicsBackend::Empty
|
||||
}
|
||||
|
||||
fn supported_backends(&self) -> Vec<platform::GraphicsBackend> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn uses_native_window_decorations(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::WindowContext for Window {
|
||||
fn size(&self) -> Vector2F {
|
||||
vec2f(1024.0, 768.0)
|
||||
}
|
||||
|
||||
fn origin(&self) -> Vector2F {
|
||||
vec2f(0., 0.)
|
||||
}
|
||||
|
||||
fn backing_scale_factor(&self) -> f32 {
|
||||
2.0
|
||||
}
|
||||
|
||||
fn max_texture_dimension_2d(&self) -> Option<u32> {
|
||||
// For tests, choose a limit so low that it can run on any device.
|
||||
// https://github.com/gfx-rs/wgpu/blob/3b6112d45de8da75e47270fe3b0329e5d5166585/wgpu-types/src/lib.rs#L1278
|
||||
Some(2048)
|
||||
}
|
||||
|
||||
fn render_scene(&self, _scene: Rc<Scene>) {}
|
||||
|
||||
fn request_redraw(&self) {}
|
||||
|
||||
fn request_frame_capture(
|
||||
&self,
|
||||
_callback: Box<dyn FnOnce(platform::CapturedFrame) + Send + 'static>,
|
||||
) {
|
||||
// no-op for tests
|
||||
}
|
||||
}
|
||||
|
||||
struct DispatchDelegate;
|
||||
|
||||
impl platform::DispatchDelegate for DispatchDelegate {
|
||||
fn is_main_thread(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn run_on_main_thread(&self, _task: async_task::Runnable) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
struct LoadedSystemFonts;
|
||||
impl platform::LoadedSystemFonts for LoadedSystemFonts {
|
||||
fn as_any(self: Box<Self>) -> Box<dyn Any> {
|
||||
self as Box<dyn Any>
|
||||
}
|
||||
}
|
||||
|
||||
/// A no-op font cache for use in tests that don't want to use full platform
|
||||
/// functionality.
|
||||
#[derive(Default)]
|
||||
pub struct FontDB;
|
||||
|
||||
impl FontDB {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::FontDB for FontDB {
|
||||
fn load_from_bytes(&mut self, _name: &str, _bytes: Vec<Vec<u8>>) -> Result<FamilyId> {
|
||||
Ok(FamilyId(0))
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn load_from_system(&mut self, _font_family: &str) -> Result<FamilyId> {
|
||||
Ok(FamilyId(0))
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn load_all_system_fonts(
|
||||
&self,
|
||||
) -> futures::future::BoxFuture<'static, Box<dyn platform::LoadedSystemFonts>> {
|
||||
use futures::FutureExt as _;
|
||||
|
||||
futures::future::ready(Box::new(LoadedSystemFonts) as Box<dyn platform::LoadedSystemFonts>)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn process_loaded_system_fonts(
|
||||
&mut self,
|
||||
loaded_system_fonts: Box<dyn platform::LoadedSystemFonts>,
|
||||
) -> Vec<(Option<FamilyId>, crate::fonts::FontInfo)> {
|
||||
let _loaded_system_fonts: Box<LoadedSystemFonts> = loaded_system_fonts
|
||||
.as_any()
|
||||
.downcast()
|
||||
.expect("should not fail to downcast to concrete type");
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn fallback_fonts(
|
||||
&self,
|
||||
_ch: char,
|
||||
_font_id: crate::fonts::FontId,
|
||||
) -> Vec<crate::fonts::FontId> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn select_font(
|
||||
&self,
|
||||
_family_id: crate::fonts::FamilyId,
|
||||
_properties: crate::fonts::Properties,
|
||||
) -> crate::fonts::FontId {
|
||||
crate::fonts::FontId(0)
|
||||
}
|
||||
|
||||
fn font_metrics(&self, _font_id: crate::fonts::FontId) -> crate::fonts::Metrics {
|
||||
crate::fonts::Metrics {
|
||||
units_per_em: 2048,
|
||||
ascent: 1901_i16,
|
||||
descent: (-483_i16),
|
||||
line_gap: 0_i16,
|
||||
}
|
||||
}
|
||||
|
||||
fn glyph_advance(
|
||||
&self,
|
||||
_font_id: crate::fonts::FontId,
|
||||
_glyph_id: crate::fonts::GlyphId,
|
||||
) -> Result<Vector2I> {
|
||||
Ok(Vector2I::zero())
|
||||
}
|
||||
|
||||
fn load_family_name_from_id(&self, _id: crate::fonts::FamilyId) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn glyph_raster_bounds(
|
||||
&self,
|
||||
_font_id: crate::fonts::FontId,
|
||||
_size: f32,
|
||||
_glyph_id: crate::fonts::GlyphId,
|
||||
_scale: Vector2F,
|
||||
_glyph_config: &crate::rendering::GlyphConfig,
|
||||
) -> Result<pathfinder_geometry::rect::RectI> {
|
||||
Ok(pathfinder_geometry::rect::RectI::default())
|
||||
}
|
||||
|
||||
fn glyph_typographic_bounds(
|
||||
&self,
|
||||
_font_id: crate::fonts::FontId,
|
||||
_glyph_id: crate::fonts::GlyphId,
|
||||
) -> Result<RectI> {
|
||||
Ok(RectI::default())
|
||||
}
|
||||
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
_font_id: crate::fonts::FontId,
|
||||
_size: f32,
|
||||
_glyph_id: crate::fonts::GlyphId,
|
||||
_scale: Vector2F,
|
||||
_subpixel_alignment: crate::fonts::SubpixelAlignment,
|
||||
_glyph_config: &crate::rendering::GlyphConfig,
|
||||
_format: crate::fonts::canvas::RasterFormat,
|
||||
) -> Result<crate::fonts::RasterizedGlyph> {
|
||||
Ok(crate::fonts::RasterizedGlyph {
|
||||
canvas: crate::fonts::canvas::Canvas {
|
||||
pixels: vec![],
|
||||
size: vec2i(0, 0),
|
||||
row_stride: 0,
|
||||
format: crate::fonts::canvas::RasterFormat::Rgba32,
|
||||
},
|
||||
is_emoji: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn glyph_for_char(
|
||||
&self,
|
||||
_font_id: crate::fonts::FontId,
|
||||
_char: char,
|
||||
) -> Option<crate::fonts::GlyphId> {
|
||||
Some(0)
|
||||
}
|
||||
|
||||
fn family_id_for_name(&self, _name: &str) -> Option<FamilyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn text_layout_system(&self) -> &dyn TextLayoutSystem {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::TextLayoutSystem for FontDB {
|
||||
fn layout_line(
|
||||
&self,
|
||||
_text: &str,
|
||||
line_style: platform::LineStyle,
|
||||
_style_runs: &[(std::ops::Range<usize>, crate::text_layout::StyleAndFont)],
|
||||
_max_width: f32,
|
||||
_clip_config: crate::text_layout::ClipConfig,
|
||||
) -> crate::text_layout::Line {
|
||||
crate::text_layout::Line::empty(line_style.font_size, line_style.line_height_ratio, 0)
|
||||
}
|
||||
|
||||
fn layout_text(
|
||||
&self,
|
||||
_text: &str,
|
||||
line_style: platform::LineStyle,
|
||||
_style_runs: &[(std::ops::Range<usize>, crate::text_layout::StyleAndFont)],
|
||||
_max_width: f32,
|
||||
_max_height: f32,
|
||||
_alignment: TextAlignment,
|
||||
_first_line_head_indent: Option<f32>,
|
||||
) -> crate::text_layout::TextFrame {
|
||||
crate::text_layout::TextFrame::empty(line_style.font_size, line_style.line_height_ratio)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mod app;
|
||||
mod delegate;
|
||||
|
||||
pub use app::App;
|
||||
pub(crate) use delegate::WindowManager;
|
||||
pub use delegate::{AppDelegate, FontDB, IntegrationTestDelegate};
|
||||
@@ -0,0 +1,103 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use woothee::parser::{Parser, WootheeResult};
|
||||
|
||||
use crate::platform::OperatingSystem;
|
||||
|
||||
static PARSED_USER_AGENT: OnceLock<Option<ParsedUserAgent>> = OnceLock::new();
|
||||
static PLATFORM: OnceLock<OperatingSystem> = OnceLock::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ParsedUserAgent {
|
||||
os: String,
|
||||
/// For macOS, the version number is probably incorrect as it is currently
|
||||
/// capped at 10.15. See: https://bugs.webkit.org/show_bug.cgi?id=216593.
|
||||
/// It's possible to get the correct version using the Client Hints API, but
|
||||
/// this is currently only supported by Chrome: https://developer.mozilla.org/en-US/docs/Web/API/User-Agent_Client_Hints_API.
|
||||
os_version: String,
|
||||
browser: String,
|
||||
browser_version: String,
|
||||
}
|
||||
|
||||
impl ParsedUserAgent {
|
||||
/// Converts the result we get from parsing the user agent into a struct
|
||||
/// with owned values.
|
||||
fn from_woothee_result(result: &WootheeResult) -> Self {
|
||||
ParsedUserAgent {
|
||||
os: result.os.to_string(),
|
||||
os_version: result.os_version.to_string(),
|
||||
browser: result.name.to_string(),
|
||||
browser_version: result.version.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parsed_user_agent() -> Option<&'static ParsedUserAgent> {
|
||||
PARSED_USER_AGENT
|
||||
.get_or_init(|| {
|
||||
let Ok(user_agent) = gloo::utils::window().navigator().user_agent() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let parser = Parser::new();
|
||||
parser
|
||||
.parse(user_agent.as_str())
|
||||
.map(|result| ParsedUserAgent::from_woothee_result(&result))
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the current operating system by reading the user agent. If the user agent was not able
|
||||
/// to be read, [`OperatingSystem::Other`] is returned.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if called before the app was attached to the DOM.
|
||||
pub(super) fn current_platform() -> OperatingSystem {
|
||||
*PLATFORM.get_or_init(|| {
|
||||
let Some(parsed_user_agent) = parsed_user_agent() else {
|
||||
return OperatingSystem::Other(None);
|
||||
};
|
||||
|
||||
// Try to parse the user agent to determine the OS. _heavily_ inspired by
|
||||
// https://github.com/mozilla-services/contile/blob/61da2719fa4586fc0b15fe7f47ebbc1586f28a47/src/web/user_agent.rs#L95-L105.
|
||||
let os = parsed_user_agent.os.to_lowercase();
|
||||
match os.as_str() {
|
||||
_ if os.starts_with("windows") => OperatingSystem::Windows,
|
||||
"mac osx" => OperatingSystem::Mac,
|
||||
"linux" => OperatingSystem::Linux,
|
||||
_ => OperatingSystem::Other(Some(&parsed_user_agent.os)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the user agent provided by the browser. If the user agent was
|
||||
/// unable to be read, returns None.
|
||||
pub fn user_agent() -> Option<String> {
|
||||
gloo::utils::window().navigator().user_agent().ok()
|
||||
}
|
||||
|
||||
/// Returns the version of the current operating system, parsed from the user
|
||||
/// agent. If the user agent was not able to be read, returns None.
|
||||
///
|
||||
/// Also returns None if the current operating system is macOS. The version
|
||||
/// reported to the user agent is capped at 10.15, meaning it is probably
|
||||
/// incorrect in most cases: https://bugs.webkit.org/show_bug.cgi?id=216593.
|
||||
pub fn current_os_version() -> Option<&'static str> {
|
||||
if matches!(current_platform(), OperatingSystem::Mac) {
|
||||
return None;
|
||||
};
|
||||
|
||||
parsed_user_agent().map(|ua| ua.os_version.as_str())
|
||||
}
|
||||
|
||||
/// Returns the name of the browser, parsed from the user agent. If the user
|
||||
/// agent was not able to be read, returns None.
|
||||
pub fn current_browser() -> Option<&'static str> {
|
||||
parsed_user_agent().map(|ua| ua.browser.as_str())
|
||||
}
|
||||
|
||||
/// Returns the version of the current browser, parsed from the user agent.
|
||||
/// If the user agent was not able to be read, returns None.
|
||||
pub fn current_browser_version() -> Option<&'static str> {
|
||||
parsed_user_agent().map(|ua| ua.browser_version.as_str())
|
||||
}
|
||||
Reference in New Issue
Block a user