Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though

This commit is contained in:
Ryan Ward
2026-05-07 11:29:34 -05:00
parent f4e2475c60
commit a41cbd8cc7
2433 changed files with 14208 additions and 9409 deletions
+339
View File
@@ -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 Suns USB keyboard.
Again,
/// Found on Suns USB keyboard.
Copy,
/// Found on Suns USB keyboard.
Cut,
/// Found on Suns USB keyboard.
Find,
/// Found on Suns USB keyboard.
Open,
/// Found on Suns USB keyboard.
Paste,
/// Found on Suns USB keyboard.
Props,
/// Found on Suns USB keyboard.
Select,
/// Found on Suns 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,
}
+168
View File
@@ -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),
}
}
}
+749
View File
@@ -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};
+103
View File
@@ -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())
}