Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
@@ -0,0 +1,165 @@
|
||||
[package]
|
||||
name = "galaxyui"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
license = "MIT"
|
||||
|
||||
[features]
|
||||
# Enabling this feature works around a bug in Metal frame capture by loading
|
||||
# Metal shaders from the file system instead of from data compiled into the
|
||||
# binary. The shaders are written to the temp directory during initialization of
|
||||
# a renderer to enable this, which could slow opening of the first window.
|
||||
enable-metal-frame-capture = []
|
||||
# Enables the font-kit rasterizer on Windows.
|
||||
fontkit-rasterizer = []
|
||||
traces = ["galaxyui_core/traces"]
|
||||
# Feature enabled only when app is compiled for integration tests.
|
||||
integration_tests = ["galaxyui_core/integration_tests"]
|
||||
# If enabled, writes named telemetry events to the info log. Should never be
|
||||
# enabled in release builds without a PII audit of telemetry data.
|
||||
log_named_telemetry_events = ["galaxyui_core/log_named_telemetry_events"]
|
||||
schema_gen = ["galaxyui_core/schema_gen"]
|
||||
settings_value = ["galaxyui_core/settings_value"]
|
||||
# Enables utilities for testing.
|
||||
test-util = ["galaxyui_core/test-util"]
|
||||
experimental-wgpu-renderer = ["dep:wgpu", "dep:bytemuck", "dep:version-compare"]
|
||||
|
||||
[dependencies]
|
||||
async-task.workspace = true
|
||||
anyhow.workspace = true
|
||||
bytemuck = { workspace = true, features = ["derive"], optional = true }
|
||||
cfg-if.workspace = true
|
||||
dashmap.workspace = true
|
||||
derive_more.workspace = true
|
||||
futures.workspace = true
|
||||
futures-util.workspace = true
|
||||
instant.workspace = true
|
||||
itertools.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
markdown_parser.workspace = true
|
||||
num-traits.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
pathfinder_color.workspace = true
|
||||
pathfinder_geometry.workspace = true
|
||||
sum_tree.workspace = true
|
||||
takecell = "0.1.1"
|
||||
thiserror.workspace = true
|
||||
vec1.workspace = true
|
||||
version-compare = { workspace = true, optional = true }
|
||||
galaxyui_core.workspace = true
|
||||
wgpu = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
asset_cache.workspace = true
|
||||
env_logger.workspace = true
|
||||
futures-timer = "3.0.2"
|
||||
image = { workspace = true, default-features = false, features = ["png"] }
|
||||
rust-embed.workspace = true
|
||||
galaxyui = { workspace = true, features = ["test-util"] }
|
||||
galaxyui_core = { workspace = true, features = ["test-util"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
block.workspace = true
|
||||
chrono.workspace = true
|
||||
cocoa.workspace = true
|
||||
core-foundation.workspace = true
|
||||
core-graphics = "0.25.0"
|
||||
core-text = "21.0.0"
|
||||
dispatch = "0.2.0"
|
||||
font-kit.workspace = true
|
||||
foreign-types = "0.5.0"
|
||||
libc.workspace = true
|
||||
metal = "0.33.0"
|
||||
objc.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dev-dependencies]
|
||||
rand.workspace = true
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
async-io.workspace = true
|
||||
ctrlc.workspace = true
|
||||
font-kit.workspace = true
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
bimap = "0.6.2"
|
||||
bytemuck.workspace = true
|
||||
command.workspace = true
|
||||
cosmic-text = { git = "https://github.com/warpdotdev/cosmic-text.git", rev = "15198beba692162201c0ea8b15222cf5643ea068" }
|
||||
derivative.workspace = true
|
||||
fontdb = "0.23.0"
|
||||
memmap2 = "0.9.3"
|
||||
owned_ttf_parser = "0.25.0"
|
||||
resvg.workspace = true
|
||||
serde.workspace = true
|
||||
version-compare.workspace = true
|
||||
winit.workspace = true
|
||||
wgpu.workspace = true
|
||||
|
||||
[target.'cfg(not(any(target_os = "macos", target_family = "wasm")))'.dependencies]
|
||||
global-hotkey = "0.7.0"
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
gloo.workspace = true
|
||||
js-sys = "0.3.69"
|
||||
wasm-bindgen.workspace = true
|
||||
wasm-bindgen-futures.workspace = true
|
||||
web-sys.workspace = true
|
||||
|
||||
[target.'cfg(all(target_family = "wasm"))'.dependencies.wgpu]
|
||||
workspace = true
|
||||
features = ["fragile-send-sync-non-atomic-wasm", "webgl"]
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
|
||||
arboard = { workspace = true, features = [
|
||||
"wayland-data-control",
|
||||
"image-data",
|
||||
] }
|
||||
native-dialog = "0.7.0"
|
||||
open = "5.0.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
blocking.workspace = true
|
||||
dirs.workspace = true
|
||||
fontconfig = { version = "0.8.0", features = ["dlopen"] }
|
||||
# On Linux, use dlopen to locate fontconfig at runtime. Other platforms should not depend on fontconfig at all.
|
||||
font-kit = { workspace = true, default-features = false, features = [
|
||||
"source-fontconfig-dlopen",
|
||||
] }
|
||||
futures-lite.workspace = true
|
||||
nix = { workspace = true, features = ["user"] }
|
||||
notify-rust = "4.10.0"
|
||||
open = "5.0.0"
|
||||
tini = "1.3.0"
|
||||
urlencoding = "2.1.3"
|
||||
x11rb.workspace = true
|
||||
x11-dl = "2.21.0"
|
||||
zbus.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
virtual-fs.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
dwrote = { git = "https://github.com/warpdotdev/dwrote-rs", rev = "a71ce6c0136f7d0954a9a8b181b5b2d8ace5eb9c", default-features = false }
|
||||
winreg.workspace = true
|
||||
windows = { workspace = true, features = [
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_DirectWrite",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_Networking",
|
||||
"Win32_System_Com",
|
||||
"Win32_Networking_NetworkListManager",
|
||||
"Win32_System_DataExchange",
|
||||
"Win32_System_Memory",
|
||||
] }
|
||||
windows-core.workspace = true
|
||||
windows-version = "0.1.2"
|
||||
tauri-winrt-notification = "0.7.0"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.70.1"
|
||||
cc = { version = "1.0", features = ["parallel"] }
|
||||
cfg_aliases.workspace = true
|
||||
@@ -0,0 +1,186 @@
|
||||
// We can use `std::process:Command` here because this is invoked within a build script,
|
||||
// _not_ within the Warp binary (where it could cause a terminal to temporarily flash on
|
||||
// Windows).
|
||||
#![allow(clippy::disallowed_types)]
|
||||
|
||||
use std::{env, path::PathBuf, process::Command};
|
||||
|
||||
use cfg_aliases::cfg_aliases;
|
||||
|
||||
fn main() {
|
||||
cfg_aliases! {
|
||||
macos: { target_os = "macos" },
|
||||
// We use winit on all platforms other than mac, where we have a custom
|
||||
// AppKit-based platform implementation.
|
||||
winit: { not(macos) },
|
||||
// We use wgpu for rendering on all platforms where we use winit, but
|
||||
// we can also use it on macOS, if enabled.
|
||||
wgpu: { any(winit, feature = "experimental-wgpu-renderer") },
|
||||
native: { not(target_family = "wasm") },
|
||||
}
|
||||
|
||||
if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
|
||||
bindgen_shader_types();
|
||||
compile_metal_shaders();
|
||||
compile_objc_lib();
|
||||
}
|
||||
}
|
||||
|
||||
fn bindgen_shader_types() {
|
||||
let header_path = "src/platform/mac/rendering/metal/shaders/shader_types.h";
|
||||
println!("cargo:rerun-if-changed={header_path}");
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header(header_path)
|
||||
.allowlist_type("vector_float2")
|
||||
.allowlist_type("Uniforms")
|
||||
.allowlist_type("PerRectUniforms")
|
||||
.allowlist_type("PerGlyphUniforms")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
// Disable intrinsic headers that define types containing 16-bit floats (`_Float16`,
|
||||
// `__m512h`, etc.) via preprocessor directive. `bindgen` doesn't know how to process
|
||||
// these types and panics at compile time. The types aren't used by our shader headers,
|
||||
// so suppressing the declarations is safe.
|
||||
//
|
||||
// - Xcode 15+: avx512fp16intrin.h, avx512vlfp16intrin.h
|
||||
// - Xcode 26+ (clang 21): amxavx512intrin.h, avx10_2convertintrin.h,
|
||||
// avx10_2_512convertintrin.h
|
||||
//
|
||||
// TODO(charlespierce): Remove once https://github.com/rust-lang/rust-bindgen/issues/2500
|
||||
// is resolved.
|
||||
.clang_args([
|
||||
"-D__AVX512VLFP16INTRIN_H",
|
||||
"-D__AVX512FP16INTRIN_H",
|
||||
"-D__AMX_AVX512INTRIN_H",
|
||||
"-D__AVX10_2CONVERTINTRIN_H",
|
||||
"-D__AVX10_2_512CONVERTINTRIN_H",
|
||||
"-D__AVX10_2_512MINMAXINTRIN_H",
|
||||
"-D__AVX10_2_512NIINTRIN_H",
|
||||
"-D__AVX10_2_512SATCVTINTRIN_H",
|
||||
])
|
||||
.generate()
|
||||
.unwrap_or_else(|_| panic!("unable to generate bindings for {header_path}"));
|
||||
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
bindings
|
||||
.write_to_file(out_path.join("shader_types.rs"))
|
||||
.expect("Couldn't write shader type bindings!");
|
||||
}
|
||||
|
||||
fn compile_metal_shaders() {
|
||||
let header_path = "src/platform/mac/rendering/metal/shaders/shader_types.h";
|
||||
let metal_path = "src/platform/mac/rendering/metal/shaders/shaders.metal";
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
|
||||
let air_path = out_path.join("shaders.air");
|
||||
let air_path = air_path.to_str().unwrap();
|
||||
|
||||
let lib_path = out_path.join("shaders.metallib");
|
||||
let lib_path = lib_path.to_str().unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed={header_path}");
|
||||
println!("cargo:rerun-if-changed={metal_path}");
|
||||
|
||||
let mut compile_args = vec!["-sdk", "macosx", "metal", "-c", metal_path, "-o", air_path];
|
||||
if cfg!(feature = "enable-metal-frame-capture") {
|
||||
compile_args.push("-frecord-sources");
|
||||
compile_args.push("-gline-tables-only");
|
||||
}
|
||||
let result = Command::new("xcrun")
|
||||
.args(&compile_args)
|
||||
.output()
|
||||
.expect("error compiling metal shaders to .air");
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"error compiling metal shaders to .air; {}",
|
||||
std::str::from_utf8(&result.stderr).unwrap(),
|
||||
);
|
||||
|
||||
let result = Command::new("xcrun")
|
||||
.args(["-sdk", "macosx", "metallib", air_path, "-o", lib_path])
|
||||
.output()
|
||||
.expect("error compiling metal shaders to .metallib");
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"error compling metal shaders to .metallib; {}",
|
||||
std::str::from_utf8(&result.stderr).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
fn compile_objc_lib() {
|
||||
println!("cargo:rustc-link-lib=framework=UserNotifications");
|
||||
println!("cargo:rustc-link-lib=framework=Carbon");
|
||||
println!("cargo:rustc-link-lib=framework=SystemConfiguration");
|
||||
println!("cargo:rustc-link-lib=framework=UniformTypeIdentifiers");
|
||||
println!("cargo:rustc-link-lib=framework=AVFoundation");
|
||||
println!("cargo:rustc-link-lib=framework=ServiceManagement");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/app.h");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/app.m");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/keycode.m");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/host_view.m");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/host_view.h");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/hotkey.h");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/hotkey.m");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/menus.h");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/menus.m");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/notifications/notifications.h");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/notifications/notifications.m");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/window.m");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/window_blur.m");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/window_blur.h");
|
||||
// Referenced from https://github.com/tonymillion/Reachability
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/reachability.h");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/reachability.m");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/alert.m");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/alert.h");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/fullscreen_queue.h");
|
||||
println!("cargo:rerun-if-changed=src/platform/mac/objc/fullscreen_queue.m");
|
||||
|
||||
// Link against the clang_rt library so that the @available keyword
|
||||
// doesn't produce linker errors.
|
||||
//
|
||||
// See: https://github.com/alexcrichton/curl-rust/issues/279
|
||||
if let Some(path) = macos_link_search_path() {
|
||||
println!("cargo:rustc-link-lib=clang_rt.osx");
|
||||
println!("cargo:rustc-link-search={path}");
|
||||
}
|
||||
|
||||
cc::Build::new()
|
||||
.file("src/platform/mac/objc/app.m")
|
||||
.file("src/platform/mac/objc/host_view.m")
|
||||
.file("src/platform/mac/objc/hotkey.m")
|
||||
.file("src/platform/mac/objc/reachability.m")
|
||||
.file("src/platform/mac/objc/keycode.m")
|
||||
.file("src/platform/mac/objc/menus.m")
|
||||
.file("src/platform/mac/objc/notifications/notifications.m")
|
||||
.file("src/platform/mac/objc/window.m")
|
||||
.file("src/platform/mac/objc/fullscreen_queue.m")
|
||||
.file("src/platform/mac/objc/window_blur.m")
|
||||
.file("src/platform/mac/objc/alert.m")
|
||||
.compile("warp_objc");
|
||||
}
|
||||
|
||||
/// Determine the path containing the macOS standard libraries by querying
|
||||
/// clang's library search paths.
|
||||
fn macos_link_search_path() -> Option<String> {
|
||||
let output = Command::new("clang")
|
||||
.arg("--print-search-dirs")
|
||||
.output()
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
println!(
|
||||
"failed to run 'clang --print-search-dirs', continuing without a link search path"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
if line.contains("libraries: =") {
|
||||
let path = line.split('=').nth(1)?;
|
||||
return Some(format!("{path}/lib/darwin"));
|
||||
}
|
||||
}
|
||||
|
||||
println!("failed to determine link search path, continuing without it");
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {path}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(|ctx| {
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use galaxyui::color::ColorU;
|
||||
use galaxyui::elements::shimmering_text::{
|
||||
ShimmerConfig, ShimmeringTextElement, ShimmeringTextStateHandle,
|
||||
};
|
||||
use galaxyui::elements::{Align, ConstrainedBox, ParentElement, Rect, Stack};
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::SingletonEntity as _;
|
||||
use galaxyui::{AppContext, Element, Entity, TypedActionView, View, ViewContext};
|
||||
|
||||
pub struct RootView {
|
||||
text: String,
|
||||
font_family: FamilyId,
|
||||
font_size: f32,
|
||||
start: ColorU,
|
||||
end: ColorU,
|
||||
config: ShimmerConfig,
|
||||
|
||||
// Persist the animation/layout state across renders.
|
||||
shimmering_text_handle: ShimmeringTextStateHandle,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let font_family = galaxyui::fonts::Cache::handle(ctx).update(ctx, |cache, _| {
|
||||
cache
|
||||
.load_system_font("Times")
|
||||
.or_else(|_| cache.load_system_font("Arial"))
|
||||
.expect("Should load a system font")
|
||||
});
|
||||
|
||||
// Treat start/end as the dim → bright endpoints.
|
||||
let start = ColorU::new(160, 160, 160, 255);
|
||||
let end = ColorU::new(255, 255, 255, 255);
|
||||
|
||||
Self {
|
||||
text: "Warp shimmer: 👩💻with ligatures — fi fl 🇺🇸".to_string(),
|
||||
font_family,
|
||||
font_size: 28.0,
|
||||
start,
|
||||
end,
|
||||
config: ShimmerConfig::default(),
|
||||
shimmering_text_handle: ShimmeringTextStateHandle::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"AnimatedGradientText"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
let shimmering = ShimmeringTextElement::new(
|
||||
self.text.clone(),
|
||||
self.font_family,
|
||||
self.font_size,
|
||||
self.start,
|
||||
self.end,
|
||||
self.config,
|
||||
self.shimmering_text_handle.clone(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
Stack::new()
|
||||
.with_child(Rect::new().with_background_color(ColorU::black()).finish())
|
||||
.with_child(
|
||||
Align::new(
|
||||
ConstrainedBox::new(shimmering)
|
||||
.with_max_width(900.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use root_view::RootView;
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(galaxyui::AddWindowOptions::default(), |_| RootView::new());
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use instant::Instant;
|
||||
use galaxyui::{
|
||||
assets::asset_cache::AssetSource,
|
||||
elements::{
|
||||
CacheOption, ConstrainedBox, CrossAxisAlignment, Flex, Image, ParentElement, Shrinkable,
|
||||
Stack,
|
||||
},
|
||||
AppContext, Element, Entity, TypedActionView, View,
|
||||
};
|
||||
|
||||
pub struct RootView {
|
||||
animation_start_time: Instant,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new() -> Self {
|
||||
println!(
|
||||
"WARN: This example is slow to start up due to the huge GIF. Compiling with --release \
|
||||
helps."
|
||||
);
|
||||
RootView {
|
||||
animation_start_time: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RootView {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Image::new(
|
||||
AssetSource::Bundled {
|
||||
path: "rustyrain.gif",
|
||||
},
|
||||
CacheOption::Original,
|
||||
)
|
||||
.enable_animation_with_start_time(self.animation_start_time)
|
||||
.cover()
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(
|
||||
ConstrainedBox::new(
|
||||
Image::new(
|
||||
AssetSource::Bundled {
|
||||
path: "numbers-1000ms.gif",
|
||||
},
|
||||
CacheOption::BySize,
|
||||
)
|
||||
.enable_animation_with_start_time(self.animation_start_time)
|
||||
.finish(),
|
||||
)
|
||||
.with_height(350.)
|
||||
.with_width(350.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
ConstrainedBox::new(
|
||||
Image::new(
|
||||
AssetSource::Bundled {
|
||||
path: "numbers-750ms.gif",
|
||||
},
|
||||
CacheOption::BySize,
|
||||
)
|
||||
.enable_animation_with_start_time(self.animation_start_time)
|
||||
.finish(),
|
||||
)
|
||||
.with_height(350.)
|
||||
.with_width(350.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
This directory is used for any static assets we want to load in the app (e.g. svg icons, images, fonts, etc.)
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.477 4.0729C2.806 4.2489 2.238 4.8229 2.06 5.5079C1.941 5.9619 2.003 6.4919 2.225 6.9299C2.361 7.1989 2.801 7.6389 3.07 7.7749C3.659 8.0729 4.341 8.0729 4.93 7.7749C5.196 7.6399 5.638 7.1999 5.771 6.9369C5.994 6.4979 6.052 6.0069 5.94 5.5289C5.858 5.1819 5.72 4.9269 5.465 4.6529C4.948 4.0969 4.211 3.8819 3.477 4.0729ZM8.614 5.0569C8.377 5.1309 8.212 5.2609 8.109 5.4529C8.035 5.5919 8.02 5.6839 8.02 5.9999C8.02 6.3309 8.033 6.4029 8.12 6.5579C8.23 6.7549 8.414 6.8979 8.635 6.9589C8.834 7.0149 21.166 7.0149 21.365 6.9589C21.586 6.8979 21.77 6.7549 21.88 6.5579C21.967 6.4019 21.98 6.3319 21.979 5.9999C21.978 5.5669 21.898 5.3549 21.678 5.1989C21.388 4.9919 21.642 4.9999 14.984 5.0029C10.009 5.0049 8.747 5.0159 8.614 5.0569ZM3.477 10.0729C3.13 10.1639 2.802 10.3659 2.535 10.6529C2.178 11.0379 2 11.4849 2 11.9999C2 12.5459 2.193 13.0009 2.596 13.4039C2.741 13.5499 2.955 13.7159 3.07 13.7749C3.659 14.0729 4.341 14.0729 4.93 13.7749C5.196 13.6399 5.638 13.1999 5.771 12.9369C6.161 12.1679 6.043 11.2749 5.471 10.6589C5.074 10.2319 4.563 10.0049 4 10.0049C3.857 10.0049 3.622 10.0359 3.477 10.0729ZM8.614 11.0569C8.377 11.1309 8.212 11.2609 8.109 11.4529C8.035 11.5919 8.02 11.6839 8.02 11.9999C8.02 12.3309 8.033 12.4029 8.12 12.5579C8.23 12.7549 8.414 12.8979 8.635 12.9589C8.834 13.0149 21.166 13.0149 21.365 12.9589C21.586 12.8979 21.77 12.7549 21.88 12.5579C21.967 12.4019 21.98 12.3319 21.979 11.9999C21.978 11.5669 21.898 11.3549 21.678 11.1989C21.388 10.9919 21.642 10.9999 14.984 11.0029C10.009 11.0049 8.747 11.0159 8.614 11.0569ZM3.477 16.0729C3.13 16.1639 2.802 16.3659 2.535 16.6529C2.178 17.0379 2 17.4849 2 17.9999C2 18.5459 2.193 19.0009 2.596 19.4039C2.999 19.8069 3.454 19.9999 4 19.9999C4.546 19.9999 5.001 19.8069 5.404 19.4039C5.92 18.8889 6.106 18.2359 5.94 17.5289C5.858 17.1819 5.72 16.9269 5.465 16.6529C5.074 16.2319 4.56 16.0049 4 16.0049C3.857 16.0049 3.622 16.0359 3.477 16.0729ZM8.614 17.0569C8.377 17.1309 8.212 17.2609 8.109 17.4529C8.035 17.5919 8.02 17.6839 8.02 17.9999C8.02 18.3309 8.033 18.4029 8.12 18.5579C8.23 18.7549 8.414 18.8979 8.635 18.9589C8.834 19.0149 21.166 19.0149 21.365 18.9589C21.586 18.8979 21.77 18.7549 21.88 18.5579C21.967 18.4019 21.98 18.3319 21.979 17.9999C21.978 17.5669 21.898 17.3549 21.678 17.1989C21.388 16.9919 21.642 16.9999 14.984 17.0029C10.009 17.0049 8.747 17.0159 8.614 17.0569Z" fill="#FF0000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.536 1.03894C10.781 1.14594 9.98205 1.60094 9.51205 2.19394C9.25205 2.52094 9.15305 2.70094 8.80005 3.49994C8.34005 4.53894 8.19505 4.73894 7.76005 4.93494C7.42605 5.08594 7.25305 5.09394 6.40005 4.99894C5.14705 4.85894 4.63305 4.89094 4.03005 5.14394C3.08205 5.54294 2.42005 6.32094 2.12705 7.37994C2.03405 7.71594 2.04005 8.52794 2.13705 8.87994C2.26405 9.33694 2.53705 9.83294 3.04105 10.5199C3.72605 11.4549 3.74005 11.4839 3.74005 11.9999C3.74005 12.5149 3.72705 12.5429 3.04305 13.4799C2.24405 14.5739 2.06705 15.0069 2.06305 15.8799C2.06005 16.5789 2.20005 17.0639 2.56705 17.6279C3.02105 18.3239 3.75005 18.8409 4.52705 19.0169C5.02104 19.1289 5.26505 19.1289 6.26105 19.0219C7.30105 18.9099 7.48005 18.9169 7.80305 19.0819C7.89405 19.1279 8.05005 19.2409 8.15105 19.3329C8.33905 19.5049 8.45505 19.7219 8.96305 20.8579C9.31305 21.6389 9.66205 22.0809 10.227 22.4529C11.424 23.2419 12.902 23.1679 14.049 22.2619C14.513 21.8949 14.801 21.4259 15.402 20.0599C15.659 19.4769 15.913 19.1879 16.298 19.0439C16.602 18.9299 16.831 18.9299 17.88 19.0409C18.625 19.1209 18.789 19.1269 19.105 19.0869C21.04 18.8449 22.307 17.0199 21.855 15.1259C21.754 14.6989 21.474 14.1879 20.957 13.4799C20.28 12.5539 20.26 12.5129 20.26 12.0199C20.26 11.5269 20.313 11.4069 20.846 10.6799C21.775 9.41494 21.931 9.04894 21.936 8.11994C21.939 7.59094 21.893 7.34194 21.704 6.87394C21.388 6.09294 20.705 5.43194 19.88 5.10894C19.304 4.88394 18.833 4.86094 17.6 4.99894C17.169 5.04694 16.739 5.07394 16.64 5.06094C16.357 5.02194 15.988 4.82194 15.788 4.60094C15.671 4.47094 15.54 4.24494 15.4 3.93194C14.768 2.51594 14.545 2.14894 14.1 1.78894C13.667 1.43794 13.219 1.20794 12.752 1.09594C12.452 1.02494 11.842 0.995943 11.536 1.03894ZM12.346 3.05994C12.598 3.13394 12.848 3.31294 12.98 3.51294C13.036 3.59694 13.243 4.02294 13.44 4.45994C13.87 5.41094 13.982 5.60494 14.316 5.96694C14.904 6.60494 15.691 6.98494 16.583 7.05994C16.803 7.07894 17.144 7.05994 17.7 6.99894C18.704 6.88894 18.963 6.88894 19.209 6.99494C19.47 7.10794 19.733 7.36294 19.844 7.61094C19.957 7.86394 19.97 8.33394 19.869 8.56094C19.835 8.63794 19.571 9.02494 19.282 9.42094C18.461 10.5469 18.236 11.1019 18.237 11.9969C18.237 12.4909 18.305 12.8679 18.463 13.2639C18.614 13.6409 18.719 13.8099 19.282 14.5819C19.869 15.3889 19.935 15.5219 19.935 15.8999C19.935 16.4919 19.553 16.9559 18.961 17.0829C18.801 17.1179 18.6 17.1089 17.97 17.0399C16.523 16.8819 16.051 16.9319 15.27 17.3249C14.938 17.4919 14.705 17.6539 14.445 17.8979C14.037 18.2819 13.855 18.5949 13.237 19.9799C13.076 20.3399 12.976 20.5059 12.839 20.6439C12.423 21.0609 11.77 21.1099 11.287 20.7609C11.061 20.5969 10.969 20.4459 10.618 19.6589C10.088 18.4729 9.97005 18.2819 9.49005 17.8369C8.95805 17.3449 8.26305 17.0339 7.52405 16.9589C7.19905 16.9249 6.99105 16.9339 6.19505 17.0179C5.67705 17.0719 5.18205 17.1059 5.09605 17.0929C4.69405 17.0319 4.31704 16.7419 4.14804 16.3649C4.04104 16.1249 4.03404 15.7279 4.13205 15.4739C4.17204 15.3709 4.43705 14.9659 4.72305 14.5739C5.56105 13.4209 5.76604 12.9159 5.76604 11.9999C5.76604 11.0839 5.56105 10.5789 4.72305 9.42594C4.43705 9.03394 4.17204 8.62894 4.13205 8.52594C3.91205 7.95594 4.18705 7.28294 4.74905 7.01694C5.03005 6.88494 5.23105 6.88194 6.30005 6.99894C7.18105 7.09594 7.52205 7.08694 8.03705 6.95594C8.95404 6.72194 9.78204 6.06894 10.197 5.25294C10.262 5.12494 10.451 4.71494 10.618 4.34094C10.961 3.57394 11.056 3.41294 11.262 3.25594C11.568 3.02194 11.974 2.94894 12.346 3.05994ZM11.358 8.06094C10.23 8.23694 9.14305 8.97394 8.56905 9.95294C7.32005 12.0829 8.23304 14.7849 10.512 15.7039C12.558 16.5279 14.877 15.5379 15.704 13.4879C16.09 12.5299 16.09 11.4689 15.705 10.5159C15.172 9.19694 14.001 8.26894 12.605 8.05794C12.144 7.98894 11.816 7.98994 11.358 8.06094ZM12.523 10.0729C13.262 10.2659 13.85 10.9239 13.967 11.6899C14.067 12.3429 13.873 12.9329 13.403 13.4039C13.002 13.8069 12.547 13.9999 12 13.9999C11.485 13.9999 11.038 13.8219 10.653 13.4649C9.32304 12.2289 10.189 10.0049 12 10.0049C12.143 10.0049 12.378 10.0359 12.523 10.0729Z" fill="#FF0000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.33334 2.66659C4.75601 2.66659 2.66668 4.75592 2.66668 7.33325C2.66668 9.91058 4.75601 11.9999 7.33334 11.9999C9.91067 11.9999 12 9.91058 12 7.33325C12 4.75592 9.91067 2.66659 7.33334 2.66659ZM1.33334 7.33325C1.33334 4.01954 4.01963 1.33325 7.33334 1.33325C10.6471 1.33325 13.3333 4.01954 13.3333 7.33325C13.3333 10.647 10.6471 13.3333 7.33334 13.3333C4.01963 13.3333 1.33334 10.647 1.33334 7.33325Z" fill="#ffffff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.6286 10.6286C10.8889 10.3683 11.311 10.3683 11.5714 10.6286L14.4714 13.5286C14.7317 13.789 14.7317 14.2111 14.4714 14.4714C14.211 14.7318 13.7889 14.7318 13.5286 14.4714L10.6286 11.5714C10.3682 11.3111 10.3682 10.889 10.6286 10.6286Z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 885 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5774 0.364039C5.40473 0.486706 4.3334 1.22337 3.75073 2.30871C3.28207 3.18137 3.2114 4.3047 3.56407 5.27137C3.93607 6.29137 4.79873 7.12537 5.84407 7.47537C7.1234 7.90404 8.54807 7.58537 9.5174 6.65404C10.1961 6.00204 10.5781 5.20604 10.6507 4.28937C10.7387 3.18204 10.3401 2.13871 9.53073 1.35937C8.7414 0.598706 7.69673 0.246706 6.5774 0.364039ZM7.60073 1.74471C8.22407 1.90537 8.78207 2.36337 9.08073 2.96004C9.27607 3.35004 9.3074 3.49137 9.3074 4.00004C9.3074 4.50871 9.27607 4.65071 9.08073 5.04004C8.78673 5.62671 8.28273 6.05137 7.65407 6.24271C7.43673 6.30937 7.3754 6.31604 7.00073 6.31604C6.62673 6.31604 6.56473 6.30937 6.3474 6.24337C5.56607 6.00604 4.9574 5.38271 4.7414 4.59804C4.7054 4.46737 4.69407 4.32604 4.69407 4.00004C4.69407 3.53871 4.72207 3.38071 4.86007 3.06871C5.11673 2.48604 5.66807 1.97804 6.25007 1.78737C6.6514 1.65604 7.1894 1.63871 7.60073 1.74471ZM6.33407 9.02804C4.2194 9.21204 2.33607 10.088 0.786734 11.6094C0.386734 12.002 0.302734 12.13 0.302734 12.348C0.302734 12.6614 0.687401 13.0314 1.01273 13.0314C1.19407 13.0314 1.33407 12.9387 1.72073 12.5627C2.9614 11.356 4.33073 10.6627 5.97407 10.4087C6.2814 10.3607 6.4634 10.3507 7.00073 10.3507C7.53807 10.3507 7.72007 10.3607 8.0274 10.4087C9.67473 10.6634 11.0294 11.3507 12.2941 12.574C12.6907 12.9574 12.8021 13.0314 12.9874 13.0314C13.3141 13.0314 13.6987 12.662 13.6987 12.348C13.6987 12.1 13.5527 11.9054 12.9607 11.3654C11.5514 10.0794 9.8454 9.28537 8.01407 9.0647C7.6014 9.0147 6.70807 8.99537 6.33407 9.02804Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5774 0.364039C5.40473 0.486706 4.3334 1.22337 3.75073 2.30871C3.28207 3.18137 3.2114 4.3047 3.56407 5.27137C3.93607 6.29137 4.79873 7.12537 5.84407 7.47537C7.1234 7.90404 8.54807 7.58537 9.5174 6.65404C10.1961 6.00204 10.5781 5.20604 10.6507 4.28937C10.7387 3.18204 10.3401 2.13871 9.53073 1.35937C8.7414 0.598706 7.69673 0.246706 6.5774 0.364039ZM7.60073 1.74471C8.22407 1.90537 8.78207 2.36337 9.08073 2.96004C9.27607 3.35004 9.3074 3.49137 9.3074 4.00004C9.3074 4.50871 9.27607 4.65071 9.08073 5.04004C8.78673 5.62671 8.28273 6.05137 7.65407 6.24271C7.43673 6.30937 7.3754 6.31604 7.00073 6.31604C6.62673 6.31604 6.56473 6.30937 6.3474 6.24337C5.56607 6.00604 4.9574 5.38271 4.7414 4.59804C4.7054 4.46737 4.69407 4.32604 4.69407 4.00004C4.69407 3.53871 4.72207 3.38071 4.86007 3.06871C5.11673 2.48604 5.66807 1.97804 6.25007 1.78737C6.6514 1.65604 7.1894 1.63871 7.60073 1.74471ZM6.33407 9.02804C4.2194 9.21204 2.33607 10.088 0.786734 11.6094C0.386734 12.002 0.302734 12.13 0.302734 12.348C0.302734 12.6614 0.687401 13.0314 1.01273 13.0314C1.19407 13.0314 1.33407 12.9387 1.72073 12.5627C2.9614 11.356 4.33073 10.6627 5.97407 10.4087C6.2814 10.3607 6.4634 10.3507 7.00073 10.3507C7.53807 10.3507 7.72007 10.3607 8.0274 10.4087C9.67473 10.6634 11.0294 11.3507 12.2941 12.574C12.6907 12.9574 12.8021 13.0314 12.9874 13.0314C13.3141 13.0314 13.6987 12.662 13.6987 12.348C13.6987 12.1 13.5527 11.9054 12.9607 11.3654C11.5514 10.0794 9.8454 9.28537 8.01407 9.0647C7.6014 9.0147 6.70807 8.99537 6.33407 9.02804Z" fill="white" fill-opacity="0.6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5774 0.364039C5.40473 0.486706 4.3334 1.22337 3.75073 2.30871C3.28207 3.18137 3.2114 4.3047 3.56407 5.27137C3.93607 6.29137 4.79873 7.12537 5.84407 7.47537C7.1234 7.90404 8.54807 7.58537 9.5174 6.65404C10.1961 6.00204 10.5781 5.20604 10.6507 4.28937C10.7387 3.18204 10.3401 2.13871 9.53073 1.35937C8.7414 0.598706 7.69673 0.246706 6.5774 0.364039ZM7.60073 1.74471C8.22407 1.90537 8.78207 2.36337 9.08073 2.96004C9.27607 3.35004 9.3074 3.49137 9.3074 4.00004C9.3074 4.50871 9.27607 4.65071 9.08073 5.04004C8.78673 5.62671 8.28273 6.05137 7.65407 6.24271C7.43673 6.30937 7.3754 6.31604 7.00073 6.31604C6.62673 6.31604 6.56473 6.30937 6.3474 6.24337C5.56607 6.00604 4.9574 5.38271 4.7414 4.59804C4.7054 4.46737 4.69407 4.32604 4.69407 4.00004C4.69407 3.53871 4.72207 3.38071 4.86007 3.06871C5.11673 2.48604 5.66807 1.97804 6.25007 1.78737C6.6514 1.65604 7.1894 1.63871 7.60073 1.74471ZM6.33407 9.02804C4.2194 9.21204 2.33607 10.088 0.786734 11.6094C0.386734 12.002 0.302734 12.13 0.302734 12.348C0.302734 12.6614 0.687401 13.0314 1.01273 13.0314C1.19407 13.0314 1.33407 12.9387 1.72073 12.5627C2.9614 11.356 4.33073 10.6627 5.97407 10.4087C6.2814 10.3607 6.4634 10.3507 7.00073 10.3507C7.53807 10.3507 7.72007 10.3607 8.0274 10.4087C9.67473 10.6634 11.0294 11.3507 12.2941 12.574C12.6907 12.9574 12.8021 13.0314 12.9874 13.0314C13.3141 13.0314 13.6987 12.662 13.6987 12.348C13.6987 12.1 13.5527 11.9054 12.9607 11.3654C11.5514 10.0794 9.8454 9.28537 8.01407 9.0647C7.6014 9.0147 6.70807 8.99537 6.33407 9.02804Z" fill="black" fill-opacity="0.2"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 17 MiB |
@@ -0,0 +1,37 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
root_view::init(ctx);
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use galaxyui::elements::DispatchEventResult;
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, Border, ChildView, Container, CornerRadius, EventHandler, Flex, ParentElement,
|
||||
Radius, Rect, Stack, Text,
|
||||
},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity, Tracked, TypedActionView, View,
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
pub fn init(ctx: &mut AppContext) {
|
||||
ctx.add_singleton_model(|_| Settings {
|
||||
dark_mode: Tracked::new(false),
|
||||
});
|
||||
}
|
||||
|
||||
pub struct RootView {
|
||||
main: ViewHandle<MainView>,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let main = ctx.add_typed_action_view(MainView::new);
|
||||
|
||||
RootView { main }
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let dark_mode = *Settings::as_ref(app).dark_mode;
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Rect::new()
|
||||
.with_background_color(if dark_mode {
|
||||
ColorU::black()
|
||||
} else {
|
||||
ColorU::white()
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.with_child(ChildView::new(&self.main).finish())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
dark_mode: Tracked<bool>,
|
||||
}
|
||||
|
||||
impl Entity for Settings {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl SingletonEntity for Settings {}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Counter {
|
||||
value: Tracked<isize>,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
fn increment(&mut self) {
|
||||
*self.value += 1;
|
||||
}
|
||||
|
||||
fn decrement(&mut self) {
|
||||
*self.value -= 1;
|
||||
}
|
||||
|
||||
fn value(&self) -> isize {
|
||||
*self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Counter {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
struct MainView {
|
||||
model: ModelHandle<Counter>,
|
||||
stored: Tracked<Option<isize>>,
|
||||
font_family: FamilyId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum MainViewAction {
|
||||
Increment,
|
||||
Decrement,
|
||||
Save,
|
||||
Restore,
|
||||
ToggleDarkMode,
|
||||
}
|
||||
|
||||
impl MainView {
|
||||
fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let model = ctx.add_model(|_| Counter::default());
|
||||
let font_family = galaxyui::fonts::Cache::handle(ctx)
|
||||
.update(ctx, |cache, _| cache.load_system_font("Arial").unwrap());
|
||||
MainView {
|
||||
model,
|
||||
stored: Tracked::new(None),
|
||||
font_family,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for MainView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl TypedActionView for MainView {
|
||||
type Action = MainViewAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
use MainViewAction::*;
|
||||
|
||||
match action {
|
||||
Increment => self.model.update(ctx, |model, _| model.increment()),
|
||||
Decrement => self.model.update(ctx, |model, _| model.decrement()),
|
||||
Save => {
|
||||
let current = self.model.read(ctx, |model, _| model.value());
|
||||
*self.stored = Some(current);
|
||||
}
|
||||
Restore => {
|
||||
if let Some(stored) = self.stored.take() {
|
||||
self.model.update(ctx, |model, _| *model.value = stored);
|
||||
}
|
||||
}
|
||||
ToggleDarkMode => {
|
||||
Settings::handle(ctx).update(ctx, |settings, _| {
|
||||
*settings.dark_mode = !*settings.dark_mode;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View for MainView {
|
||||
fn ui_name() -> &'static str {
|
||||
"MainView"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let dark_mode = *Settings::as_ref(app).dark_mode;
|
||||
let text_color = if dark_mode {
|
||||
ColorU::white()
|
||||
} else {
|
||||
ColorU::black()
|
||||
};
|
||||
let counter = self.model.as_ref(app).value();
|
||||
|
||||
Align::new(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Align::new(
|
||||
Flex::row()
|
||||
.with_child(render_button(
|
||||
Text::new_inline("-", self.font_family, 16.)
|
||||
.with_color(text_color)
|
||||
.finish(),
|
||||
MainViewAction::Decrement,
|
||||
))
|
||||
.with_child(
|
||||
Text::new_inline(format!("{counter}"), self.font_family, 16.)
|
||||
.with_color(text_color)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(render_button(
|
||||
Text::new_inline("+", self.font_family, 16.)
|
||||
.with_color(text_color)
|
||||
.finish(),
|
||||
MainViewAction::Increment,
|
||||
))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Align::new(
|
||||
Flex::row()
|
||||
.with_child(render_button(
|
||||
Text::new_inline("Toggle Dark Mode", self.font_family, 16.)
|
||||
.with_color(text_color)
|
||||
.finish(),
|
||||
MainViewAction::ToggleDarkMode,
|
||||
))
|
||||
.with_child(if self.stored.is_some() {
|
||||
render_button(
|
||||
Text::new_inline("Restore", self.font_family, 16.)
|
||||
.with_color(text_color)
|
||||
.finish(),
|
||||
MainViewAction::Restore,
|
||||
)
|
||||
} else {
|
||||
render_button(
|
||||
Text::new_inline("Save Value", self.font_family, 16.)
|
||||
.with_color(text_color)
|
||||
.finish(),
|
||||
MainViewAction::Save,
|
||||
)
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_button(inner: Box<dyn Element>, action: MainViewAction) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
EventHandler::new(
|
||||
Container::new(inner)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
|
||||
.with_border(Border::all(1.).with_border_color(ColorU::new(128, 128, 128, 255)))
|
||||
.with_uniform_padding(4.)
|
||||
.finish(),
|
||||
)
|
||||
.on_left_mouse_down(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(action);
|
||||
DispatchEventResult::StopPropagation
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.with_uniform_margin(4.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions {
|
||||
// Set the window background blur radius to 18 pixels.
|
||||
background_blur_radius_pixels: Some(18),
|
||||
..Default::default()
|
||||
},
|
||||
|_| root_view::BlurredView {},
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use galaxyui::{elements::Rect, AppContext, Element, Entity, TypedActionView, View};
|
||||
|
||||
pub struct BlurredView {}
|
||||
|
||||
impl Entity for BlurredView {
|
||||
type Event = ();
|
||||
}
|
||||
impl View for BlurredView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
/// Renders a transparent red rectangle. The blur effect is applied on the window (see
|
||||
/// `open_new()`.
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(255, 0, 0, 50))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for BlurredView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[allow(unused_mut)]
|
||||
let mut app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(galaxyui::AddWindowOptions::default(), |_| {
|
||||
root_view::RootView::default()
|
||||
});
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
use std::any::Any;
|
||||
|
||||
use pathfinder_color::ColorU;
|
||||
use pathfinder_geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
};
|
||||
use galaxyui::elements::{AcceptedByDropTarget, DropTarget, DropTargetData};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, ConstrainedBox, Container, DragAxis, Draggable, DraggableState, ParentElement, Rect,
|
||||
Stack,
|
||||
},
|
||||
AppContext, Element, Entity, TypedActionView, View,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RootView {
|
||||
basic_draggable_state: DraggableState,
|
||||
horizontal_draggable_state: DraggableState,
|
||||
vertical_draggable_state: DraggableState,
|
||||
clamped_draggable_state: DraggableState,
|
||||
}
|
||||
|
||||
// Implement the entity trait.
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
// Implement the view trait so RootView could be considered as a view.
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
// Let's render a simple black rect background.
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
Stack::new()
|
||||
.with_child(Rect::new().with_background_color(ColorU::black()).finish())
|
||||
.with_child(
|
||||
Align::new(
|
||||
Draggable::new(
|
||||
self.basic_draggable_state.clone(),
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(255, 0, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(50.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.on_drag_start(|_, _, _| eprintln!("Regular Drag Start!"))
|
||||
.on_drop(|_, _, _, drop_data| eprintln!("Regular Drop! Data: {drop_data:?}"))
|
||||
.with_accepted_by_drop_target_fn(|_, _| AcceptedByDropTarget::Yes)
|
||||
.with_drag_bounds_callback(|_, window_size| {
|
||||
Some(RectF::new(Vector2F::zero(), window_size))
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Align::new(
|
||||
Container::new({
|
||||
let draggable = Draggable::new(
|
||||
self.horizontal_draggable_state.clone(),
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(255, 0, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(100.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_drag_axis(DragAxis::HorizontalOnly)
|
||||
.on_drag_start(|_, _, _| eprintln!("Horizontal Drag Start!"))
|
||||
.on_drop(|_, _, _, drop_data| {
|
||||
eprintln!("Horizontal Drop! Drop data: {drop_data:?}")
|
||||
})
|
||||
.with_accepted_by_drop_target_fn(|_, _| AcceptedByDropTarget::Yes)
|
||||
.finish();
|
||||
DropTarget::new(draggable, DropTargetColor::Magenta).finish()
|
||||
})
|
||||
.with_margin_top(20.)
|
||||
.finish(),
|
||||
)
|
||||
.top_center()
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Align::new(
|
||||
Container::new(
|
||||
Draggable::new(
|
||||
self.vertical_draggable_state.clone(),
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(0, 255, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(50.)
|
||||
.with_height(100.)
|
||||
.finish(),
|
||||
)
|
||||
.with_drag_axis(DragAxis::VerticalOnly)
|
||||
.on_drag_start(|_, _, _| eprintln!("Vertical Drag Start!"))
|
||||
.on_drop(|_, _, _, _| eprintln!("Vertical Drop!"))
|
||||
.with_accepted_by_drop_target_fn(|_, _| AcceptedByDropTarget::Yes)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_right(20.)
|
||||
.finish(),
|
||||
)
|
||||
.right()
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Align::new(
|
||||
Container::new({
|
||||
let draggable = Draggable::new(
|
||||
self.clamped_draggable_state.clone(),
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(255, 255, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(50.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_drag_bounds(RectF::new(vec2f(20., 30.), vec2f(200., 400.)))
|
||||
.on_drag_start(|_, _, _| eprintln!("Clamped Drag Start!"))
|
||||
.on_drop(|_, _, _, drop_target_data| {
|
||||
eprintln!("Clamped Drop! Data: {drop_target_data:?}")
|
||||
})
|
||||
.with_accepted_by_drop_target_fn(|_, _| AcceptedByDropTarget::Yes)
|
||||
.finish();
|
||||
|
||||
DropTarget::new(draggable, DropTargetColor::Yellow).finish()
|
||||
})
|
||||
.with_margin_left(50.)
|
||||
.with_margin_top(50.)
|
||||
.finish(),
|
||||
)
|
||||
.top_left()
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Align::new(
|
||||
DropTarget::new(
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(0, 0, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(100.)
|
||||
.with_height(100.)
|
||||
.finish(),
|
||||
DropTargetColor::Blue,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.bottom_center()
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DropTargetColor {
|
||||
Yellow,
|
||||
Blue,
|
||||
Magenta,
|
||||
}
|
||||
|
||||
impl DropTargetData for DropTargetColor {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(galaxyui::AddWindowOptions::default(), |_| {
|
||||
root_view::RootView {}
|
||||
});
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use galaxyui::{elements::Rect, AppContext, Element, Entity, TypedActionView, View};
|
||||
|
||||
pub struct RootView {}
|
||||
|
||||
// Implement the entity trait.
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
// Implement the view trait so RootView could be considered as a view.
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
// Let's render a simple black rect background.
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
Rect::new().with_background_color(ColorU::black()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
use galaxyui::elements::{Expanded, Shrinkable};
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::SingletonEntity as _;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ConstrainedBox, Container, Flex, MainAxisAlignment, MainAxisSize, ParentElement,
|
||||
Rect, Stack, Text,
|
||||
},
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
use galaxyui::color::ColorU;
|
||||
|
||||
pub struct RootView {
|
||||
font_family: FamilyId,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let font_family = galaxyui::fonts::Cache::handle(ctx)
|
||||
.update(ctx, |cache, _| cache.load_system_font("Arial").unwrap());
|
||||
RootView { font_family }
|
||||
}
|
||||
|
||||
fn make_label(&self, label: String) -> Box<dyn Element> {
|
||||
Flex::row()
|
||||
.with_child(Text::new_inline(label, self.font_family, 16.).finish())
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn make_expanded_row(&self) -> Flex {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Fixed 100", self.font_family, 16.).finish(),
|
||||
)
|
||||
.with_width(100.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(255, 0, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Expanded::new(
|
||||
1.0,
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Max Width 500, Min 200", self.font_family, 16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_max_width(500.)
|
||||
.with_min_width(200.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 150, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Fixed 100", self.font_family, 16.).finish(),
|
||||
)
|
||||
.with_width(100.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 0, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
}
|
||||
|
||||
fn make_shrinkable_row(&self) -> Flex {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Fixed 100", self.font_family, 16.).finish(),
|
||||
)
|
||||
.with_width(100.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(255, 0, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Shrinkable::new(
|
||||
1.0,
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Max Width 500, Min 200", self.font_family, 16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_max_width(500.)
|
||||
.with_min_width(200.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 150, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Fixed 100", self.font_family, 16.).finish(),
|
||||
)
|
||||
.with_width(100.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 0, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
}
|
||||
|
||||
fn make_multiple_expanded_row(&self) -> Flex {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Expanded::new(
|
||||
3.0,
|
||||
Container::new(Text::new_inline("Flex: 3.0", self.font_family, 16.).finish())
|
||||
.with_background_color(ColorU::new(255, 0, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Expanded::new(
|
||||
1.0,
|
||||
Container::new(Text::new_inline("Flex: 1.0", self.font_family, 16.).finish())
|
||||
.with_background_color(ColorU::new(0, 150, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Expanded::new(
|
||||
2.0,
|
||||
Container::new(Text::new_inline("Flex: 2.0", self.font_family, 16.).finish())
|
||||
.with_background_color(ColorU::new(0, 0, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
}
|
||||
|
||||
fn make_multiple_expanded_with_constraints_row(&self) -> Flex {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Expanded::new(
|
||||
1.0,
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Min 100, Max 200", self.font_family, 16.).finish(),
|
||||
)
|
||||
.with_max_width(200.)
|
||||
.with_min_width(100.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(255, 0, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Expanded::new(
|
||||
1.0,
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Min 100, Max 200", self.font_family, 16.).finish(),
|
||||
)
|
||||
.with_max_width(200.)
|
||||
.with_min_width(100.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 150, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Expanded::new(
|
||||
1.0,
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Min 100, Max 200", self.font_family, 16.).finish(),
|
||||
)
|
||||
.with_max_width(200.)
|
||||
.with_min_width(100.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 0, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
}
|
||||
|
||||
fn make_multiple_expanded_with_constraints_varying_flex_row(&self) -> Flex {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Expanded::new(
|
||||
3.0,
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Min 100, Max 200, Flex: 3.0", self.font_family, 16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_max_width(200.)
|
||||
.with_min_width(100.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(255, 0, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Expanded::new(
|
||||
1.0,
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Min 100, Max 200, Flex: 1.0", self.font_family, 16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_max_width(200.)
|
||||
.with_min_width(100.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 150, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Expanded::new(
|
||||
2.0,
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline("Min 100, Max 200, Flex: 2.0", self.font_family, 16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_max_width(200.)
|
||||
.with_min_width(100.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 0, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
// Let's render a simple black rect background.
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
let row_expanded = Container::new(
|
||||
self.make_expanded_row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Start)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
let row_shrinkable = Container::new(
|
||||
self.make_shrinkable_row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Start)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_bottom(32.)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let multiple_expanded_row = Container::new(
|
||||
self.make_multiple_expanded_row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Start)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let multiple_expanded_with_constraints_row = Container::new(
|
||||
self.make_multiple_expanded_with_constraints_row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Start)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let multiple_expanded_with_constraints_varying_flex_row = Container::new(
|
||||
self.make_multiple_expanded_with_constraints_varying_flex_row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Start)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
Stack::new()
|
||||
.with_child(Rect::new().with_background_color(ColorU::black()).finish())
|
||||
.with_child(
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_child(self.make_label("Expanded - FlexFit::Tight".to_owned()))
|
||||
.with_child(row_expanded)
|
||||
.with_child(
|
||||
self.make_label(
|
||||
"Shrinkable (Old Expanded) - FlexFit::Loose ".to_owned(),
|
||||
),
|
||||
)
|
||||
.with_child(row_shrinkable)
|
||||
.with_child(self.make_label("Multiple Expanded with varying flex amounts".to_owned()))
|
||||
.with_child(multiple_expanded_row)
|
||||
.with_child(self.make_label("Multiple Expanded with constraints in Flex with MainAxisSize::Min".to_owned()))
|
||||
.with_child(multiple_expanded_with_constraints_row)
|
||||
.with_child(self.make_label("Multiple Expanded with constraints and varying flex amounts in Flex with MainAxisSize::Min".to_owned()))
|
||||
.with_child(self.make_label("Note that this results in children beginning to shrink at different times and rates".to_owned()))
|
||||
.with_child(multiple_expanded_with_constraints_varying_flex_row)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(32.)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::SingletonEntity as _;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ConstrainedBox, Container, Flex, MainAxisAlignment, MainAxisSize, ParentElement,
|
||||
Rect, Shrinkable, Stack, Text, Wrap,
|
||||
},
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
use galaxyui::color::ColorU;
|
||||
|
||||
pub struct RootView {
|
||||
font_family: FamilyId,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let font_family = galaxyui::fonts::Cache::handle(ctx)
|
||||
.update(ctx, |cache, _| cache.load_system_font("Arial").unwrap());
|
||||
RootView { font_family }
|
||||
}
|
||||
|
||||
fn make_label(&self, label: String) -> Box<dyn Element> {
|
||||
Flex::row()
|
||||
.with_child(Text::new_inline(label, self.font_family, 16.).finish())
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn make_row(&self) -> Flex {
|
||||
Flex::row()
|
||||
.with_spacing(20.)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(Text::new_inline("1", self.font_family, 16.).finish())
|
||||
.with_width(200.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(255, 0, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(Text::new_inline("2", self.font_family, 16.).finish())
|
||||
.with_width(200.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 255, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(Text::new_inline("3", self.font_family, 16.).finish())
|
||||
.with_width(200.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 0, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
}
|
||||
|
||||
fn make_wrap_row(&self) -> Wrap {
|
||||
Wrap::row()
|
||||
.with_spacing(20.)
|
||||
.with_run_spacing(10.)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(Text::new_inline("1", self.font_family, 16.).finish())
|
||||
.with_width(200.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(255, 0, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(Text::new_inline("2", self.font_family, 16.).finish())
|
||||
.with_width(200.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 255, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(Text::new_inline("3", self.font_family, 16.).finish())
|
||||
.with_width(200.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 0, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
}
|
||||
|
||||
fn make_column(&self) -> Flex {
|
||||
Flex::column()
|
||||
.with_spacing(20.)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(Text::new_inline("1", self.font_family, 16.).finish())
|
||||
.with_width(20.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(255, 0, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(Text::new_inline("2", self.font_family, 16.).finish())
|
||||
.with_width(20.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 255, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(Text::new_inline("3", self.font_family, 16.).finish())
|
||||
.with_width(20.)
|
||||
.with_height(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(0, 0, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
// Let's render a simple black rect background.
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
let row_between = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let row_between_reverse = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_row()
|
||||
.with_reverse_orientation()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let row_evenly = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceEvenly)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let row_evenly_reverse = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_row()
|
||||
.with_reverse_orientation()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceEvenly)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let row_center = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Center)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let row_center_reverse = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_row()
|
||||
.with_reverse_orientation()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Center)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let row_start = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Start)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let row_start_reverse = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_row()
|
||||
.with_reverse_orientation()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Start)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let row_end = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::End)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let row_end_reverse = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_row()
|
||||
.with_reverse_orientation()
|
||||
.with_main_axis_alignment(MainAxisAlignment::End)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let column_between = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_column()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let column_between_reverse = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_column()
|
||||
.with_reverse_orientation()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let column_evenly = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_column()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceEvenly)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let column_evenly_reverse = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_column()
|
||||
.with_reverse_orientation()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceEvenly)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let column_center = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_column()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Center)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let column_center_reverse = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_column()
|
||||
.with_reverse_orientation()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Center)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let column_start = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_column()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Start)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let column_start_reverse = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_column()
|
||||
.with_reverse_orientation()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Start)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let column_end = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_column()
|
||||
.with_main_axis_alignment(MainAxisAlignment::End)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let column_end_reverse = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_column()
|
||||
.with_reverse_orientation()
|
||||
.with_main_axis_alignment(MainAxisAlignment::End)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let wrap_row = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_wrap_row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceEvenly)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
let wrap_row_reverse = Container::new(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
self.make_wrap_row()
|
||||
.with_reverse_orientation()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceEvenly)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::white()))
|
||||
.finish();
|
||||
|
||||
Stack::new()
|
||||
.with_child(Rect::new().with_background_color(ColorU::black()).finish())
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(self.make_label("Space Between".to_owned()))
|
||||
.with_child(row_between)
|
||||
.with_child(row_between_reverse)
|
||||
.with_child(self.make_label("Space Evenly".to_owned()))
|
||||
.with_child(row_evenly)
|
||||
.with_child(row_evenly_reverse)
|
||||
.with_child(self.make_label("Center".to_owned()))
|
||||
.with_child(row_center)
|
||||
.with_child(row_center_reverse)
|
||||
.with_child(self.make_label("Start".to_owned()))
|
||||
.with_child(row_start)
|
||||
.with_child(row_start_reverse)
|
||||
.with_child(self.make_label("End".to_owned()))
|
||||
.with_child(row_end)
|
||||
.with_child(row_end_reverse)
|
||||
.with_child(self.make_label("Wrap Row".to_owned()))
|
||||
.with_child(wrap_row)
|
||||
.with_child(wrap_row_reverse)
|
||||
.with_child(
|
||||
ConstrainedBox::new(
|
||||
Flex::row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_child(self.make_label("Space Between ->".to_owned()))
|
||||
.with_child(column_between)
|
||||
.with_child(column_between_reverse)
|
||||
.with_child(self.make_label("Space Evenly ->".to_owned()))
|
||||
.with_child(column_evenly)
|
||||
.with_child(column_evenly_reverse)
|
||||
.with_child(self.make_label("Center ->".to_owned()))
|
||||
.with_child(column_center)
|
||||
.with_child(column_center_reverse)
|
||||
.with_child(self.make_label("Start ->".to_owned()))
|
||||
.with_child(column_start)
|
||||
.with_child(column_start_reverse)
|
||||
.with_child(self.make_label("End ->".to_owned()))
|
||||
.with_child(column_end)
|
||||
.with_child(column_end_reverse)
|
||||
.finish(),
|
||||
)
|
||||
.with_max_height(300.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
let view_handle = root_view::RootView::new;
|
||||
ctx.add_window(galaxyui::AddWindowOptions::default(), view_handle);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
//! A UI sample demonstrating how the SelectableArea element can be used.
|
||||
|
||||
use markdown_parser::{parse_markdown, FormattedTextFragment, FormattedTextLine};
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::SingletonEntity as _;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
ChildView, ConstrainedBox, Flex, FormattedTextElement, HeadingFontSizeMultipliers,
|
||||
ParentElement, Rect, SelectableArea, SelectionHandle, Stack, Text,
|
||||
},
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use galaxyui::color::ColorU;
|
||||
use galaxyui::elements::{Align, HighlightedHyperlink, HyperlinkLens};
|
||||
|
||||
pub struct RootView {
|
||||
sub_view: ViewHandle<FormattedTextView>,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let sub_view = ctx.add_view(|ctx| {
|
||||
let font_family = galaxyui::fonts::Cache::handle(ctx).update(ctx, |cache, _| {
|
||||
cache.load_system_font("Menlo").expect("Should load Menlo")
|
||||
});
|
||||
let view = FormattedTextView {
|
||||
font_family,
|
||||
highlighted_link: Default::default(),
|
||||
selectable_area_state_handle: Default::default(),
|
||||
};
|
||||
ctx.focus_self();
|
||||
view
|
||||
});
|
||||
Self { sub_view }
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, _ctx: &AppContext) -> Box<dyn Element> {
|
||||
ChildView::new(&self.sub_view).finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FormattedTextView {
|
||||
font_family: FamilyId,
|
||||
highlighted_link: HighlightedHyperlink,
|
||||
selectable_area_state_handle: SelectionHandle,
|
||||
}
|
||||
|
||||
impl Entity for FormattedTextView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for FormattedTextView {
|
||||
fn ui_name() -> &'static str {
|
||||
"SelectableExampleView"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
Stack::new()
|
||||
.with_child(Rect::new().with_background_color(ColorU::black()).finish())
|
||||
.with_child(
|
||||
SelectableArea::new(
|
||||
self.selectable_area_state_handle.clone(),
|
||||
|selection_args, _, _| {
|
||||
println!("Selected text: {:?}", selection_args.selection);
|
||||
},
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
FormattedTextElement::new(
|
||||
parse_markdown(concat!(
|
||||
"## This is a markdown header\n",
|
||||
"### This is a subheader\n",
|
||||
"This is a ~~strikethrough~~ text.\n",
|
||||
"* list item 1\n",
|
||||
"* list item 2\n",
|
||||
"* list item 3\n",
|
||||
"* list item 4\n",
|
||||
"```rust\n",
|
||||
"fn main() {\n",
|
||||
" println!(\"Hello, world!\");\n",
|
||||
"}\n",
|
||||
"```\n",
|
||||
"fi\n",
|
||||
"cd\n",
|
||||
"this is a [link](https://www.google.com)\n",
|
||||
))
|
||||
.unwrap()
|
||||
.append_line(
|
||||
FormattedTextLine::Line(vec![
|
||||
FormattedTextFragment::plain_text(
|
||||
"\nThis is a link that dispatches an action: ",
|
||||
),
|
||||
FormattedTextFragment::hyperlink_action(
|
||||
"press enter",
|
||||
RootViewAction::LinkClicked,
|
||||
),
|
||||
]),
|
||||
),
|
||||
13.,
|
||||
self.font_family,
|
||||
self.font_family,
|
||||
ColorU::white(),
|
||||
self.highlighted_link.clone(),
|
||||
)
|
||||
.with_line_height_ratio(1.2)
|
||||
.with_heading_to_font_size_multipliers(
|
||||
HeadingFontSizeMultipliers {
|
||||
h1: 1.8,
|
||||
h2: 1.5,
|
||||
h3: 1.2,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.register_default_click_handlers_with_action_support(
|
||||
|hyperlink_lens, evt, ctx| match hyperlink_lens {
|
||||
HyperlinkLens::Url(url) => {
|
||||
ctx.open_url(url);
|
||||
}
|
||||
HyperlinkLens::Action(action_ref) => {
|
||||
if let Some(root_action) = action_ref
|
||||
.as_any()
|
||||
.downcast_ref::<RootViewAction>(
|
||||
) {
|
||||
evt.dispatch_typed_action(root_action.clone());
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.set_selectable(true)
|
||||
.finish(),
|
||||
Text::new(
|
||||
"This is normal Text (large font, not a header)",
|
||||
self.font_family,
|
||||
13. * 1.5,
|
||||
)
|
||||
.finish(),
|
||||
])
|
||||
.finish(),
|
||||
)
|
||||
.with_max_width(700.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RootViewAction {
|
||||
LinkClicked,
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = RootViewAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, _ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
RootViewAction::LinkClicked => {
|
||||
println!("Link clicked");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
use image::ImageEncoder;
|
||||
use pathfinder_color::ColorU;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use galaxyui::SingletonEntity as _;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, ConstrainedBox, Container, DispatchEventResult, EventHandler, Padding,
|
||||
ParentElement, Rect, Stack, Text,
|
||||
},
|
||||
fonts::{Cache as FontCache, FamilyId},
|
||||
platform::CapturedFrame,
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RootViewAction {
|
||||
CaptureFrame,
|
||||
}
|
||||
|
||||
pub struct RootView {
|
||||
window_id: galaxyui::WindowId,
|
||||
font_family: FamilyId,
|
||||
last_capture_msg: Arc<Mutex<Option<String>>>,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let window_id = ctx.window_id();
|
||||
let font_family = FontCache::handle(ctx)
|
||||
.update(ctx, |cache: &mut FontCache, _| {
|
||||
cache.load_system_font("Arial").ok()
|
||||
})
|
||||
.unwrap_or(FamilyId(0));
|
||||
log::info!("Frame capture demo initialized. Click the button to capture!");
|
||||
println!("\n📸 Click the blue button to capture the frame!\n");
|
||||
Self {
|
||||
window_id,
|
||||
font_family,
|
||||
last_capture_msg: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn request_capture(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let filename = format!("frame_capture_{}.png", timestamp);
|
||||
|
||||
log::info!("Requesting frame capture to: {}", filename);
|
||||
println!("\n📸 Requesting frame capture to: {}\n", filename);
|
||||
|
||||
*self.last_capture_msg.lock().unwrap() = Some("Capture requested...".to_string());
|
||||
ctx.notify();
|
||||
|
||||
if let Some(window) = ctx.windows().platform_window(self.window_id) {
|
||||
let msg_handle = Arc::clone(&self.last_capture_msg);
|
||||
window
|
||||
.as_ctx()
|
||||
.request_frame_capture(Box::new(move |frame| {
|
||||
log::info!("Frame captured, saving to file");
|
||||
match save_frame_as_png(&frame, &filename) {
|
||||
Ok(()) => {
|
||||
log::info!("Frame saved to: {}", filename);
|
||||
println!("\n✅ Frame saved to: {}\n", filename);
|
||||
*msg_handle.lock().unwrap() =
|
||||
Some(format!("Frame written to {}", filename));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to save frame: {}", e);
|
||||
*msg_handle.lock().unwrap() = Some(format!("Error: {}", e));
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_frame_as_png(frame: &CapturedFrame, path: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file = std::fs::File::create(path)?;
|
||||
let mut writer = std::io::BufWriter::new(file);
|
||||
|
||||
let encoder = image::codecs::png::PngEncoder::new_with_quality(
|
||||
&mut writer,
|
||||
image::codecs::png::CompressionType::Fast,
|
||||
image::codecs::png::FilterType::NoFilter,
|
||||
);
|
||||
|
||||
encoder.write_image(
|
||||
&frame.data,
|
||||
frame.width,
|
||||
frame.height,
|
||||
image::ColorType::Rgba8.into(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
let button_color = ColorU::new(70, 120, 200, 255);
|
||||
let status_msg = self
|
||||
.last_capture_msg
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.clone())
|
||||
.unwrap_or_else(|| "Click the button below to write a frame".to_string());
|
||||
|
||||
Stack::new()
|
||||
// Dark background
|
||||
.with_child(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(40, 40, 45, 255))
|
||||
.finish(),
|
||||
)
|
||||
// Content: Colorful squares arranged vertically
|
||||
.with_child(
|
||||
Align::new(
|
||||
Container::new(
|
||||
Stack::new()
|
||||
// Title text
|
||||
.with_child(
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
"Frame Capture Test".to_string(),
|
||||
self.font_family,
|
||||
28.0,
|
||||
)
|
||||
.with_color(ColorU::white())
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(16.0))
|
||||
.finish(),
|
||||
)
|
||||
// Subtitle
|
||||
.with_child(
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
"WarpUI rendering sample with clickable capture button"
|
||||
.to_string(),
|
||||
self.font_family,
|
||||
16.0,
|
||||
)
|
||||
.with_color(ColorU::new(200, 200, 200, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(12.0))
|
||||
.finish(),
|
||||
)
|
||||
// Toast / status line
|
||||
.with_child(
|
||||
Container::new(
|
||||
Text::new_inline(status_msg, self.font_family, 14.0)
|
||||
.with_color(ColorU::new(180, 220, 180, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(12.0))
|
||||
.finish(),
|
||||
)
|
||||
// Red square
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(255, 100, 100, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(150.0)
|
||||
.with_height(150.0)
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(15.0))
|
||||
.finish(),
|
||||
)
|
||||
// Green square
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(100, 255, 100, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(150.0)
|
||||
.with_height(150.0)
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(15.0))
|
||||
.finish(),
|
||||
)
|
||||
// Blue square
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(100, 100, 255, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(150.0)
|
||||
.with_height(150.0)
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(15.0))
|
||||
.finish(),
|
||||
)
|
||||
// Orange square
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(255, 165, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(150.0)
|
||||
.with_height(150.0)
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(15.0))
|
||||
.finish(),
|
||||
)
|
||||
// Capture button (purple square)
|
||||
.with_child(
|
||||
Container::new(
|
||||
EventHandler::new(
|
||||
ConstrainedBox::new(
|
||||
Stack::new()
|
||||
// Button background
|
||||
.with_child(
|
||||
Rect::new()
|
||||
.with_background_color(button_color)
|
||||
.finish(),
|
||||
)
|
||||
// Button label
|
||||
.with_child(
|
||||
Align::new(
|
||||
Text::new_inline(
|
||||
"Write Frame to File System"
|
||||
.to_string(),
|
||||
self.font_family,
|
||||
16.0,
|
||||
)
|
||||
.with_color(ColorU::white())
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_width(200.0)
|
||||
.with_height(80.0)
|
||||
.finish(),
|
||||
)
|
||||
.on_left_mouse_down(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(RootViewAction::CaptureFrame);
|
||||
DispatchEventResult::StopPropagation
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(15.0))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_padding(Padding::uniform(40.0))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = RootViewAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
RootViewAction::CaptureFrame => self.request_capture(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#13D3D3" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#13D3D3" fill-rule="evenodd" clip-rule="evenodd" d="M11.614 1.05689C11.377 1.13089 11.212 1.26089 11.109 1.45289C11.02 1.61889 11.02 1.62889 11.009 3.91089C11.001 5.39189 11.012 6.25589 11.039 6.35489C11.144 6.74689 11.433 6.96089 11.896 6.99089C12.381 7.02189 12.699 6.88189 12.88 6.55789L12.98 6.37989V3.99989V1.61989L12.891 1.45289C12.842 1.36189 12.746 1.24689 12.678 1.19889C12.414 1.00989 11.957 0.949891 11.614 1.05689ZM4.54602 4.04489C4.34302 4.15189 4.06602 4.43189 3.96302 4.63489C3.85302 4.84889 3.85402 5.15089 3.96402 5.37189C4.09202 5.62789 7.16402 8.69089 7.40002 8.79689C7.76202 8.95989 8.13302 8.86189 8.46602 8.51289C8.79602 8.16789 8.87702 7.83489 8.71702 7.47989C8.61102 7.24389 5.54802 4.17189 5.29202 4.04389C5.07202 3.93389 4.75502 3.93489 4.54602 4.04489ZM18.7 4.04589C18.605 4.09489 17.892 4.77489 16.952 5.71389C15.277 7.38689 15.203 7.47589 15.201 7.82489C15.2 8.05489 15.293 8.25489 15.511 8.48789C15.86 8.85989 16.228 8.96489 16.6 8.79689C16.836 8.69089 19.908 5.62789 20.036 5.37189C20.212 5.01989 20.121 4.63889 19.781 4.29889C19.441 3.95889 19.047 3.86689 18.7 4.04589ZM1.61402 11.0569C1.37702 11.1309 1.21202 11.2609 1.10902 11.4529C1.03502 11.5919 1.02002 11.6839 1.02002 11.9999C1.02002 12.3309 1.03302 12.4029 1.12002 12.5579C1.23002 12.7549 1.41402 12.8979 1.63502 12.9589C1.83102 13.0139 6.16902 13.0139 6.36502 12.9589C6.58602 12.8979 6.77002 12.7549 6.88002 12.5579C6.96702 12.4019 6.98002 12.3319 6.97902 11.9999C6.97802 11.5669 6.89802 11.3549 6.67802 11.1989C6.40602 11.0049 6.33602 10.9999 3.98402 11.0029C2.29302 11.0049 1.74102 11.0169 1.61402 11.0569ZM17.614 11.0569C17.377 11.1309 17.212 11.2609 17.109 11.4529C17.035 11.5919 17.02 11.6839 17.02 11.9999C17.02 12.3309 17.033 12.4029 17.12 12.5579C17.23 12.7549 17.414 12.8979 17.635 12.9589C17.831 13.0139 22.169 13.0139 22.365 12.9589C22.586 12.8979 22.77 12.7549 22.88 12.5579C22.967 12.4019 22.98 12.3319 22.979 11.9999C22.978 11.5669 22.898 11.3549 22.678 11.1989C22.406 11.0049 22.336 10.9999 19.984 11.0029C18.293 11.0049 17.741 11.0169 17.614 11.0569ZM7.40002 15.2829C7.15902 15.3909 4.09202 18.4529 3.96002 18.7159C3.78502 19.0669 3.87502 19.4369 4.21902 19.7809C4.55902 20.1209 4.94002 20.2119 5.29202 20.0359C5.54802 19.9079 8.61102 16.8359 8.71702 16.5999C8.87702 16.2449 8.80602 15.9309 8.48902 15.5919C8.14002 15.2199 7.77202 15.1149 7.40002 15.2829ZM15.879 15.2979C15.659 15.4129 15.347 15.7379 15.262 15.9409C15.178 16.1429 15.185 16.3839 15.283 16.5999C15.389 16.8359 18.452 19.9079 18.708 20.0359C19.06 20.2119 19.441 20.1209 19.781 19.7809C20.121 19.4409 20.212 19.0599 20.036 18.7079C19.908 18.4519 16.836 15.3889 16.6 15.2829C16.36 15.1749 16.105 15.1799 15.879 15.2979ZM11.614 17.0569C11.377 17.1309 11.212 17.2609 11.109 17.4529C11.02 17.6189 11.02 17.6289 11.009 19.9109C11.001 21.3919 11.012 22.2559 11.039 22.3549C11.144 22.7469 11.433 22.9609 11.896 22.9909C12.381 23.0219 12.699 22.8819 12.88 22.5579L12.98 22.3799V19.9999V17.6199L12.891 17.4529C12.842 17.3619 12.746 17.2469 12.678 17.1989C12.414 17.0099 11.957 16.9499 11.614 17.0569Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,16 @@
|
||||
use anyhow::Result;
|
||||
use root_view::RootView;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use galaxyui::platform;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(()), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(galaxyui::AddWindowOptions::default(), |_| RootView::new());
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
CacheOption, ConstrainedBox, Flex, Icon, Image, MainAxisAlignment, MainAxisSize,
|
||||
ParentElement, Rect, Stack,
|
||||
},
|
||||
AppContext, Element, Entity, TypedActionView, View,
|
||||
};
|
||||
|
||||
pub struct RootView {}
|
||||
|
||||
impl RootView {
|
||||
pub fn new() -> Self {
|
||||
RootView {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RootView {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
let asset_source = ::asset_cache::url_source(
|
||||
"https://i.ebayimg.com/images/g/B~gAAOSwhNthhdjn/s-l1600.jpg",
|
||||
);
|
||||
|
||||
Stack::new()
|
||||
.with_child(Rect::new().with_background_color(ColorU::white()).finish())
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Center)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Center)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_child(
|
||||
ConstrainedBox::new(
|
||||
Image::new(asset_source, CacheOption::BySize)
|
||||
.before_load(
|
||||
Icon::new(
|
||||
"ui/examples/image/loading.svg",
|
||||
ColorU::black(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_height(500.)
|
||||
.with_width(500.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::SingletonEntity as _;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ConstrainedBox, Container, Fill, Flex, List, ListState, MainAxisSize,
|
||||
ParentElement, Rect, ScrollStateHandle, Scrollable, ScrollableElement, ScrollbarWidth,
|
||||
Stack, Text,
|
||||
},
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use galaxyui::color::ColorU;
|
||||
|
||||
pub struct RootView {
|
||||
font_family: FamilyId,
|
||||
list_state: ListState<()>,
|
||||
scroll_state: ScrollStateHandle,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let font_family = galaxyui::fonts::Cache::handle(ctx)
|
||||
.update(ctx, |cache, _| cache.load_system_font("Arial").unwrap());
|
||||
|
||||
let list_state = ListState::new(move |i, _scroll_offset, _app| {
|
||||
println!(" 📦 Creating element for item {i}"); // This should only appear for visible items!
|
||||
Self::make_list_item(i, font_family).finish()
|
||||
});
|
||||
let scroll_state = Arc::new(Mutex::new(Default::default()));
|
||||
|
||||
// Add many items to demonstrate viewporting - only visible ones should be rendered
|
||||
println!("Creating List with 1000 items...");
|
||||
for _ in 0..=1000 {
|
||||
list_state.add_item();
|
||||
}
|
||||
println!(
|
||||
"✅ All 1000 items added to list state. Now only visible ones should be rendered."
|
||||
);
|
||||
|
||||
RootView {
|
||||
font_family,
|
||||
list_state,
|
||||
scroll_state,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_list_item(index: usize, font_family: FamilyId) -> Container {
|
||||
// Alternate colors to make it easy to see which items are rendered
|
||||
let bg_color = if index.is_multiple_of(2) {
|
||||
ColorU::new(240, 240, 240, 255) // Light gray
|
||||
} else {
|
||||
ColorU::new(255, 255, 255, 255) // White
|
||||
};
|
||||
|
||||
let border_color = if index.is_multiple_of(10) {
|
||||
ColorU::new(255, 0, 0, 255) // Red border for every 10th item
|
||||
} else {
|
||||
ColorU::new(200, 200, 200, 255) // Light gray border
|
||||
};
|
||||
|
||||
let height = 50. * (index + 1) as f32;
|
||||
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Text::new_inline(format!("Item #{index}"), font_family, 16.)
|
||||
.with_color(ColorU::black())
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline(
|
||||
if index.is_multiple_of(10) {
|
||||
" (MILESTONE)".to_string()
|
||||
} else {
|
||||
format!(" - Height: {height}px")
|
||||
},
|
||||
font_family,
|
||||
14.,
|
||||
)
|
||||
.with_color(ColorU::black())
|
||||
.finish(),
|
||||
)
|
||||
.with_width(200.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.finish(),
|
||||
)
|
||||
.with_width(600.)
|
||||
.with_height(height)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(bg_color)
|
||||
.with_border(Border::all(1.).with_border_color(border_color))
|
||||
}
|
||||
|
||||
fn make_instructions(&self) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Text::new_inline(
|
||||
"List Demo - 1000 Items".to_string(),
|
||||
self.font_family,
|
||||
20.,
|
||||
)
|
||||
.with_color(ColorU::black())
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new_inline(
|
||||
"This demonstrates viewporting: only visible items are rendered!"
|
||||
.to_string(),
|
||||
self.font_family,
|
||||
14.,
|
||||
)
|
||||
.with_color(ColorU::black())
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new_inline(
|
||||
"🔍 Check the console output to see which items are being rendered."
|
||||
.to_string(),
|
||||
self.font_family,
|
||||
14.,
|
||||
)
|
||||
.with_color(ColorU::black())
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new_inline(
|
||||
"🟥 Red borders mark milestone items (every 10th).".to_string(),
|
||||
self.font_family,
|
||||
14.,
|
||||
)
|
||||
.with_color(ColorU::black())
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new_inline(
|
||||
"📏 Viewport height: 400px.".to_string(),
|
||||
self.font_family,
|
||||
12.,
|
||||
)
|
||||
.with_color(ColorU::black())
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_height(140.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(230, 230, 255, 255))
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::new(100, 100, 200, 255)))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"ListRootView"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
Stack::new()
|
||||
.with_child(Rect::new().with_background_color(ColorU::white()).finish())
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(self.make_instructions())
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Scrollable::vertical(
|
||||
self.scroll_state.clone(),
|
||||
List::new(self.list_state.clone()).finish_scrollable(),
|
||||
ScrollbarWidth::Auto,
|
||||
Fill::Solid(ColorU::new(150, 150, 150, 255)), // Non-active thumb
|
||||
Fill::Solid(ColorU::new(100, 100, 100, 255)), // Active thumb
|
||||
Fill::Solid(ColorU::new(240, 240, 240, 255)), // Track background
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_height(400.) // Constrain the viewport height
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(250, 250, 250, 255))
|
||||
.with_border(Border::all(2.).with_border_color(ColorU::black()))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
root_view::init(ctx);
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
use pathfinder_geometry::rect::RectF;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use galaxyui::elements::new_scrollable::AxisConfiguration;
|
||||
use galaxyui::elements::new_scrollable::ClippedAxisConfiguration;
|
||||
use galaxyui::elements::new_scrollable::DualAxisConfig;
|
||||
use galaxyui::elements::new_scrollable::NewScrollableElement;
|
||||
use galaxyui::elements::new_scrollable::ScrollableAppearance;
|
||||
use galaxyui::elements::new_scrollable::ScrollableAxis;
|
||||
use galaxyui::elements::Axis;
|
||||
|
||||
use galaxyui::elements::ChildView;
|
||||
use galaxyui::elements::ClippedScrollStateHandle;
|
||||
use galaxyui::elements::NewScrollable;
|
||||
use galaxyui::elements::Point;
|
||||
use galaxyui::elements::ScrollData;
|
||||
use galaxyui::elements::ScrollStateHandle;
|
||||
use galaxyui::keymap::FixedBinding;
|
||||
use galaxyui::units::Pixels;
|
||||
use galaxyui::AppContext;
|
||||
use galaxyui::TypedActionView;
|
||||
use galaxyui::ViewHandle;
|
||||
use galaxyui::{
|
||||
elements::{ConstrainedBox, ParentElement, Rect, ScrollbarWidth, Stack},
|
||||
Element, Entity, View, ViewContext,
|
||||
};
|
||||
|
||||
use galaxyui::color::ColorU;
|
||||
|
||||
pub fn init(ctx: &mut AppContext) {
|
||||
use galaxyui::keymap::macros::*;
|
||||
|
||||
// Add bindings to trigger actions in the subview.
|
||||
ctx.register_fixed_bindings([
|
||||
FixedBinding::new("up", SubViewAction::ScrollVertical(50.), id!("SubView")),
|
||||
FixedBinding::new("down", SubViewAction::ScrollVertical(-50.), id!("SubView")),
|
||||
]);
|
||||
}
|
||||
|
||||
pub struct RootView {
|
||||
// RootView "owns" a viewhandle to the subview.
|
||||
sub_view: ViewHandle<SubView>,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
// Adding typed action view allows the view to receive keydown events.
|
||||
let sub_view = ctx.add_typed_action_view(|ctx| {
|
||||
let view = SubView::default();
|
||||
// Need the view to be focused for keydown actions to be dispatched to it.
|
||||
ctx.focus_self();
|
||||
view
|
||||
});
|
||||
Self { sub_view }
|
||||
}
|
||||
}
|
||||
|
||||
// Implement the entity trait.
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
// Implement the view trait so RootView could be considered as a view.
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
// Renders the child view of sub_view.
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
ChildView::new(&self.sub_view).finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SubViewAction {
|
||||
ScrollVertical(f32),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SubView {
|
||||
pub scroll_state_horizontal: ClippedScrollStateHandle,
|
||||
pub scroll_state_vertical: ScrollStateHandle,
|
||||
pub scroll_top: f32,
|
||||
}
|
||||
|
||||
impl SubView {
|
||||
pub fn new(_ctx: &mut ViewContext<Self>) -> Self {
|
||||
SubView::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for SubView {
|
||||
type Event = ();
|
||||
}
|
||||
impl View for SubView {
|
||||
fn ui_name() -> &'static str {
|
||||
"SubView"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
let axis_config = DualAxisConfig::Manual {
|
||||
horizontal: AxisConfiguration::Clipped(ClippedAxisConfiguration {
|
||||
handle: self.scroll_state_horizontal.clone(),
|
||||
max_size: None,
|
||||
stretch_child: false,
|
||||
}),
|
||||
vertical: AxisConfiguration::Manual(self.scroll_state_vertical.clone()),
|
||||
child: ScrollableElement::new(self.scroll_top).finish_scrollable(),
|
||||
};
|
||||
let horizontally_scrollable = NewScrollable::horizontal_and_vertical(
|
||||
axis_config,
|
||||
ColorU::new(255, 255, 255, 150).into(),
|
||||
ColorU::white().into(),
|
||||
ColorU::new(100, 100, 100, 255).into(),
|
||||
)
|
||||
.with_horizontal_scrollbar(ScrollableAppearance::new(ScrollbarWidth::Auto, true))
|
||||
.with_vertical_scrollbar(ScrollableAppearance::new(ScrollbarWidth::Auto, false));
|
||||
|
||||
let constrained = ConstrainedBox::new(horizontally_scrollable.finish())
|
||||
.with_height(250.)
|
||||
.with_width(250.);
|
||||
|
||||
Stack::new()
|
||||
.with_child(Rect::new().with_background_color(ColorU::black()).finish())
|
||||
.with_child(constrained.finish())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
impl TypedActionView for SubView {
|
||||
type Action = SubViewAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
SubViewAction::ScrollVertical(scroll_top) => {
|
||||
// 250. viewport + 7. scrollbar width.
|
||||
self.scroll_top = (self.scroll_top - *scroll_top).clamp(0., 257.);
|
||||
}
|
||||
};
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
struct ScrollableElement {
|
||||
size: Option<Vector2F>,
|
||||
origin: Option<Point>,
|
||||
scroll_top: f32,
|
||||
}
|
||||
|
||||
impl ScrollableElement {
|
||||
fn new(scroll_top: f32) -> Self {
|
||||
Self {
|
||||
scroll_top,
|
||||
size: None,
|
||||
origin: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ScrollableElement {
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: galaxyui::SizeConstraint,
|
||||
_: &mut galaxyui::LayoutContext,
|
||||
_: &AppContext,
|
||||
) -> Vector2F {
|
||||
let size = vec2f(
|
||||
constraint.max_along(Axis::Horizontal).min(500.),
|
||||
constraint.max_along(Axis::Vertical).min(500.),
|
||||
);
|
||||
self.size = Some(size);
|
||||
size
|
||||
}
|
||||
|
||||
fn after_layout(&mut self, _: &mut galaxyui::AfterLayoutContext, _: &AppContext) {}
|
||||
|
||||
fn paint(&mut self, origin: Vector2F, ctx: &mut galaxyui::PaintContext, _app: &AppContext) {
|
||||
self.origin = Some(Point::from_vec2f(origin, ctx.scene.z_index()));
|
||||
let adjusted_origin = origin - vec2f(0., self.scroll_top);
|
||||
for i in 0..10 {
|
||||
for j in 0..10 {
|
||||
let color = (i + j) % 3;
|
||||
let color = if color == 0 {
|
||||
ColorU::new(255, 0, 0, 255)
|
||||
} else if color == 1 {
|
||||
ColorU::new(0, 255, 0, 255)
|
||||
} else {
|
||||
ColorU::new(0, 0, 255, 255)
|
||||
};
|
||||
|
||||
let cell_origin = adjusted_origin + vec2f(i as f32 * 50., j as f32 * 50.);
|
||||
ctx.scene
|
||||
.draw_rect_with_hit_recording(RectF::new(cell_origin, vec2f(50., 50.)))
|
||||
.with_background(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn size(&self) -> Option<Vector2F> {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn origin(&self) -> Option<Point> {
|
||||
self.origin
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_: &galaxyui::event::DispatchedEvent,
|
||||
_: &mut galaxyui::EventContext,
|
||||
_: &AppContext,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl NewScrollableElement for ScrollableElement {
|
||||
fn axis(&self) -> ScrollableAxis {
|
||||
ScrollableAxis::Vertical
|
||||
}
|
||||
|
||||
fn scroll_data(&self, axis: Axis, _app: &AppContext) -> Option<ScrollData> {
|
||||
match axis {
|
||||
Axis::Horizontal => None,
|
||||
Axis::Vertical => Some(ScrollData {
|
||||
scroll_start: Pixels::new(self.scroll_top),
|
||||
visible_px: Pixels::new(self.size.unwrap().y()),
|
||||
total_size: Pixels::new(500.),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll(&mut self, delta: galaxyui::units::Pixels, axis: Axis, ctx: &mut galaxyui::EventContext) {
|
||||
match axis {
|
||||
Axis::Horizontal => (),
|
||||
Axis::Vertical => {
|
||||
ctx.dispatch_typed_action(SubViewAction::ScrollVertical(delta.as_f32()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn axis_should_handle_scroll_wheel(&self, _axis: Axis) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::SingletonEntity as _;
|
||||
use galaxyui::{
|
||||
color::ColorU,
|
||||
elements::{
|
||||
Align, Border, ConstrainedBox, Container, CrossAxisAlignment, Flex, MainAxisAlignment,
|
||||
MainAxisSize, ParentElement, Percentage, Rect, Shrinkable, Text,
|
||||
},
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
pub struct RootView {
|
||||
font_family: FamilyId,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let font_family = galaxyui::fonts::Cache::handle(ctx)
|
||||
.update(ctx, |cache, _| cache.load_system_font("Arial").unwrap());
|
||||
RootView { font_family }
|
||||
}
|
||||
|
||||
fn make_label(&self, text: &str) -> Box<dyn Element> {
|
||||
Text::new_inline(text.to_string(), self.font_family, 16.).finish()
|
||||
}
|
||||
|
||||
fn make_width(&self) -> Box<dyn Element> {
|
||||
let bar = |pct: f32, color: ColorU| {
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Align::new(
|
||||
Percentage::width(
|
||||
pct,
|
||||
Rect::new().with_background_color(color).finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.left()
|
||||
.finish(),
|
||||
)
|
||||
.with_height(12.)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(1.).with_border_color(ColorU::white()))
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(self.make_label("Width 25/50/75%:"))
|
||||
.with_child(bar(0.25, ColorU::new(200, 80, 80, 255)))
|
||||
.with_child(bar(0.50, ColorU::new(80, 200, 80, 255)))
|
||||
.with_child(bar(0.75, ColorU::new(80, 120, 220, 255)))
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn make_height(&self) -> Box<dyn Element> {
|
||||
let bar = |pct: f32, color: ColorU| {
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Container::new(
|
||||
Align::new(
|
||||
Percentage::height(pct, Rect::new().with_background_color(color).finish())
|
||||
.finish(),
|
||||
)
|
||||
.top_left()
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::all(1.).with_border_color(ColorU::white()))
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
};
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::End)
|
||||
.with_spacing(12.)
|
||||
.with_child(self.make_label("Height 30/60/90%:"))
|
||||
.with_child(bar(0.30, ColorU::new(200, 80, 80, 255)))
|
||||
.with_child(bar(0.60, ColorU::new(80, 200, 80, 255)))
|
||||
.with_child(bar(0.90, ColorU::new(80, 120, 220, 255)))
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn make_both(&self) -> Box<dyn Element> {
|
||||
let cell = |w: f32, h: f32, color: ColorU| {
|
||||
let child =
|
||||
Percentage::both(w, h, Rect::new().with_background_color(color).finish()).finish();
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Container::new(Align::new(child).top_left().finish())
|
||||
.with_border(Border::all(1.).with_border_color(ColorU::white()))
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
};
|
||||
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_spacing(12.)
|
||||
.with_child(self.make_label("Both 60% x 50%, 80% x 30%:"))
|
||||
.with_child(cell(0.6, 0.5, ColorU::new(200, 80, 80, 255)))
|
||||
.with_child(cell(0.8, 0.3, ColorU::new(80, 200, 80, 255)))
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_spacing(16.)
|
||||
.with_child(self.make_width())
|
||||
.with_child(self.make_height())
|
||||
.with_child(self.make_both())
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::Start)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::black())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::SingletonEntity as _;
|
||||
use galaxyui::{
|
||||
color::ColorU,
|
||||
elements::{
|
||||
resizable_state_handle, Container, CrossAxisAlignment, DragBarSide, Flex,
|
||||
MainAxisAlignment, MainAxisSize, ParentElement, Rect, Resizable, ResizableStateHandle,
|
||||
Shrinkable, Stack, Text,
|
||||
},
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
pub struct RootView {
|
||||
font_family: FamilyId,
|
||||
left_panel_state: ResizableStateHandle,
|
||||
top_panel_state: ResizableStateHandle,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let font_family = galaxyui::fonts::Cache::handle(ctx)
|
||||
.update(ctx, |cache, _| cache.load_system_font("Arial").unwrap());
|
||||
|
||||
// Initialize resizable state handles
|
||||
let left_panel_state = resizable_state_handle(250.0);
|
||||
let top_panel_state = resizable_state_handle(150.0);
|
||||
|
||||
RootView {
|
||||
font_family,
|
||||
left_panel_state,
|
||||
top_panel_state,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_panel_content(&self, text: String, color: ColorU) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Center)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(
|
||||
Text::new_inline(text, self.font_family, 16.)
|
||||
.with_color(ColorU::white())
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(color)
|
||||
.with_uniform_padding(10.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn make_info_text(&self, text: String) -> Box<dyn Element> {
|
||||
Text::new_inline(text, self.font_family, 14.)
|
||||
.with_color(ColorU::white())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
// Create main column for vertical layout
|
||||
let mut main_column = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
|
||||
|
||||
// Top panel with bottom-side dragbar for vertical resizing
|
||||
let top_panel = Container::new(
|
||||
Resizable::new(
|
||||
self.top_panel_state.clone(),
|
||||
self.make_panel_content(
|
||||
"Top Panel\n(Drag bottom edge)".to_string(),
|
||||
ColorU::new(200, 100, 200, 255),
|
||||
),
|
||||
)
|
||||
.with_dragbar_side(DragBarSide::Bottom)
|
||||
.with_dragbar_color(galaxyui::elements::Fill::Solid(ColorU::new(0, 255, 255, 200)))
|
||||
.on_resize(move |ctx, _| {
|
||||
ctx.notify();
|
||||
})
|
||||
.on_start_resizing(|_, _| {
|
||||
eprintln!("Top panel: Started resizing");
|
||||
})
|
||||
.on_end_resizing(|_, _| {
|
||||
eprintln!("Top panel: Finished resizing");
|
||||
})
|
||||
.with_bounds_callback(Box::new(|window_size| {
|
||||
let min_height = 100.0;
|
||||
let max_height = window_size.y() * 0.5;
|
||||
(min_height, max_height.max(min_height))
|
||||
}))
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
main_column.add_child(top_panel);
|
||||
|
||||
// Create the row with CrossAxisAlignment::Stretch
|
||||
let mut main_row = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
|
||||
|
||||
// Left panel with right-side dragbar - added directly to the row
|
||||
let left_panel = Container::new(
|
||||
Resizable::new(
|
||||
self.left_panel_state.clone(),
|
||||
self.make_panel_content(
|
||||
"Left Panel\n(Drag right edge)".to_string(),
|
||||
ColorU::new(100, 100, 200, 255),
|
||||
),
|
||||
)
|
||||
.with_dragbar_side(DragBarSide::Right)
|
||||
.with_dragbar_color(galaxyui::elements::Fill::Solid(ColorU::new(255, 255, 0, 200)))
|
||||
.on_resize(move |ctx, _| {
|
||||
ctx.notify();
|
||||
})
|
||||
.on_start_resizing(|_, _| {
|
||||
eprintln!("Left panel: Started resizing");
|
||||
})
|
||||
.on_end_resizing(|_, _| {
|
||||
eprintln!("Left panel: Finished resizing");
|
||||
})
|
||||
.with_bounds_callback(Box::new(|window_size| {
|
||||
let min_width = 150.0;
|
||||
let max_width = window_size.x() * 0.6;
|
||||
(min_width, max_width.max(min_width))
|
||||
}))
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
main_row.add_child(left_panel);
|
||||
|
||||
// Right content area - wrapped in Shrinkable to fill remaining space
|
||||
let right_content = Container::new(
|
||||
Flex::column()
|
||||
.with_main_axis_alignment(MainAxisAlignment::Center)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_spacing(20.)
|
||||
.with_child(
|
||||
Text::new_inline("Resizable Example", self.font_family, 24.)
|
||||
.with_color(ColorU::white())
|
||||
.finish(),
|
||||
)
|
||||
.with_child(self.make_info_text("Yellow bar: Horizontal resize (left)".to_string()))
|
||||
.with_child(self.make_info_text("Cyan bar: Vertical resize (top)".to_string()))
|
||||
.with_child(self.make_info_text("Check terminal for events".to_string()))
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(50, 50, 50, 255))
|
||||
.with_uniform_padding(20.)
|
||||
.finish();
|
||||
|
||||
main_row.add_child(Shrinkable::new(1.0, right_content).finish());
|
||||
|
||||
main_column.add_child(Shrinkable::new(1.0, main_row.finish()).finish());
|
||||
|
||||
// Main layout
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(30, 30, 30, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_child(Shrinkable::new(1.0, main_column.finish()).finish())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use galaxyui::elements::ClippedScrollStateHandle;
|
||||
use galaxyui::elements::ClippedScrollable;
|
||||
|
||||
use galaxyui::{
|
||||
elements::{ConstrainedBox, Container, Flex, ParentElement, Rect, ScrollbarWidth, Stack},
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
use galaxyui::color::ColorU;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RootView {
|
||||
pub clipped_scroll_state: ClippedScrollStateHandle,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(_ctx: &mut ViewContext<Self>) -> Self {
|
||||
RootView::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
let mut column = Flex::column();
|
||||
|
||||
// Create 10 rows, where each row has 10 rectanges (each of size 50*50).
|
||||
// By the end, `column` will be 500 * 500.
|
||||
for i in 0..10 {
|
||||
let mut row = Flex::row();
|
||||
for j in 0..10 {
|
||||
let color = (i + j) % 3;
|
||||
let color = if color == 0 {
|
||||
ColorU::new(255, 0, 0, 255)
|
||||
} else if color == 1 {
|
||||
ColorU::new(0, 255, 0, 255)
|
||||
} else {
|
||||
ColorU::new(0, 0, 255, 255)
|
||||
};
|
||||
|
||||
row.add_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(Rect::new().finish())
|
||||
.with_height(50.)
|
||||
.with_width(50.)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(color)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
column.add_child(row.finish());
|
||||
}
|
||||
|
||||
// Change this to [`ClippedScrollable::vertical`] to see what a vertically scrollable element looks like.
|
||||
let horizontally_scrollable = ClippedScrollable::horizontal(
|
||||
self.clipped_scroll_state.clone(),
|
||||
column.finish(),
|
||||
ScrollbarWidth::Auto,
|
||||
ColorU::white().into(),
|
||||
ColorU::white().into(),
|
||||
ColorU::new(100, 100, 100, 255).into(),
|
||||
);
|
||||
|
||||
let constrained = ConstrainedBox::new(horizontally_scrollable.finish())
|
||||
.with_height(250.)
|
||||
.with_width(250.);
|
||||
|
||||
Stack::new()
|
||||
.with_child(Rect::new().with_background_color(ColorU::black()).finish())
|
||||
.with_child(constrained.finish())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
use galaxyui::color::ColorU;
|
||||
use galaxyui::elements::{
|
||||
Align, Container, CornerRadius, CrossAxisAlignment, Fill, Flex, ParentElement, Radius, Text,
|
||||
};
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::presenter::ChildView;
|
||||
use galaxyui::ui_components::components::{Coords, UiComponentStyles};
|
||||
use galaxyui::ui_components::segmented_control::{
|
||||
LabelConfig, RenderableOptionConfig, SegmentedControl, SegmentedControlEvent,
|
||||
};
|
||||
use galaxyui::SingletonEntity as _;
|
||||
use galaxyui::{AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DisplayMode {
|
||||
Rendered,
|
||||
Raw,
|
||||
}
|
||||
|
||||
pub struct RootView {
|
||||
font_family: FamilyId,
|
||||
segmented_control: ViewHandle<SegmentedControl<DisplayMode>>,
|
||||
selected_mode: DisplayMode,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let font_family = galaxyui::fonts::Cache::handle(ctx)
|
||||
.update(ctx, |cache, _| cache.load_system_font("Arial").unwrap());
|
||||
|
||||
let segmented_control = ctx.add_typed_action_view(move |_ctx| {
|
||||
SegmentedControl::new(
|
||||
vec![DisplayMode::Rendered, DisplayMode::Raw],
|
||||
move |mode, is_selected, _app| {
|
||||
Some(RenderableOptionConfig {
|
||||
icon_path: "",
|
||||
icon_color: ColorU::white(),
|
||||
label: Some(LabelConfig {
|
||||
label: match mode {
|
||||
DisplayMode::Rendered => "Rendered".into(),
|
||||
DisplayMode::Raw => "Raw".into(),
|
||||
},
|
||||
width_override: Some(55.0),
|
||||
color: if is_selected {
|
||||
ColorU::new(100, 200, 255, 255) // accent color
|
||||
} else {
|
||||
ColorU::white()
|
||||
},
|
||||
}),
|
||||
tooltip: None,
|
||||
background: if is_selected {
|
||||
Fill::Solid(ColorU::new(60, 60, 60, 255)) // surface_3
|
||||
} else {
|
||||
Fill::None
|
||||
},
|
||||
})
|
||||
},
|
||||
DisplayMode::Rendered,
|
||||
segmented_control_styles(font_family),
|
||||
)
|
||||
});
|
||||
|
||||
ctx.subscribe_to_view(&segmented_control, |me, _, event, ctx| {
|
||||
let SegmentedControlEvent::OptionSelected(mode) = event;
|
||||
me.selected_mode = *mode;
|
||||
ctx.notify();
|
||||
});
|
||||
|
||||
Self {
|
||||
font_family,
|
||||
segmented_control,
|
||||
selected_mode: DisplayMode::Rendered,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn segmented_control_styles(font_family: FamilyId) -> UiComponentStyles {
|
||||
UiComponentStyles {
|
||||
font_family_id: Some(font_family),
|
||||
font_size: Some(12.0),
|
||||
border_radius: Some(CornerRadius::with_all(Radius::Pixels(4.0))),
|
||||
border_width: Some(1.0),
|
||||
border_color: Some(Fill::Solid(ColorU::new(60, 60, 60, 255))), // surface_3
|
||||
background: Some(Fill::Solid(ColorU::new(30, 30, 30, 255))), // background
|
||||
height: Some(20.0),
|
||||
padding: Some(Coords::uniform(2.0)),
|
||||
margin: Some(Coords {
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
left: 0.0,
|
||||
right: 8.0,
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, _app: &AppContext) -> Box<dyn Element> {
|
||||
let status_text = format!("Selected: {:?}", self.selected_mode);
|
||||
|
||||
let content = Flex::column()
|
||||
.with_spacing(20.0)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(
|
||||
Text::new_inline("Segmented Control Example", self.font_family, 20.0)
|
||||
.with_color(ColorU::white())
|
||||
.finish(),
|
||||
)
|
||||
.with_child(ChildView::new(&self.segmented_control).finish())
|
||||
.with_child(
|
||||
Text::new_inline(status_text, self.font_family, 14.0)
|
||||
.with_color(ColorU::new(150, 150, 150, 255))
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
Container::new(Align::new(content).finish())
|
||||
.with_background_color(ColorU::new(20, 20, 20, 255))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
let view_handle = root_view::RootView::new;
|
||||
ctx.add_window(galaxyui::AddWindowOptions::default(), view_handle);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
//! A UI sample demonstrating how the SelectableArea element can be used.
|
||||
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::SingletonEntity as _;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ChildView, ConstrainedBox, Container, Flex, ParentElement, Rect, SelectableArea,
|
||||
SelectionHandle, Stack, Text,
|
||||
},
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use galaxyui::color::ColorU;
|
||||
|
||||
pub struct RootView {
|
||||
sub_view: ViewHandle<SelectableExampleView>,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let sub_view = ctx.add_view(|ctx| {
|
||||
let font_family = galaxyui::fonts::Cache::handle(ctx).update(ctx, |cache, _| {
|
||||
cache.load_system_font("Menlo").expect("Should load Menlo")
|
||||
});
|
||||
let view = SelectableExampleView {
|
||||
font_family,
|
||||
selectable_area_state_handle_1: Default::default(),
|
||||
selectable_area_state_handle_2: Default::default(),
|
||||
};
|
||||
ctx.focus_self();
|
||||
view
|
||||
});
|
||||
Self { sub_view }
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, _ctx: &AppContext) -> Box<dyn Element> {
|
||||
ChildView::new(&self.sub_view).finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SelectableExampleView {
|
||||
font_family: FamilyId,
|
||||
selectable_area_state_handle_1: SelectionHandle,
|
||||
selectable_area_state_handle_2: SelectionHandle,
|
||||
}
|
||||
|
||||
impl Entity for SelectableExampleView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for SelectableExampleView {
|
||||
fn ui_name() -> &'static str {
|
||||
"SelectableExampleView"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
Stack::new()
|
||||
.with_child(Rect::new().with_background_color(ColorU::black()).finish())
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
SelectableArea::new(
|
||||
self.selectable_area_state_handle_1.clone(),
|
||||
|selection_args, _, _| {
|
||||
println!("SELECTED TEXT - {:?}", selection_args.selection);
|
||||
},
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
Container::new(
|
||||
Text::new(
|
||||
"HELLO WORLD 1",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_vertical_margin(10.)
|
||||
.finish(),
|
||||
ConstrainedBox::new(
|
||||
Text::new(
|
||||
"HELLO WORLD 2",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_width(400.)
|
||||
.with_height(400.)
|
||||
.finish(),
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
"HELLO WORLD 3",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_vertical_margin(10.)
|
||||
.finish(),
|
||||
])
|
||||
.finish(),
|
||||
)
|
||||
.with_horizontal_margin(10.)
|
||||
.with_uniform_padding(10.)
|
||||
.with_border(
|
||||
Border::all(1.0).with_border_fill(ColorU::new(
|
||||
255, 194, 255, 255,
|
||||
)),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
"HELLO WORLD 4",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_vertical_margin(10.)
|
||||
.finish(),
|
||||
ConstrainedBox::new(
|
||||
Text::new(
|
||||
"HELLO WORLD 5",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
"HELLO WORLD 6",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_vertical_margin(10.)
|
||||
.finish(),
|
||||
])
|
||||
.finish(),
|
||||
)
|
||||
.with_horizontal_margin(10.)
|
||||
.with_uniform_padding(10.)
|
||||
.with_border(
|
||||
Border::all(1.0).with_border_fill(ColorU::new(
|
||||
255, 194, 255, 255,
|
||||
)),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_uniform_padding(100.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
SelectableArea::new(
|
||||
self.selectable_area_state_handle_2.clone(),
|
||||
|selection_args, _, _| {
|
||||
println!("SELECTED TEXT - {:?}", selection_args.selection);
|
||||
},
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
"HELLO WORLD 11",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_vertical_margin(10.)
|
||||
.finish(),
|
||||
ConstrainedBox::new(
|
||||
Text::new(
|
||||
"HELLO WORLD 22",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
"HELLO WORLD 33",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_vertical_margin(10.)
|
||||
.finish(),
|
||||
])
|
||||
.finish(),
|
||||
)
|
||||
.with_horizontal_margin(10.)
|
||||
.with_uniform_padding(10.)
|
||||
.with_border(
|
||||
Border::all(1.0).with_border_fill(ColorU::new(
|
||||
255, 194, 255, 255,
|
||||
)),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
"HELLO WORLD 44",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_vertical_margin(10.)
|
||||
.finish(),
|
||||
ConstrainedBox::new(
|
||||
Text::new(
|
||||
"HELLO 👀👀👀 WORLD 55",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_width(400.)
|
||||
.with_height(400.)
|
||||
.finish(),
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
"HELLO WORLD 66 👀",
|
||||
self.font_family,
|
||||
16.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_vertical_margin(10.)
|
||||
.finish(),
|
||||
])
|
||||
.finish(),
|
||||
)
|
||||
.with_horizontal_margin(10.)
|
||||
.with_uniform_padding(10.)
|
||||
.with_border(
|
||||
Border::all(1.0).with_border_fill(ColorU::new(
|
||||
255, 194, 255, 255,
|
||||
)),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_uniform_margin(100.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(galaxyui::AddWindowOptions::default(), |_| root_view::RootView);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use galaxyui::elements::{
|
||||
Align, ConstrainedBox, Container, CornerRadius, DropShadow, Radius, Shrinkable,
|
||||
};
|
||||
use galaxyui::{
|
||||
elements::{Flex, ParentElement, Rect},
|
||||
AppContext, Element, Entity, TypedActionView, View,
|
||||
};
|
||||
|
||||
use galaxyui::color::ColorU;
|
||||
|
||||
pub struct RootView;
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
fn rect_with_shadow(shadow: DropShadow, corner_radius: CornerRadius) -> Box<dyn Element> {
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(255, 255, 255, 255))
|
||||
.with_corner_radius(corner_radius)
|
||||
.with_drop_shadow(shadow)
|
||||
.finish(),
|
||||
)
|
||||
.with_width(200.)
|
||||
.with_height(100.)
|
||||
.finish(),
|
||||
)
|
||||
.with_uniform_margin(30.)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
Align::new(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
rect_with_shadow(
|
||||
DropShadow {
|
||||
color: ColorU::black(),
|
||||
offset: vec2f(0., 10.),
|
||||
blur_radius: 10.,
|
||||
spread_radius: 30.,
|
||||
},
|
||||
CornerRadius::default(),
|
||||
),
|
||||
rect_with_shadow(
|
||||
DropShadow {
|
||||
color: ColorU::new(255, 0, 0, 255),
|
||||
offset: vec2f(10., 10.),
|
||||
blur_radius: 5.,
|
||||
spread_radius: 20.,
|
||||
},
|
||||
CornerRadius::with_all(Radius::Pixels(8.)),
|
||||
),
|
||||
rect_with_shadow(
|
||||
DropShadow {
|
||||
color: ColorU::new(0, 255, 0, 255),
|
||||
offset: vec2f(-10., -20.),
|
||||
blur_radius: 20.,
|
||||
spread_radius: 10.,
|
||||
},
|
||||
CornerRadius::with_all(Radius::Percentage(30.)),
|
||||
),
|
||||
rect_with_shadow(
|
||||
DropShadow {
|
||||
color: ColorU::new(0, 0, 255, 255),
|
||||
offset: vec2f(30., 0.),
|
||||
blur_radius: 30.,
|
||||
spread_radius: 40.,
|
||||
},
|
||||
CornerRadius::with_right(Radius::Pixels(40.)),
|
||||
),
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::white())
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.)))
|
||||
.with_drop_shadow(DropShadow {
|
||||
color: ColorU::black(),
|
||||
offset: vec2f(-0.5, 2.),
|
||||
blur_radius: 20.,
|
||||
spread_radius: 0.,
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.with_width(30.)
|
||||
.with_height(30.)
|
||||
.finish(),
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::white())
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(10.)))
|
||||
.with_drop_shadow(DropShadow {
|
||||
color: ColorU::black(),
|
||||
offset: vec2f(50.5, 2.),
|
||||
blur_radius: 2.,
|
||||
spread_radius: 0.,
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.with_width(30.)
|
||||
.with_height(30.)
|
||||
.finish(),
|
||||
])
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(ColorU::new(128, 128, 128, 255))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use galaxyui::{
|
||||
elements::{Align, Container},
|
||||
presenter::ChildView,
|
||||
ui_components::{
|
||||
components::{UiComponent, UiComponentStyles},
|
||||
slider::{Slider, SliderStateHandle},
|
||||
},
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
/// Renders a center-aligned slider component against a black background. When the slider is
|
||||
/// dragged, the updated value is printed to stdout.
|
||||
#[derive(Default)]
|
||||
pub struct SliderExample {
|
||||
slider_state: SliderStateHandle,
|
||||
}
|
||||
|
||||
impl SliderExample {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
slider_state: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View for SliderExample {
|
||||
fn ui_name() -> &'static str {
|
||||
"SliderExample"
|
||||
}
|
||||
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
Slider::new(self.slider_state.clone())
|
||||
.on_drag(|event_ctx, _app, new_value| {
|
||||
event_ctx.dispatch_typed_action(SliderExampleAction::OnSliderDrag(new_value))
|
||||
})
|
||||
.on_change(|event_ctx, _app, new_value| {
|
||||
event_ctx.dispatch_typed_action(SliderExampleAction::OnSliderValueChange(new_value))
|
||||
})
|
||||
// Set a custom value range.
|
||||
.with_range(0.0..100.)
|
||||
.with_style(UiComponentStyles {
|
||||
width: Some(400.),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for SliderExample {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SliderExampleAction {
|
||||
OnSliderDrag(f32),
|
||||
OnSliderValueChange(f32),
|
||||
}
|
||||
|
||||
impl TypedActionView for SliderExample {
|
||||
type Action = SliderExampleAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
SliderExampleAction::OnSliderDrag(new_value) => {
|
||||
println!("Slider dragged: {new_value:?}");
|
||||
ctx.notify();
|
||||
}
|
||||
SliderExampleAction::OnSliderValueChange(new_value) => {
|
||||
println!("Slider dropped: {new_value:?}");
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a wrapper view so [`SliderExample`] can be added as a [`TypedActionView`].
|
||||
pub struct RootView {
|
||||
slider_example_view: ViewHandle<SliderExample>,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let slider_example_view = ctx.add_typed_action_view(|_| SliderExample::new());
|
||||
Self {
|
||||
slider_example_view,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
fn render(&self, _app: &AppContext) -> Box<dyn Element> {
|
||||
Container::new(Align::new(ChildView::new(&self.slider_example_view).finish()).finish())
|
||||
.with_background_color(ColorU::black())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Don't include output or screenshot directories from testing.
|
||||
output/
|
||||
screenshots/
|
||||
@@ -0,0 +1,25 @@
|
||||
# WARP.md
|
||||
|
||||
## Visual Sanity Check for Table Example Screenshots
|
||||
This project can auto-generate screenshots of the table example demos and then sanity check them using computer vision. The goal is to quickly catch apparent rendering bugs (e.g., empty cells, obvious misalignment, missing headers) before committing or opening a PR.
|
||||
|
||||
### How to capture images
|
||||
- Build and run the example with capture flags:
|
||||
- Baseline (reference images): `../../../../target/debug/examples/table-sample --capture-baseline`
|
||||
- Current (to compare locally): `../../../../target/debug/examples/table-sample --capture-screenshots`
|
||||
- Output directories:
|
||||
- Baseline: `screenshots/baseline/`
|
||||
- Current: `screenshots/current/`
|
||||
|
||||
### Sanity-check protocol (Agent/Agent Mode)
|
||||
- Use the read_file tool to upload all PNGs in the chosen directory (baseline or current).
|
||||
- For each image, scan for:
|
||||
- Completely blank/black/solid-color large areas where UI should be rendered
|
||||
- Obvious missing headers, rows, or columns
|
||||
- Clearly misaligned row bands or headers vs. body
|
||||
- Text clipped mid-line or unreadable due to extreme contrast issues
|
||||
- Report any images that exhibit the above, with a short note.
|
||||
|
||||
Notes:
|
||||
- This is a quick visual smoke test, not a pixel-perfect comparison.
|
||||
- If a failure is found, re-run the example for a single demo by navigating with arrow keys or by re-running the full capture and re-checking.
|
||||
@@ -0,0 +1,95 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, platform::WindowBounds, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CaptureConfig {
|
||||
pub capture_screenshots: bool,
|
||||
pub capture_baseline: bool,
|
||||
}
|
||||
|
||||
fn parse_args() -> CaptureConfig {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut config = CaptureConfig::default();
|
||||
|
||||
for arg in args.iter() {
|
||||
match arg.as_str() {
|
||||
"--capture-screenshots" => config.capture_screenshots = true,
|
||||
"--capture-baseline" => {
|
||||
config.capture_screenshots = true;
|
||||
config.capture_baseline = true;
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
println!("Table Sample Example - Screenshot Testing");
|
||||
println!("\nUsage: table-sample [OPTIONS]");
|
||||
println!("\nOptions:");
|
||||
println!(" --capture-screenshots Capture screenshots of all demos");
|
||||
println!(" --capture-baseline Capture and save as baseline screenshots");
|
||||
println!(" --help, -h Show this help message");
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
env_logger::builder().format_timestamp_millis().init();
|
||||
let capture_config = parse_args();
|
||||
|
||||
if capture_config.capture_screenshots {
|
||||
println!("📸 Screenshot capture mode enabled");
|
||||
if capture_config.capture_baseline {
|
||||
println!("📁 Baseline mode: screenshots will be saved as reference images");
|
||||
}
|
||||
}
|
||||
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
root_view::init(ctx);
|
||||
let window_options = galaxyui::AddWindowOptions {
|
||||
window_bounds: WindowBounds::ExactSize(vec2f(1000.0, 800.0)),
|
||||
window_style: if capture_config.capture_screenshots {
|
||||
galaxyui::platform::WindowStyle::NotStealFocus
|
||||
} else {
|
||||
galaxyui::platform::WindowStyle::Normal
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let config = capture_config.clone();
|
||||
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
|
||||
let (window_id, _root) = ctx.add_window(window_options, move |view_ctx| {
|
||||
root_view::RootView::new(view_ctx, config)
|
||||
});
|
||||
#[cfg(target_os = "macos")]
|
||||
if capture_config.capture_screenshots {
|
||||
// Make it visible for rendering but keep z-index
|
||||
ctx.windows()
|
||||
.show_window_and_focus_app_without_ordering_front(window_id);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
pub mod root_view;
|
||||
|
||||
extern crate galaxyui;
|
||||
use rust_embed::RustEmbed;
|
||||
use galaxyui::{platform, AssetProvider};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "examples/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
// The static assets we need to load in app.
|
||||
pub static ASSETS: Assets = Assets;
|
||||
|
||||
// Implement the AssetProvider trait here (required by App::new).
|
||||
impl AssetProvider for Assets {
|
||||
fn get(&self, path: &str) -> Result<Cow<'_, [u8]>> {
|
||||
<Assets as RustEmbed>::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("no asset exists at path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
// Create a view where we could use keybindings to change its state.
|
||||
// In this example, we are going to have two simple bindings:
|
||||
// 1. Cmd-Enter to toggle showing / hiding a solid red rect
|
||||
// 2. Enter to toggle showing / hiding some texts
|
||||
fn main() -> Result<()> {
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
root_view::init(ctx);
|
||||
ctx.add_window(
|
||||
galaxyui::AddWindowOptions::default(),
|
||||
root_view::RootView::new,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::SingletonEntity as _;
|
||||
use galaxyui::{
|
||||
elements::{Align, ConstrainedBox, ParentElement, Rect, Stack, Text},
|
||||
keymap::FixedBinding,
|
||||
presenter::ChildView,
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
// We could initiate global action and bindings here.
|
||||
pub fn init(ctx: &mut AppContext) {
|
||||
use galaxyui::keymap::macros::*;
|
||||
|
||||
// Add bindings to trigger actions in the subview.
|
||||
ctx.register_fixed_bindings([
|
||||
FixedBinding::new(
|
||||
"cmdorctrl-enter",
|
||||
SubViewAction::ToggleRedRect,
|
||||
id!("SubView"),
|
||||
),
|
||||
FixedBinding::new("enter", SubViewAction::ToggleText, id!("SubView")),
|
||||
]);
|
||||
}
|
||||
|
||||
pub struct RootView {
|
||||
// RootView "owns" a viewhandle to the subview.
|
||||
sub_view: ViewHandle<SubView>,
|
||||
}
|
||||
|
||||
impl RootView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
// Adding typed action view allows the view to receive keydown events.
|
||||
let sub_view = ctx.add_typed_action_view(|ctx| {
|
||||
let menlo = galaxyui::fonts::Cache::handle(ctx).update(ctx, |cache, _| {
|
||||
cache.load_system_font("Menlo").expect("Should load Menlo")
|
||||
});
|
||||
let view = SubView {
|
||||
display_red_rect: true,
|
||||
display_text: true,
|
||||
menlo_font_family: menlo,
|
||||
};
|
||||
// Need the view to be focused for keydown actions to be dispatched to it.
|
||||
ctx.focus_self();
|
||||
view
|
||||
});
|
||||
Self { sub_view }
|
||||
}
|
||||
}
|
||||
|
||||
// Implement the entity trait.
|
||||
impl Entity for RootView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
// Implement the view trait so RootView could be considered as a view.
|
||||
impl View for RootView {
|
||||
fn ui_name() -> &'static str {
|
||||
"RootView"
|
||||
}
|
||||
|
||||
// Renders the child view of sub_view.
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
ChildView::new(&self.sub_view).finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SubViewAction {
|
||||
ToggleRedRect,
|
||||
ToggleText,
|
||||
}
|
||||
|
||||
pub struct SubView {
|
||||
display_red_rect: bool,
|
||||
display_text: bool,
|
||||
menlo_font_family: FamilyId,
|
||||
}
|
||||
|
||||
// Implement the entity trait.
|
||||
impl Entity for SubView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
// Implement the view trait so SubView could be considered as a view.
|
||||
impl View for SubView {
|
||||
fn ui_name() -> &'static str {
|
||||
"SubView"
|
||||
}
|
||||
|
||||
// Renders a stack of a centered solid red box and some text on top.
|
||||
fn render(&self, _: &AppContext) -> Box<dyn Element> {
|
||||
// Half transparent black background.
|
||||
let mut stack = Stack::new().with_child(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(0, 0, 0, 150))
|
||||
.finish(),
|
||||
);
|
||||
|
||||
// If flag is true, display a solid red box.
|
||||
if self.display_red_rect {
|
||||
stack.add_child(
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(ColorU::new(255, 0, 0, 255))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(300.)
|
||||
.with_height(200.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
// If flag is true, display some texts.
|
||||
if self.display_text {
|
||||
stack.add_child(
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Text::new_inline(
|
||||
"This is some text for testing",
|
||||
self.menlo_font_family,
|
||||
12.,
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_width(250.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
};
|
||||
|
||||
stack.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for SubView {
|
||||
type Action = SubViewAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
SubViewAction::ToggleRedRect => self.display_red_rect = !self.display_red_rect,
|
||||
SubViewAction::ToggleText => self.display_text = !self.display_text,
|
||||
};
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for RootView {
|
||||
type Action = ();
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
//! A text rasterizer backed by [`font_kit`] that supports rasterizing at subpixel offsets
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use dashmap::DashMap;
|
||||
use font_kit::canvas::{AntialiasingStrategy, Canvas, RasterizationOptions};
|
||||
use font_kit::font::Font;
|
||||
use font_kit::hinting::HintingOptions;
|
||||
use pathfinder_geometry::rect::RectI;
|
||||
use pathfinder_geometry::transform2d::Transform2F;
|
||||
use pathfinder_geometry::vector::{vec2i, Vector2F, Vector2I};
|
||||
use galaxyui_core::fonts::canvas::RasterFormat;
|
||||
use galaxyui_core::fonts::{
|
||||
FontId, GlyphId, Properties, RasterizedGlyph, Style, SubpixelAlignment, Weight,
|
||||
};
|
||||
use galaxyui_core::rendering;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::platform::mac::AutoreleasePoolGuard;
|
||||
|
||||
/// A simpler rasterizer backed by font-kit.
|
||||
pub(crate) struct Rasterizer {
|
||||
fonts: DashMap<FontId, Arc<Font>>,
|
||||
}
|
||||
|
||||
impl Rasterizer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
fonts: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&self, font_id: FontId, font: Arc<Font>) {
|
||||
self.fonts.insert(font_id, font);
|
||||
}
|
||||
|
||||
pub fn font_for_id(&self, font_id: FontId) -> Arc<Font> {
|
||||
self.fonts.get(&font_id).expect("Font must exist").clone()
|
||||
}
|
||||
|
||||
pub fn glyph_raster_bounds(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
point_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
scale: Vector2F,
|
||||
glyph_config: &rendering::GlyphConfig,
|
||||
) -> Result<RectI> {
|
||||
let raw_raster_bounds = self.font_for_id(font_id).raster_bounds(
|
||||
glyph_id,
|
||||
point_size,
|
||||
Transform2F::from_scale(scale),
|
||||
HintingOptions::None,
|
||||
RasterizationOptions {
|
||||
antialiasing_strategy: AntialiasingStrategy::GrayscaleAa,
|
||||
use_thin_strokes: glyph_config
|
||||
.use_thin_strokes
|
||||
.enabled_for_scale_factor(scale.x()),
|
||||
},
|
||||
)?;
|
||||
if raw_raster_bounds.size() == Vector2I::zero() {
|
||||
// Don't adjust the size of a glyph with a default size of zero.
|
||||
return Ok(raw_raster_bounds);
|
||||
}
|
||||
// The default raster bounds provided by font-kit sometimes clip pixels
|
||||
// off of anti-aliased glyphs; add one pixel to the glyph bounds to
|
||||
// compensate. We only adjust the origin vertically because the extra
|
||||
// pixel of height changes the baseline; the extra pixel on the right
|
||||
// side doesn't change positioning (as the origin is on the left edge of
|
||||
// the glyph).
|
||||
let fudge_factor = vec2i(1, 1);
|
||||
let offset = vec2i(0, 1);
|
||||
Ok(RectI::new(
|
||||
raw_raster_bounds.origin() - offset,
|
||||
raw_raster_bounds.size() + fudge_factor,
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn rasterize_glyph(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
point_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
scale: Vector2F,
|
||||
subpixel_alignment: SubpixelAlignment,
|
||||
glyph_config: &rendering::GlyphConfig,
|
||||
format: RasterFormat,
|
||||
) -> Result<RasterizedGlyph> {
|
||||
// On macOS, this function calls into Core Graphics and Core Text
|
||||
// (`CGBitmapContextCreate` per glyph plus `raster_bounds` reading font
|
||||
// metadata), each of which leaves transient bookkeeping objects in the
|
||||
// thread's autorelease pool. Because this is invoked during Metal
|
||||
// frame rendering on the main thread, hundreds of those objects can
|
||||
// accumulate between run-loop turns before AppKit's outer pool
|
||||
// drains. A local pool bounds that peak without relying on the outer
|
||||
// pool. The guard drains on `Drop`, covering the error paths from `?`
|
||||
// below and any panics from `font_kit`.
|
||||
#[cfg(target_os = "macos")]
|
||||
let _pool = AutoreleasePoolGuard::new();
|
||||
|
||||
let bounds =
|
||||
self.glyph_raster_bounds(font_id, point_size, glyph_id, scale, glyph_config)?;
|
||||
let mut canvas = Canvas::new(bounds.size(), raster_format_to_font_kit(format));
|
||||
|
||||
let base_transform = Transform2F::from_scale(scale).translate(-bounds.origin().to_f32());
|
||||
let aligned_transform = base_transform.translate(subpixel_alignment.to_offset());
|
||||
|
||||
self.font_for_id(font_id).rasterize_glyph(
|
||||
&mut canvas,
|
||||
glyph_id,
|
||||
point_size,
|
||||
aligned_transform,
|
||||
HintingOptions::None,
|
||||
RasterizationOptions {
|
||||
antialiasing_strategy: AntialiasingStrategy::GrayscaleAa,
|
||||
use_thin_strokes: glyph_config
|
||||
.use_thin_strokes
|
||||
.enabled_for_scale_factor(scale.x()),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(RasterizedGlyph {
|
||||
canvas: canvas.into(),
|
||||
// TODO(alokedesai): Properly support colored glyphs on Windows.
|
||||
is_emoji: self.font_for_id(font_id).is_colored() && !cfg!(windows),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn properties_to_font_kit(properties: Properties) -> font_kit::properties::Properties {
|
||||
font_kit::properties::Properties {
|
||||
style: style_to_font_kit(properties.style),
|
||||
weight: weight_to_font_kit(properties.weight),
|
||||
stretch: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn raster_format_to_font_kit(format: RasterFormat) -> font_kit::canvas::Format {
|
||||
use font_kit::canvas::Format as FKFormat;
|
||||
match format {
|
||||
RasterFormat::Rgba32 => FKFormat::Rgba32,
|
||||
RasterFormat::Rgb24 => FKFormat::Rgb24,
|
||||
RasterFormat::A8 => FKFormat::Rgb24,
|
||||
}
|
||||
}
|
||||
|
||||
fn weight_to_font_kit(weight: Weight) -> font_kit::properties::Weight {
|
||||
match weight {
|
||||
Weight::Thin => font_kit::properties::Weight::THIN,
|
||||
Weight::ExtraLight => font_kit::properties::Weight::EXTRA_LIGHT,
|
||||
Weight::Light => font_kit::properties::Weight::LIGHT,
|
||||
Weight::Normal => font_kit::properties::Weight::NORMAL,
|
||||
Weight::Medium => font_kit::properties::Weight::MEDIUM,
|
||||
Weight::Semibold => font_kit::properties::Weight::SEMIBOLD,
|
||||
Weight::Bold => font_kit::properties::Weight::BOLD,
|
||||
Weight::ExtraBold => font_kit::properties::Weight::EXTRA_BOLD,
|
||||
Weight::Black => font_kit::properties::Weight::BLACK,
|
||||
}
|
||||
}
|
||||
|
||||
fn style_to_font_kit(value: Style) -> font_kit::properties::Style {
|
||||
match value {
|
||||
Style::Normal => font_kit::properties::Style::Normal,
|
||||
Style::Italic => font_kit::properties::Style::Italic,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#[cfg(native)]
|
||||
#[cfg_attr(not(macos), allow(dead_code))]
|
||||
pub mod font_kit;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "text_layout_test.rs"]
|
||||
mod text_layout_tests;
|
||||
|
||||
pub use galaxyui_core::fonts::*;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use text_layout_tests::{collect_glyph_indices, init_fonts};
|
||||
|
||||
#[cfg(all(test, target_os = "macos"))]
|
||||
pub(crate) use text_layout_tests::collect_line_caret_position_starts;
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod fonts;
|
||||
pub mod platform;
|
||||
pub mod rendering;
|
||||
pub mod windowing;
|
||||
|
||||
// Re-export everything from the core crate.
|
||||
pub use galaxyui_core::*;
|
||||
@@ -0,0 +1,146 @@
|
||||
use galaxyui_core::{
|
||||
integration::TestDriver,
|
||||
keymap::{CustomTag, Keystroke},
|
||||
r#async::LocalBoxFuture,
|
||||
AppContext, AssetProvider,
|
||||
};
|
||||
|
||||
pub use galaxyui_core::platform::app::*;
|
||||
|
||||
use super::AsInnerMut;
|
||||
|
||||
/// Platform-specific app implementation. On any given platform, there are at least two possible
|
||||
/// implementations:
|
||||
/// * The platform-native backend (e.g. Cocoa on macOS, or Winit+X11/Wayland on Linux)
|
||||
/// * A headless backend
|
||||
pub enum AppBackend {
|
||||
CurrentPlatform(Box<super::current::App>),
|
||||
Headless(Box<super::headless::App>),
|
||||
}
|
||||
|
||||
impl AppBackend {
|
||||
fn run(
|
||||
self,
|
||||
init_fn: impl FnOnce(&mut AppContext, LocalBoxFuture<'static, crate::App>) + 'static,
|
||||
) -> TerminationResult {
|
||||
match self {
|
||||
AppBackend::CurrentPlatform(inner) => {
|
||||
inner.run(init_fn);
|
||||
// We don't report errors for the GUI app on termination.
|
||||
Ok(())
|
||||
}
|
||||
AppBackend::Headless(inner) => inner.run(init_fn),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure to help us construct and start the application.
|
||||
pub struct AppBuilder {
|
||||
/// The actual platform-specific implementation of the app. This
|
||||
/// stores a strong reference to the application state and the
|
||||
/// callback functions to invoke when things occur at the platform
|
||||
/// level.
|
||||
inner: AppBackend,
|
||||
test_driver: Option<TestDriver>,
|
||||
custom_tag_to_keystroke_fn: Option<Box<dyn Fn(CustomTag) -> Option<Keystroke> + 'static>>,
|
||||
default_keystroke_trigger_for_custom_actions:
|
||||
Option<Box<dyn Fn(CustomTag) -> Option<Keystroke> + 'static>>,
|
||||
}
|
||||
|
||||
impl AppBuilder {
|
||||
/// Constructs a new application using the current platform backend.
|
||||
pub fn new(
|
||||
callbacks: AppCallbacks,
|
||||
assets: Box<dyn AssetProvider>,
|
||||
test_driver: Option<TestDriver>,
|
||||
) -> Self {
|
||||
let inner = super::current::App::new(callbacks, assets, test_driver.as_ref());
|
||||
|
||||
Self {
|
||||
inner: AppBackend::CurrentPlatform(Box::new(inner)),
|
||||
test_driver,
|
||||
custom_tag_to_keystroke_fn: None,
|
||||
default_keystroke_trigger_for_custom_actions: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new application using the headless backend.
|
||||
pub fn new_headless(
|
||||
callbacks: AppCallbacks,
|
||||
assets: Box<dyn AssetProvider>,
|
||||
test_driver: Option<TestDriver>,
|
||||
) -> Self {
|
||||
let inner = super::headless::App::new(callbacks, assets, test_driver.as_ref());
|
||||
Self {
|
||||
inner: AppBackend::Headless(Box::new(inner)),
|
||||
test_driver,
|
||||
custom_tag_to_keystroke_fn: None,
|
||||
default_keystroke_trigger_for_custom_actions: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts any [`crate::keymap::Trigger::Custom`]-based binding to a traditional
|
||||
/// [`Keystroke`]-based binding using the provided `custom_tag_to_keystroke` function.
|
||||
///
|
||||
/// This can be useful in the cases where an application registers a binding with a
|
||||
/// [`crate::keymap::Trigger::Custom`] for use in a Mac menu, but still wants to to register the
|
||||
/// binding with its corresponding `Keystroke` on other platforms that don't support menus.
|
||||
pub fn convert_custom_triggers_to_keystroke_triggers(
|
||||
&mut self,
|
||||
custom_tag_to_keystroke: impl Fn(CustomTag) -> Option<Keystroke> + 'static,
|
||||
) {
|
||||
self.custom_tag_to_keystroke_fn = Some(Box::new(custom_tag_to_keystroke));
|
||||
}
|
||||
|
||||
/// Registers a lookup function that returns the default keystroke for a given custom action.
|
||||
/// Used when converting custom actions to key events during keybinding editing.
|
||||
pub fn register_default_keystroke_triggers_for_custom_actions(
|
||||
&mut self,
|
||||
custom_tag_to_keystroke: impl Fn(CustomTag) -> Option<Keystroke> + 'static,
|
||||
) {
|
||||
self.default_keystroke_trigger_for_custom_actions = Some(Box::new(custom_tag_to_keystroke));
|
||||
}
|
||||
|
||||
/// Runs the application, invoking the provided function to
|
||||
/// initialize application state and be ready to process
|
||||
/// events from the main event loop.
|
||||
pub fn run(mut self, init_fn: impl FnOnce(&mut AppContext) + 'static) -> TerminationResult {
|
||||
let custom_tag_to_keystroke = self.custom_tag_to_keystroke_fn.take();
|
||||
// Wrap the initialization fn with one that first tries to convert custom triggers to
|
||||
// keystroke triggers.
|
||||
let init_fn = |ctx: &mut AppContext| {
|
||||
if let Some(custom_tag_to_keystroke) = custom_tag_to_keystroke {
|
||||
ctx.convert_custom_triggers_to_keystroke_triggers(custom_tag_to_keystroke);
|
||||
}
|
||||
if let Some(default_keystroke_trigger_for_custom_actions) =
|
||||
self.default_keystroke_trigger_for_custom_actions
|
||||
{
|
||||
ctx.register_default_keystroke_triggers_for_custom_actions(
|
||||
default_keystroke_trigger_for_custom_actions,
|
||||
);
|
||||
}
|
||||
init_fn(ctx);
|
||||
};
|
||||
|
||||
if let Some(test_driver) = self.test_driver {
|
||||
self.inner.run(move |ctx, ui_app_future| {
|
||||
init_fn(ctx);
|
||||
|
||||
ctx.foreground_executor()
|
||||
.spawn(async move {
|
||||
let ui_app = ui_app_future.await;
|
||||
test_driver.run_test_and_cleanup(ui_app).await;
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
} else {
|
||||
self.inner.run(|ctx, _| init_fn(ctx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsInnerMut<AppBackend> for AppBuilder {
|
||||
fn as_inner_mut(&mut self) -> &mut AppBackend {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use futures::future::LocalBoxFuture;
|
||||
|
||||
use crate::platform::app::TerminationResult;
|
||||
use crate::platform::test::FontDB as TestFontDB;
|
||||
use crate::{
|
||||
integration::TestDriver,
|
||||
platform::{self},
|
||||
AppContext, AssetProvider,
|
||||
};
|
||||
|
||||
use super::delegate::{self, AppDelegate};
|
||||
use super::event_loop::{self, AppEvent};
|
||||
use super::windowing::WindowManager;
|
||||
use std::sync::mpsc;
|
||||
|
||||
pub struct App {
|
||||
callbacks: platform::app::AppCallbacks,
|
||||
assets: Box<dyn AssetProvider>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(in crate::platform) fn new(
|
||||
callbacks: platform::app::AppCallbacks,
|
||||
assets: Box<dyn AssetProvider>,
|
||||
test_driver: Option<&TestDriver>,
|
||||
) -> Self {
|
||||
// Other platforms use the test_driver parameter to enable an alternative platform delegate implementation
|
||||
// in integration tests - that doesn't apply here.
|
||||
let _ = test_driver;
|
||||
Self { callbacks, assets }
|
||||
}
|
||||
|
||||
pub(in crate::platform) fn run(
|
||||
self,
|
||||
init_fn: impl FnOnce(&mut AppContext, LocalBoxFuture<'static, crate::App>) + 'static,
|
||||
) -> TerminationResult {
|
||||
let App { callbacks, assets } = self;
|
||||
|
||||
let (sender, receiver) = mpsc::channel::<AppEvent>();
|
||||
|
||||
// Mark this thread as the main thread for DispatchDelegate checks.
|
||||
delegate::mark_current_thread_as_main();
|
||||
|
||||
let platform_delegate = Box::new(AppDelegate::new(sender.clone()));
|
||||
let window_manager = Box::new(WindowManager::new(sender.clone()));
|
||||
// Reuse the testing FontDB implementation, as no font features are needed in headless mode.
|
||||
let font_db: Box<dyn platform::FontDB> = Box::new(TestFontDB::new());
|
||||
|
||||
let ui_app = crate::App::new(platform_delegate, window_manager, font_db, assets)
|
||||
.expect("should not fail to construct application");
|
||||
|
||||
let mut callbacks =
|
||||
galaxyui_core::platform::app::AppCallbackDispatcher::new(callbacks, ui_app.clone());
|
||||
|
||||
// Run the event loop until the app terminates.
|
||||
event_loop::run(ui_app, &mut callbacks, Box::new(init_fn), receiver, sender)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{
|
||||
clipboard::InMemoryClipboard,
|
||||
notification::{NotificationSendError, RequestPermissionsOutcome},
|
||||
platform::{self, Cursor},
|
||||
};
|
||||
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::thread;
|
||||
|
||||
use super::event_loop::AppEvent;
|
||||
|
||||
/// Stores the ID of the application's main thread, which we can reference
|
||||
/// to determine if a given thread is the main thread or not.
|
||||
static MAIN_THREAD_ID: OnceLock<thread::ThreadId> = OnceLock::new();
|
||||
|
||||
/// Marks the current thread as the application's main thread.
|
||||
///
|
||||
/// Panics if called more than once.
|
||||
pub(super) fn mark_current_thread_as_main() {
|
||||
MAIN_THREAD_ID
|
||||
.set(thread::current().id())
|
||||
.expect("should only call mark_current_thread_as_main once!");
|
||||
}
|
||||
|
||||
pub struct AppDelegate {
|
||||
clipboard: InMemoryClipboard,
|
||||
cursor_shape: Mutex<Cursor>,
|
||||
event_sender: Sender<AppEvent>,
|
||||
}
|
||||
|
||||
impl AppDelegate {
|
||||
pub(super) fn new(event_sender: Sender<AppEvent>) -> Self {
|
||||
Self {
|
||||
clipboard: InMemoryClipboard::default(),
|
||||
cursor_shape: Mutex::new(Cursor::Arrow),
|
||||
event_sender,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_event(&self, event: AppEvent) {
|
||||
if self.event_sender.send(event).is_err() {
|
||||
log::warn!("Tried to send event, but event loop is no longer running");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::Delegate for AppDelegate {
|
||||
fn dispatch_delegate(&self) -> Arc<dyn platform::DispatchDelegate> {
|
||||
Arc::new(DispatchDelegate {
|
||||
event_sender: self.event_sender.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn request_user_attention(&self, _window_id: crate::WindowId) {
|
||||
// Unsupported.
|
||||
}
|
||||
|
||||
fn clipboard(&mut self) -> &mut dyn crate::Clipboard {
|
||||
&mut self.clipboard
|
||||
}
|
||||
|
||||
fn system_theme(&self) -> platform::SystemTheme {
|
||||
platform::SystemTheme::Light
|
||||
}
|
||||
|
||||
fn open_url(&self, url: &str) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Use macOS platform implementation
|
||||
crate::platform::mac::Window::open_url(url);
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
// Reuse the winit implementation for non-mac platforms
|
||||
crate::windowing::winit::delegate::open_url_in_system(url);
|
||||
}
|
||||
}
|
||||
|
||||
fn open_file_path(&self, _path: &std::path::Path) {
|
||||
// Unsupported.
|
||||
}
|
||||
|
||||
fn open_file_path_in_explorer(&self, _path: &std::path::Path) {
|
||||
// Unsupported.
|
||||
}
|
||||
|
||||
fn open_file_picker(
|
||||
&self,
|
||||
callback: platform::FilePickerCallback,
|
||||
_file_picker_config: platform::FilePickerConfiguration,
|
||||
) {
|
||||
self.send_event(AppEvent::RunCallback(Box::new(move |ctx| {
|
||||
callback(Ok(vec![]), ctx);
|
||||
})));
|
||||
}
|
||||
|
||||
fn open_save_file_picker(
|
||||
&self,
|
||||
callback: platform::SaveFilePickerCallback,
|
||||
_config: platform::SaveFilePickerConfiguration,
|
||||
) {
|
||||
self.send_event(AppEvent::RunCallback(Box::new(move |ctx| {
|
||||
callback(None, ctx);
|
||||
})));
|
||||
}
|
||||
|
||||
fn application_bundle_info(
|
||||
&self,
|
||||
_bundle_identifier: &str,
|
||||
) -> Option<crate::ApplicationBundleInfo<'_>> {
|
||||
// This is unsupported, though we could delegate to the macOS implementation.
|
||||
None
|
||||
}
|
||||
|
||||
fn show_native_platform_modal(
|
||||
&self,
|
||||
_id: crate::modals::ModalId,
|
||||
_modal: crate::modals::AlertDialog,
|
||||
) {
|
||||
// Unsupported.
|
||||
}
|
||||
|
||||
fn request_desktop_notification_permissions(
|
||||
&self,
|
||||
on_completion: platform::RequestNotificationPermissionsCallback,
|
||||
) {
|
||||
self.send_event(AppEvent::RunCallback(Box::new(move |ctx| {
|
||||
on_completion(RequestPermissionsOutcome::PermissionsDenied, ctx);
|
||||
})));
|
||||
}
|
||||
|
||||
fn send_desktop_notification(
|
||||
&self,
|
||||
_notification_content: crate::notification::UserNotification,
|
||||
_window_id: crate::WindowId,
|
||||
on_error: platform::SendNotificationErrorCallback,
|
||||
) {
|
||||
self.send_event(AppEvent::RunCallback(Box::new(move |ctx| {
|
||||
on_error(NotificationSendError::PermissionsDenied, ctx);
|
||||
})));
|
||||
}
|
||||
|
||||
fn set_cursor_shape(&self, cursor: Cursor) {
|
||||
*self.cursor_shape.lock() = cursor;
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-util")]
|
||||
fn get_cursor_shape(&self) -> Cursor {
|
||||
*self.cursor_shape.lock()
|
||||
}
|
||||
|
||||
fn close_ime_async(&self, _window_id: crate::WindowId) {
|
||||
// Unsupported.
|
||||
}
|
||||
|
||||
fn is_ime_open(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn open_character_palette(&self) {
|
||||
// Unsupported.
|
||||
}
|
||||
|
||||
fn set_accessibility_contents(&self, _content: crate::accessibility::AccessibilityContent) {
|
||||
// Unsupported.
|
||||
}
|
||||
|
||||
fn register_global_shortcut(&self, _shortcut: crate::keymap::Keystroke) {
|
||||
// Unsupported.
|
||||
}
|
||||
|
||||
fn unregister_global_shortcut(&self, _shortcut: &crate::keymap::Keystroke) {
|
||||
// Unsupported.
|
||||
}
|
||||
|
||||
fn terminate_app(&self, termination_mode: platform::TerminationMode) {
|
||||
self.send_event(AppEvent::Terminate(termination_mode));
|
||||
}
|
||||
|
||||
fn is_screen_reader_enabled(&self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
||||
fn microphone_access_state(&self) -> platform::MicrophoneAccessState {
|
||||
platform::MicrophoneAccessState::Denied
|
||||
}
|
||||
|
||||
fn is_headless(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
struct DispatchDelegate {
|
||||
event_sender: Sender<AppEvent>,
|
||||
}
|
||||
|
||||
impl platform::DispatchDelegate for DispatchDelegate {
|
||||
fn is_main_thread(&self) -> bool {
|
||||
thread::current().id()
|
||||
== *MAIN_THREAD_ID
|
||||
.get()
|
||||
.expect("should have marked a thread as the main thread")
|
||||
}
|
||||
|
||||
fn run_on_main_thread(&self, task: async_task::Runnable) {
|
||||
// See crate::windowing::winit::delegate::DispatchDelegate for why we use ManuallyDrop.
|
||||
if self
|
||||
.event_sender
|
||||
.send(AppEvent::RunTask(ManuallyDrop::new(task)))
|
||||
.is_err()
|
||||
{
|
||||
log::warn!("Tried to send event, but event loop is no longer running");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
use crate::{
|
||||
platform::{
|
||||
self,
|
||||
app::{AppCallbackDispatcher, ApproveTerminateResult, TerminationResult},
|
||||
TerminationMode,
|
||||
},
|
||||
AppContext, WindowId,
|
||||
};
|
||||
|
||||
/// Application events handled on the headless platform's main thread.
|
||||
pub(super) enum AppEvent {
|
||||
/// Run the wrapped task on the main thread.
|
||||
RunTask(ManuallyDrop<async_task::Runnable>),
|
||||
/// Run a synchronous callback on the main thread.
|
||||
RunCallback(Box<dyn FnOnce(&mut AppContext) + Send + Sync>),
|
||||
/// Close a window.
|
||||
CloseWindow(WindowId),
|
||||
/// Active window changed.
|
||||
ActiveWindowChanged(Option<WindowId>),
|
||||
/// Exit the event loop, terminating the application.
|
||||
Terminate(TerminationMode),
|
||||
}
|
||||
|
||||
/// Run a simple, blocking event loop that processes AppEvent messages until termination.
|
||||
pub(super) fn run(
|
||||
mut ui_app: crate::App,
|
||||
callbacks: &mut AppCallbackDispatcher,
|
||||
init_fn: platform::app::AppInitCallbackFn,
|
||||
receiver: Receiver<AppEvent>,
|
||||
sender: Sender<AppEvent>,
|
||||
) -> TerminationResult {
|
||||
// Set up Ctrl-C handler to gracefully terminate the app
|
||||
setup_signal_handler(sender);
|
||||
|
||||
// First, initialize the app.
|
||||
callbacks.initialize_app(init_fn);
|
||||
|
||||
// Then, process events until termination.
|
||||
for event in receiver.iter() {
|
||||
match event {
|
||||
AppEvent::RunCallback(callback) => ui_app.update(callback),
|
||||
AppEvent::RunTask(task) => {
|
||||
// Poll a task on the main thread.
|
||||
let task = ManuallyDrop::into_inner(task);
|
||||
task.run();
|
||||
}
|
||||
AppEvent::Terminate(termination_mode) => {
|
||||
let should_terminate = match termination_mode {
|
||||
TerminationMode::Cancellable => {
|
||||
matches!(
|
||||
callbacks.should_terminate_app(),
|
||||
ApproveTerminateResult::Terminate
|
||||
)
|
||||
}
|
||||
TerminationMode::ForceTerminate | TerminationMode::ContentTransferred => true,
|
||||
};
|
||||
if should_terminate {
|
||||
break;
|
||||
}
|
||||
}
|
||||
AppEvent::CloseWindow(window_id) => {
|
||||
// Notify the app that a window is closing. The app will then remove the window
|
||||
// from WindowManager.
|
||||
callbacks.window_will_close(window_id);
|
||||
}
|
||||
AppEvent::ActiveWindowChanged(window_id) => {
|
||||
callbacks.active_window_changed(window_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drop the receiver so the Ctrl+C signal handler's channel send will fail,
|
||||
// causing it to fall through to `process::exit(130)`. Without this, the
|
||||
// send succeeds (since the receiver is still in scope) but nobody is reading
|
||||
// from the channel, making Ctrl+C ineffective during shutdown.
|
||||
drop(receiver);
|
||||
|
||||
callbacks.app_will_terminate();
|
||||
|
||||
ui_app.termination_result().unwrap_or(Ok(()))
|
||||
}
|
||||
|
||||
/// Set up a signal handler for Ctrl-C (SIGINT) to gracefully terminate the app.
|
||||
///
|
||||
/// When Ctrl-C is received, this will send a Terminate event to the event loop,
|
||||
/// allowing the app to shut down gracefully via the existing termination logic.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn setup_signal_handler(sender: Sender<AppEvent>) {
|
||||
let result = ctrlc::set_handler(move || {
|
||||
log::info!("Received Ctrl-C signal in headless mode, terminating application");
|
||||
// Send a ForceTerminate event to ensure the app exits cleanly.
|
||||
// We use ForceTerminate rather than Cancellable to ensure the app exits
|
||||
// even if there are unsaved changes or other conditions that might prevent shutdown.
|
||||
if sender
|
||||
.send(AppEvent::Terminate(TerminationMode::ForceTerminate))
|
||||
.is_err()
|
||||
{
|
||||
log::warn!("Failed to send termination event - event loop may have already stopped");
|
||||
// If we can't send the event, force exit
|
||||
std::process::exit(130); // 128 + SIGINT (2) = 130
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(e) = result {
|
||||
log::warn!("Failed to set up Ctrl-C handler: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
fn setup_signal_handler(_sender: Sender<AppEvent>) {
|
||||
// No signal handling on WASM
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! A headless implementation of the UI framework's platform abstraction.
|
||||
//!
|
||||
//! This provides enough functionality to run an app, but no GUI or visible output.
|
||||
|
||||
mod app;
|
||||
mod delegate;
|
||||
mod event_loop;
|
||||
mod windowing;
|
||||
|
||||
pub use app::App;
|
||||
pub use delegate::AppDelegate;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use windowing::Window;
|
||||
@@ -0,0 +1,269 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::mpsc};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
geometry::rect::RectF,
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
platform::{self, WindowOptions},
|
||||
windowing::WindowCallbacks,
|
||||
WindowId,
|
||||
};
|
||||
|
||||
use super::event_loop::AppEvent;
|
||||
|
||||
pub struct WindowManager {
|
||||
windows: HashMap<WindowId, Rc<Window>>,
|
||||
active_window: RefCell<Option<WindowId>>,
|
||||
event_sender: mpsc::Sender<AppEvent>,
|
||||
}
|
||||
|
||||
impl WindowManager {
|
||||
pub(super) fn new(event_sender: mpsc::Sender<AppEvent>) -> Self {
|
||||
Self {
|
||||
windows: HashMap::new(),
|
||||
active_window: RefCell::new(None),
|
||||
event_sender,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_active_window(&self, window_id: Option<WindowId>) {
|
||||
*self.active_window.borrow_mut() = window_id;
|
||||
|
||||
if self
|
||||
.event_sender
|
||||
.send(AppEvent::ActiveWindowChanged(window_id))
|
||||
.is_err()
|
||||
{
|
||||
log::warn!(
|
||||
"Tried to send ActiveWindowChanged event, but event loop is no longer running"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl galaxyui_core::platform::WindowManager for WindowManager {
|
||||
fn open_window(
|
||||
&mut self,
|
||||
window_id: WindowId,
|
||||
window_options: WindowOptions,
|
||||
callbacks: WindowCallbacks,
|
||||
) -> Result<()> {
|
||||
let window = Rc::new(Window::new(window_options, callbacks));
|
||||
self.windows.insert(window_id, window);
|
||||
self.set_active_window(Some(window_id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn platform_window(&self, window_id: WindowId) -> galaxyui_core::OptionalPlatformWindow {
|
||||
self.windows
|
||||
.get(&window_id)
|
||||
.map(Rc::clone)
|
||||
.map(|inner| inner as Rc<dyn crate::platform::Window>)
|
||||
}
|
||||
|
||||
fn remove_window(&mut self, window_id: WindowId) {
|
||||
self.windows.remove(&window_id);
|
||||
if *self.active_window.borrow() == Some(window_id) {
|
||||
self.set_active_window(None);
|
||||
}
|
||||
}
|
||||
|
||||
fn active_window_id(&self) -> Option<WindowId> {
|
||||
*self.active_window.borrow()
|
||||
}
|
||||
|
||||
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> {
|
||||
self.set_active_window(last_active_window);
|
||||
last_active_window
|
||||
}
|
||||
|
||||
fn show_window_and_focus_app(
|
||||
&self,
|
||||
window_id: WindowId,
|
||||
_behavior: platform::WindowFocusBehavior,
|
||||
) {
|
||||
self.set_active_window(Some(window_id));
|
||||
}
|
||||
|
||||
fn hide_app(&self) {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
fn hide_window(&self, window_id: WindowId) {
|
||||
// If hiding the active window, clear focus.
|
||||
if *self.active_window.borrow() == Some(window_id) {
|
||||
self.set_active_window(None);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_window_bounds(&self, window_id: WindowId, bound: RectF) {
|
||||
if let Some(window) = self.windows.get(&window_id) {
|
||||
window.set_bounds(bound);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_all_windows_background_blur_radius(&self, _blur_radius_pixels: u8) {
|
||||
// No-op for headless.
|
||||
}
|
||||
|
||||
fn set_all_windows_background_blur_texture(&self, _use_blur_texture: bool) {
|
||||
// No-op for headless.
|
||||
}
|
||||
|
||||
fn set_window_title(&self, _window_id: WindowId, _title: &str) {
|
||||
// No-op for headless.
|
||||
}
|
||||
|
||||
fn close_window_async(
|
||||
&self,
|
||||
window_id: WindowId,
|
||||
_termination_mode: platform::TerminationMode,
|
||||
) {
|
||||
// In headless mode, always force-close the window since there's no confirmation dialog.
|
||||
if self
|
||||
.event_sender
|
||||
.send(AppEvent::CloseWindow(window_id))
|
||||
.is_err()
|
||||
{
|
||||
log::warn!("Tried to send event, but event loop is no longer running");
|
||||
}
|
||||
}
|
||||
|
||||
fn active_display_bounds(&self) -> RectF {
|
||||
// A single default display.
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn active_display_id(&self) -> crate::DisplayId {
|
||||
crate::DisplayId::from(0)
|
||||
}
|
||||
|
||||
fn display_count(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn bounds_for_display_idx(&self, _idx: crate::DisplayIdx) -> Option<RectF> {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn active_cursor_position_updated(&self) {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Window {
|
||||
callbacks: WindowCallbacks,
|
||||
bounds: RefCell<RectF>,
|
||||
fullscreen_state: RefCell<platform::FullscreenState>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
fn new(options: WindowOptions, callbacks: WindowCallbacks) -> Self {
|
||||
let bounds = match options.bounds {
|
||||
platform::WindowBounds::Default => RectF::new(vec2f(0.0, 0.0), vec2f(1024.0, 768.0)),
|
||||
platform::WindowBounds::ExactSize(size) => RectF::new(vec2f(0.0, 0.0), size),
|
||||
platform::WindowBounds::ExactPosition(rect) => rect,
|
||||
};
|
||||
Self {
|
||||
callbacks,
|
||||
bounds: RefCell::new(bounds),
|
||||
fullscreen_state: RefCell::new(options.fullscreen_state),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_bounds(&self, rect: RectF) {
|
||||
*self.bounds.borrow_mut() = rect;
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::Window for Window {
|
||||
fn minimize(&self) {}
|
||||
|
||||
fn toggle_maximized(&self) {}
|
||||
|
||||
fn toggle_fullscreen(&self) {}
|
||||
|
||||
fn fullscreen_state(&self) -> platform::FullscreenState {
|
||||
*self.fullscreen_state.borrow()
|
||||
}
|
||||
|
||||
fn set_titlebar_height(&self, _height: f64) {}
|
||||
|
||||
fn supports_transparency(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn as_ctx(&self) -> &dyn platform::WindowContext {
|
||||
self
|
||||
}
|
||||
|
||||
fn callbacks(&self) -> &crate::windowing::WindowCallbacks {
|
||||
&self.callbacks
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::WindowContext for Window {
|
||||
fn size(&self) -> Vector2F {
|
||||
self.bounds.borrow().size()
|
||||
}
|
||||
|
||||
fn origin(&self) -> Vector2F {
|
||||
self.bounds.borrow().origin()
|
||||
}
|
||||
|
||||
fn backing_scale_factor(&self) -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
fn max_texture_dimension_2d(&self) -> Option<u32> {
|
||||
Some(2048)
|
||||
}
|
||||
|
||||
fn render_scene(&self, _scene: Rc<crate::Scene>) {}
|
||||
|
||||
fn request_redraw(&self) {}
|
||||
|
||||
fn request_frame_capture(
|
||||
&self,
|
||||
_callback: Box<dyn FnOnce(platform::CapturedFrame) + Send + 'static>,
|
||||
) {
|
||||
// no-op for headless
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Re-export a couple winit types and modules as the concrete implementations
|
||||
// for the linux platform.
|
||||
pub use crate::windowing::winit::app::App;
|
||||
|
||||
use crate::{
|
||||
windowing::{self, WindowingSystem},
|
||||
AppContext,
|
||||
};
|
||||
|
||||
use super::{app::AppBackend, AsInnerMut};
|
||||
|
||||
/// An extension trait defining additional configurability for
|
||||
/// applications when running on Linux.
|
||||
pub trait AppBuilderExt {
|
||||
/// Sets the value to use for WM_CLASS (when running under X11) or app_id
|
||||
/// (when running under Wayland).
|
||||
///
|
||||
/// This is used to identify the application and link it properly to its
|
||||
/// .desktop file and associated resources (like app icons).
|
||||
fn set_window_class(&mut self, window_class: String);
|
||||
|
||||
/// Whether or not to force the use of XWayland for users running Wayland.
|
||||
fn force_x11(&mut self, force_x11: bool);
|
||||
}
|
||||
|
||||
impl AppBuilderExt for super::AppBuilder {
|
||||
fn set_window_class(&mut self, window_class: String) {
|
||||
match self.as_inner_mut() {
|
||||
AppBackend::CurrentPlatform(app) => app.set_window_class(window_class),
|
||||
AppBackend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn force_x11(&mut self, force_x11: bool) {
|
||||
match self.as_inner_mut() {
|
||||
AppBackend::CurrentPlatform(app) => app.force_x11(force_x11),
|
||||
AppBackend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the windowing system that the user is running before the event loop is created.
|
||||
pub fn user_windowing_system() -> WindowingSystem {
|
||||
// This mirrors winit's logic [here](https://github.com/rust-windowing/winit/blob/4cd6877e8e19e7e1ba957a409394dca1af4afcdd/src/platform_impl/linux/mod.rs#L735-L745).
|
||||
if std::env::var("WAYLAND_DISPLAY")
|
||||
.ok()
|
||||
.filter(|var| !var.is_empty())
|
||||
.or_else(|| std::env::var("WAYLAND_SOCKET").ok())
|
||||
.filter(|var| !var.is_empty())
|
||||
.is_some()
|
||||
{
|
||||
WindowingSystem::Wayland
|
||||
} else {
|
||||
WindowingSystem::X11
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_wsl() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static IS_WSL: OnceLock<bool> = OnceLock::new();
|
||||
IS_WSL
|
||||
.get_or_init(|| std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists())
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub fn is_wayland_env_var_set() -> bool {
|
||||
std::env::var_os("WARP_ENABLE_WAYLAND")
|
||||
.is_some_and(|warp_enable_wayland| warp_enable_wayland.eq_ignore_ascii_case("1"))
|
||||
}
|
||||
|
||||
pub fn windowing_system_is_customizable(app: &AppContext) -> bool {
|
||||
!is_wayland_env_var_set()
|
||||
&& app
|
||||
.windows()
|
||||
.windowing_system()
|
||||
.is_some_and(|windowing_system| {
|
||||
matches!(
|
||||
windowing_system,
|
||||
windowing::System::X11 { is_x_wayland: true } | windowing::System::Wayland
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,643 @@
|
||||
use cocoa::appkit::NSApp;
|
||||
use cocoa::foundation::{NSUInteger, NSURL};
|
||||
use cocoa::{
|
||||
base::{id, nil},
|
||||
foundation::{NSArray, NSAutoreleasePool, NSData, NSString},
|
||||
};
|
||||
use futures_util::future::LocalBoxFuture;
|
||||
use objc::{
|
||||
class, msg_send,
|
||||
runtime::{Object, Sel, BOOL, NO, YES},
|
||||
sel, sel_impl,
|
||||
};
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::CStr,
|
||||
os::raw::{c_char, c_void},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::platform::{
|
||||
app::{AppBackend, AppBuilder},
|
||||
AsInnerMut,
|
||||
};
|
||||
use galaxyui_core::{
|
||||
assets::AssetProvider,
|
||||
integration::TestDriver,
|
||||
keymap::{Keystroke, Trigger},
|
||||
modals::{AlertDialog, ModalId},
|
||||
platform::app::{AppCallbackDispatcher, ApproveTerminateResult},
|
||||
platform::menu::{Menu, MenuBar},
|
||||
platform::SaveFilePickerCallback,
|
||||
platform::{self, FilePickerCallback},
|
||||
AppContext, Event,
|
||||
};
|
||||
|
||||
use super::{
|
||||
keycode::{Keycode, CMD_KEY, CONTROL_KEY, OPTION_KEY, SHIFT_KEY},
|
||||
make_nsstring,
|
||||
menus::{make_dock_menu, make_main_menu},
|
||||
window::{get_window_state, IntegrationTestWindowManager, Window, WindowManager},
|
||||
};
|
||||
|
||||
pub trait NSAlert: Sized {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSAlert), alloc]
|
||||
}
|
||||
|
||||
unsafe fn init(self) -> id;
|
||||
unsafe fn autorelease(self) -> id;
|
||||
unsafe fn set_message_text(self, message_text: id);
|
||||
unsafe fn set_informative_text(self, informative_text: id);
|
||||
unsafe fn add_button_with_title(self, title: id);
|
||||
}
|
||||
|
||||
impl NSAlert for id {
|
||||
unsafe fn init(self) -> id {
|
||||
msg_send![self, init]
|
||||
}
|
||||
|
||||
unsafe fn autorelease(self) -> id {
|
||||
msg_send![self, autorelease]
|
||||
}
|
||||
|
||||
unsafe fn set_message_text(self, message_text: id) {
|
||||
msg_send![self, setMessageText: message_text]
|
||||
}
|
||||
|
||||
unsafe fn set_informative_text(self, informative_text: id) {
|
||||
msg_send![self, setInformativeText: informative_text]
|
||||
}
|
||||
|
||||
unsafe fn add_button_with_title(self, title: id) {
|
||||
msg_send![self, addButtonWithTitle: title]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_native_platform_modal(dialog: AlertDialog) -> id {
|
||||
unsafe {
|
||||
let alert = NSAlert::autorelease(NSAlert::init(NSAlert::alloc(nil)));
|
||||
alert.set_informative_text(make_nsstring(&dialog.info_text));
|
||||
alert.set_message_text(make_nsstring(&dialog.message_text));
|
||||
for title in dialog.buttons {
|
||||
alert.add_button_with_title(make_nsstring(&title));
|
||||
}
|
||||
alert
|
||||
}
|
||||
}
|
||||
|
||||
const RUST_WRAPPER_IVAR_NAME: &str = "rustWrapper";
|
||||
|
||||
extern "C" {
|
||||
// Implemented in ObjC to get the warp NSApplication subclass.
|
||||
pub(super) fn get_warp_app() -> id;
|
||||
}
|
||||
|
||||
/// An extension trait defining additional configurability for
|
||||
/// applications when running on macOS.
|
||||
pub trait AppExt {
|
||||
/// Sets whether or not the application should be activated
|
||||
/// when it is launched.
|
||||
fn set_activate_on_launch(&mut self, value: bool);
|
||||
|
||||
/// Sets the application icon which should be used when running
|
||||
/// without an application bundle.
|
||||
fn set_dev_icon(&mut self, value: Cow<'static, [u8]>);
|
||||
|
||||
/// Sets the main menu bar constructor function.
|
||||
fn set_menu_bar_builder(&mut self, value: impl FnOnce(&mut AppContext) -> MenuBar + 'static);
|
||||
|
||||
/// Sets the macOS dock menu constructor function.
|
||||
fn set_dock_menu_builder(&mut self, value: impl FnOnce(&mut AppContext) -> Menu + 'static);
|
||||
}
|
||||
|
||||
type MenuBarBuilderFn = Box<dyn FnOnce(&mut AppContext) -> MenuBar>;
|
||||
type DockMenuBuilderFn = Box<dyn FnOnce(&mut AppContext) -> Menu>;
|
||||
|
||||
/// The actual application, from the perspective of the platform and the
|
||||
/// main event loop. This is the true owner of all application state.
|
||||
pub struct App {
|
||||
callbacks: AppCallbackDispatcher,
|
||||
activate_on_launch: bool,
|
||||
dev_icon: Option<Cow<'static, [u8]>>,
|
||||
menu_bar_builder: Option<MenuBarBuilderFn>,
|
||||
dock_menu_builder: Option<DockMenuBuilderFn>,
|
||||
init_fn: Option<platform::app::AppInitCallbackFn>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(in crate::platform) fn new(
|
||||
callbacks: platform::app::AppCallbacks,
|
||||
assets: Box<dyn AssetProvider>,
|
||||
test_driver: Option<&TestDriver>,
|
||||
) -> Self {
|
||||
let platform_delegate: Box<dyn platform::Delegate> = if test_driver.is_some() {
|
||||
Box::new(
|
||||
super::delegate::IntegrationTestDelegate::new()
|
||||
.expect("should not fail to create platform delegate"),
|
||||
)
|
||||
} else {
|
||||
Box::new(
|
||||
super::delegate::AppDelegate::new()
|
||||
.expect("should not fail to create platform delegate"),
|
||||
)
|
||||
};
|
||||
|
||||
let window_manager: Box<dyn platform::WindowManager> = if test_driver.is_some() {
|
||||
Box::new(IntegrationTestWindowManager::new())
|
||||
} else {
|
||||
Box::new(WindowManager::new())
|
||||
};
|
||||
|
||||
let ui_app = crate::App::new(
|
||||
platform_delegate,
|
||||
window_manager,
|
||||
Box::new(super::fonts::FontDB::new()),
|
||||
assets,
|
||||
)
|
||||
.expect("should not fail to construct application");
|
||||
|
||||
Self {
|
||||
callbacks: AppCallbackDispatcher::new(callbacks, ui_app),
|
||||
activate_on_launch: true,
|
||||
dev_icon: None,
|
||||
menu_bar_builder: None,
|
||||
dock_menu_builder: None,
|
||||
init_fn: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::platform) fn run(
|
||||
mut self,
|
||||
init_fn: impl FnOnce(&mut AppContext, LocalBoxFuture<'static, crate::App>) + 'static,
|
||||
) {
|
||||
self.init_fn = Some(Box::new(init_fn));
|
||||
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
|
||||
// Get (and create, if necessary) the underlying NSApplication.
|
||||
let app: id = get_warp_app();
|
||||
|
||||
let running_app: id = msg_send![class!(NSRunningApplication), currentApplication];
|
||||
let bundle_id: id = msg_send![running_app, bundleIdentifier];
|
||||
let dev_icon = if bundle_id.is_null() {
|
||||
self.dev_icon.as_ref().map(|dev_icon| {
|
||||
let data: id = msg_send![class!(NSData), alloc];
|
||||
let data: id = data.initWithBytes_length_(
|
||||
dev_icon.as_ptr() as *const c_void,
|
||||
dev_icon.len() as u64,
|
||||
);
|
||||
let image: id = msg_send![class!(NSImage), alloc];
|
||||
image.initWithData_(data)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let app_delegate: id = msg_send![app, delegate];
|
||||
|
||||
let self_ptr = Box::into_raw(Box::new(self));
|
||||
(*app).set_ivar(RUST_WRAPPER_IVAR_NAME, self_ptr as *mut c_void);
|
||||
(*app_delegate).set_ivar(RUST_WRAPPER_IVAR_NAME, self_ptr as *mut c_void);
|
||||
|
||||
if let Some(dev_icon) = dev_icon {
|
||||
let _: () = msg_send![app, setApplicationIconImage: dev_icon];
|
||||
}
|
||||
|
||||
let _: () = msg_send![app, run];
|
||||
let _: () = msg_send![pool, drain];
|
||||
|
||||
// App is done running when we get here, so we can reinstantiate the Box and drop it.
|
||||
drop(Box::from_raw(self_ptr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppExt for AppBuilder {
|
||||
fn set_activate_on_launch(&mut self, value: bool) {
|
||||
match self.as_inner_mut() {
|
||||
AppBackend::CurrentPlatform(app) => app.activate_on_launch = value,
|
||||
AppBackend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_dev_icon(&mut self, value: Cow<'static, [u8]>) {
|
||||
match self.as_inner_mut() {
|
||||
AppBackend::CurrentPlatform(app) => app.dev_icon = Some(value),
|
||||
AppBackend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_menu_bar_builder(&mut self, value: impl FnOnce(&mut AppContext) -> MenuBar + 'static) {
|
||||
match self.as_inner_mut() {
|
||||
AppBackend::CurrentPlatform(app) => app.menu_bar_builder = Some(Box::new(value)),
|
||||
AppBackend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_dock_menu_builder(&mut self, value: impl FnOnce(&mut AppContext) -> Menu + 'static) {
|
||||
match self.as_inner_mut() {
|
||||
AppBackend::CurrentPlatform(app) => app.dock_menu_builder = Some(Box::new(value)),
|
||||
AppBackend::Headless(_) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_app(object: &mut Object) -> &mut App {
|
||||
let wrapper_ptr: *mut c_void = *object.get_ivar(RUST_WRAPPER_IVAR_NAME);
|
||||
&mut *(wrapper_ptr as *mut App)
|
||||
}
|
||||
|
||||
pub(super) fn callback_dispatcher() -> &'static mut AppCallbackDispatcher {
|
||||
unsafe {
|
||||
let app = get_warp_app();
|
||||
let app = get_app(&mut *app);
|
||||
&mut app.callbacks
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_send_global_keybinding(
|
||||
this: &mut Object,
|
||||
modifiers: NSUInteger,
|
||||
key_code: NSUInteger,
|
||||
) {
|
||||
let keystroke = {
|
||||
let modifiers = modifiers as u16;
|
||||
let shift_key_pressed = (modifiers & SHIFT_KEY) > 0;
|
||||
Keycode(key_code as u16)
|
||||
.try_to_key_name(shift_key_pressed)
|
||||
.map(|key| Keystroke {
|
||||
ctrl: (modifiers & CONTROL_KEY) > 0,
|
||||
alt: (modifiers & OPTION_KEY) > 0,
|
||||
shift: shift_key_pressed,
|
||||
cmd: (modifiers & CMD_KEY) > 0,
|
||||
meta: false,
|
||||
key,
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(keystroke) = keystroke {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.global_shortcut_triggered(keystroke);
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C-unwind" fn warp_app_will_finish_launching(this: &mut Object) {
|
||||
log::info!("application will finish launching");
|
||||
|
||||
let app = get_app(this);
|
||||
|
||||
if app.activate_on_launch {
|
||||
let _: () = msg_send![NSApp(), activateIgnoringOtherApps: YES];
|
||||
}
|
||||
|
||||
if let Some(init_fn) = app.init_fn.take() {
|
||||
app.callbacks.initialize_app(init_fn);
|
||||
}
|
||||
|
||||
let app_delegate: id = msg_send![NSApp(), delegate];
|
||||
|
||||
if app.callbacks.has_internet_reachability_changed_callback() {
|
||||
let _: () = msg_send![app_delegate, setReachabilityListener];
|
||||
}
|
||||
|
||||
if let Some(menu_bar_builder) = app.menu_bar_builder.take() {
|
||||
let menu_bar = app.callbacks.with_mutable_app_context(menu_bar_builder);
|
||||
let nsmenu = make_main_menu(menu_bar);
|
||||
let () = msg_send![NSApp(), setMainMenu: nsmenu];
|
||||
}
|
||||
|
||||
if let Some(dock_menu_builder) = app.dock_menu_builder.take() {
|
||||
let dock_menu = app.callbacks.with_mutable_app_context(dock_menu_builder);
|
||||
let nsmenu = make_dock_menu(dock_menu);
|
||||
let _: () = msg_send![app_delegate, setDockMenu: nsmenu];
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_did_become_active(this: &mut Object, _: Sel, _: id) {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.app_became_active();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_internet_reachability_changed(
|
||||
this: &mut Object,
|
||||
can_reach: u8,
|
||||
) {
|
||||
let is_reachable = can_reach != 0;
|
||||
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.internet_reachability_changed(is_reachable);
|
||||
}
|
||||
|
||||
/// Returns whether or not we can proceed with termination.
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_should_terminate_app(this: &mut Object) -> BOOL {
|
||||
let app = unsafe { get_app(this) };
|
||||
|
||||
match app.callbacks.should_terminate_app() {
|
||||
ApproveTerminateResult::Terminate => YES,
|
||||
ApproveTerminateResult::Cancel => NO,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a NSAlert object if we want to show a dialog for users to confirm or
|
||||
/// nil for closing the window immediately.
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_should_close_window(
|
||||
this: &mut Object,
|
||||
window_id: &mut Object,
|
||||
) -> BOOL {
|
||||
let app = unsafe { get_app(this) };
|
||||
let window = unsafe { get_window_state(window_id) };
|
||||
|
||||
match app.callbacks.should_close_window(window.id()) {
|
||||
ApproveTerminateResult::Terminate => YES,
|
||||
ApproveTerminateResult::Cancel => NO,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_are_key_bindings_disabled_for_window(
|
||||
this: &mut Object,
|
||||
window_id: &mut Object,
|
||||
) -> BOOL {
|
||||
let app = unsafe { get_app(this) };
|
||||
let window = unsafe { get_window_state(window_id) };
|
||||
|
||||
let disabled = app
|
||||
.callbacks
|
||||
.with_mutable_app_context(|ctx| !ctx.key_bindings_enabled(window.id()));
|
||||
|
||||
if disabled {
|
||||
YES
|
||||
} else {
|
||||
NO
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_has_binding_for_keystroke(
|
||||
this: &mut Object,
|
||||
event: id,
|
||||
) -> BOOL {
|
||||
let app = unsafe { get_app(this) };
|
||||
let warp_event = unsafe { super::event::from_native(event, None, false) };
|
||||
|
||||
let Some(Event::KeyDown { keystroke, .. }) = warp_event else {
|
||||
return NO;
|
||||
};
|
||||
let has_binding = app.callbacks.with_mutable_app_context(|ctx| {
|
||||
ctx.get_key_bindings().any(|binding| {
|
||||
if let Trigger::Keystrokes(keystrokes) = binding.trigger {
|
||||
keystrokes.len() == 1 && keystrokes[0] == keystroke
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if has_binding {
|
||||
YES
|
||||
} else {
|
||||
NO
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_has_custom_action_for_keystroke(
|
||||
this: &mut Object,
|
||||
event: id,
|
||||
) -> BOOL {
|
||||
let app = unsafe { get_app(this) };
|
||||
let warp_event = unsafe { super::event::from_native(event, None, false) };
|
||||
|
||||
let Some(Event::KeyDown { keystroke, .. }) = warp_event else {
|
||||
return NO;
|
||||
};
|
||||
let has_binding = app.callbacks.with_mutable_app_context(|ctx| {
|
||||
ctx.custom_action_bindings()
|
||||
.any(|binding| match binding.trigger {
|
||||
Trigger::Keystrokes(keystrokes) => {
|
||||
keystrokes.len() == 1 && keystrokes[0] == keystroke
|
||||
}
|
||||
Trigger::Custom(tag) => ctx
|
||||
.default_keystroke_trigger_for_custom_action(*tag)
|
||||
.is_some_and(|k| k == keystroke),
|
||||
_ => false,
|
||||
})
|
||||
});
|
||||
|
||||
if has_binding {
|
||||
YES
|
||||
} else {
|
||||
NO
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_disable_warning_modal(this: &mut Object) {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.warning_modal_disabled();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_process_modal_response(
|
||||
this: &mut Object,
|
||||
modal_id: ModalId,
|
||||
response: usize,
|
||||
disable_modal: bool,
|
||||
) {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks
|
||||
.process_platform_modal_response(modal_id, response, disable_modal);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_app_notification_clicked(
|
||||
this: &mut Object,
|
||||
date: f64,
|
||||
data: id,
|
||||
) {
|
||||
let app = unsafe { get_app(this) };
|
||||
if let Ok(notification_response) =
|
||||
unsafe { super::notification::response_from_native(date as i32, data) }
|
||||
{
|
||||
app.callbacks.notification_clicked(notification_response);
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_did_resign_active(this: &mut Object, _: Sel, _: id) {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.app_resigned_active();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_will_terminate(this: &mut Object, _: Sel, _: id) {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.app_will_terminate();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_new_window(this: &mut Object) {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.open_new_window();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_active_window_changed(this: &mut Object) {
|
||||
let app = unsafe { get_app(this) };
|
||||
Window::close_ime_on_active_window();
|
||||
app.callbacks
|
||||
.active_window_changed(Window::active_window_id());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_window_did_resize(this: &mut Object) {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.window_resized();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_window_did_move(this: &mut Object) {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.window_moved();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_window_will_close(this: &mut Object, window: &mut Object) {
|
||||
let app = unsafe { get_app(this) };
|
||||
let window_state = unsafe { get_window_state(window) };
|
||||
app.callbacks.window_will_close(window_state.id());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_screen_did_change(this: &mut Object) {
|
||||
log::info!("received NSApplicationDidChangeScreenParametersNotification");
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.screen_changed();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn cpu_awakened(this: &mut Object) {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.cpu_awakened();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn cpu_will_sleep(this: &mut Object) {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.cpu_will_sleep();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_open_files(this: &mut Object, paths: id) {
|
||||
let paths = unsafe {
|
||||
(0..paths.count())
|
||||
.filter_map(|i| {
|
||||
let path = paths.objectAtIndex(i);
|
||||
match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
|
||||
Ok(string) => Some(PathBuf::from(string)),
|
||||
Err(err) => {
|
||||
log::error!("error converting path to string: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.open_files(paths);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_open_urls(this: &mut Object, urls: id) {
|
||||
let urls = unsafe {
|
||||
(0..urls.count())
|
||||
.filter_map(|i| {
|
||||
let url = urls.objectAtIndex(i).absoluteString();
|
||||
match CStr::from_ptr(url.UTF8String() as *mut c_char).to_str() {
|
||||
Ok(string) => Some(string.to_string()),
|
||||
Err(err) => {
|
||||
log::error!("error converting url to string: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.open_urls(urls);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_app_os_appearance_changed(this: &mut Object) {
|
||||
let app = unsafe { get_app(this) };
|
||||
app.callbacks.os_appearance_changed();
|
||||
}
|
||||
|
||||
// Calls the callback with None if no file was selected
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_open_panel_file_selected(urls: id, callback: *mut c_void) {
|
||||
// Start by converting the callback from a raw pointer back into a Box, to
|
||||
// avoid the memory leak that would occur if we left it in raw pointer form.
|
||||
let callback = unsafe { Box::from_raw(callback as *mut FilePickerCallback) };
|
||||
|
||||
let paths = unsafe {
|
||||
(0..urls.count())
|
||||
.map(|i| {
|
||||
let file_url = urls.objectAtIndex(i);
|
||||
let file_path: id = msg_send![file_url, path];
|
||||
let slice = std::slice::from_raw_parts(
|
||||
file_path.UTF8String() as *const std::ffi::c_uchar,
|
||||
file_path.len(),
|
||||
);
|
||||
std::str::from_utf8_unchecked(slice).to_string()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
if paths.is_empty() {
|
||||
log::info!("No file was selected. Dialog was cancelled.")
|
||||
}
|
||||
|
||||
let app = unsafe { get_app(&mut *get_warp_app()) };
|
||||
app.callbacks.with_mutable_app_context(move |ctx| {
|
||||
callback(Ok(paths), ctx);
|
||||
});
|
||||
}
|
||||
|
||||
// Calls the save callback with the selected path or None if cancelled
|
||||
#[no_mangle]
|
||||
pub(crate) extern "C-unwind" fn warp_save_panel_file_selected(url: id, callback: *mut c_void) {
|
||||
let callback = unsafe { Box::from_raw(callback as *mut SaveFilePickerCallback) };
|
||||
|
||||
let path = if url.is_null() {
|
||||
None
|
||||
} else {
|
||||
unsafe {
|
||||
let file_path: id = msg_send![url, path];
|
||||
let slice = std::slice::from_raw_parts(
|
||||
file_path.UTF8String() as *const std::ffi::c_uchar,
|
||||
file_path.len(),
|
||||
);
|
||||
Some(std::str::from_utf8_unchecked(slice).to_string())
|
||||
}
|
||||
};
|
||||
|
||||
if path.is_none() {
|
||||
log::info!("Save dialog was cancelled.");
|
||||
}
|
||||
|
||||
let app = unsafe { get_app(&mut *get_warp_app()) };
|
||||
app.callbacks.with_mutable_app_context(move |ctx| {
|
||||
callback(path, ctx);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use cocoa::appkit::{NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypeString};
|
||||
use cocoa::foundation::{NSArray, NSData};
|
||||
use cocoa::{
|
||||
base::{id, nil},
|
||||
foundation::NSString,
|
||||
};
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::{c_uchar, c_void};
|
||||
use std::slice;
|
||||
|
||||
use super::make_nsstring;
|
||||
use galaxyui_core::clipboard::{ClipboardContent, ImageData};
|
||||
|
||||
extern "C" {
|
||||
fn getFilePathsFromPasteboard() -> id;
|
||||
}
|
||||
|
||||
pub struct Clipboard(id);
|
||||
|
||||
unsafe impl Send for Clipboard {}
|
||||
|
||||
impl Clipboard {
|
||||
pub fn new() -> Result<Self> {
|
||||
let pboard = unsafe { NSPasteboard::generalPasteboard(nil) };
|
||||
if pboard.is_null() {
|
||||
Err(anyhow!("NSPasteboard::generalPasteboard returned nil"))
|
||||
} else {
|
||||
Ok(Clipboard(pboard))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn pasteboard_type_for_image_mime_type(mime_type: &str) -> Option<id> {
|
||||
let pasteboard_type = match mime_type {
|
||||
"image/png" => "public.png",
|
||||
"image/jpeg" => "public.jpeg",
|
||||
"image/gif" => "public.gif",
|
||||
"image/webp" => "public.webp",
|
||||
"image/svg+xml" => "public.svg-image",
|
||||
_ => return None,
|
||||
};
|
||||
Some(make_nsstring(pasteboard_type))
|
||||
}
|
||||
|
||||
impl crate::Clipboard for Clipboard {
|
||||
fn write(&mut self, contents: ClipboardContent) {
|
||||
unsafe {
|
||||
let nsstr = make_nsstring(&contents.plain_text);
|
||||
self.0
|
||||
.declareTypes_owner(NSArray::arrayWithObject(nil, NSPasteboardTypeString), nil);
|
||||
NSPasteboard::setString_forType(self.0, nsstr, NSPasteboardTypeString);
|
||||
|
||||
if let Some(html) = contents.html {
|
||||
let nsstr = make_nsstring(&html);
|
||||
self.0
|
||||
.addTypes_owner(NSArray::arrayWithObject(nil, NSPasteboardTypeHTML), nil);
|
||||
NSPasteboard::setString_forType(self.0, nsstr, NSPasteboardTypeHTML);
|
||||
}
|
||||
|
||||
if let Some(images) = contents.images {
|
||||
for image in images {
|
||||
let Some(pasteboard_type) =
|
||||
pasteboard_type_for_image_mime_type(&image.mime_type)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let data: id = msg_send![class!(NSData), alloc];
|
||||
let data: id = data.initWithBytes_length_(
|
||||
image.data.as_ptr() as *const c_void,
|
||||
image.data.len() as u64,
|
||||
);
|
||||
self.0
|
||||
.addTypes_owner(NSArray::arrayWithObject(nil, pasteboard_type), nil);
|
||||
let _: () = msg_send![self.0, setData: data forType: pasteboard_type];
|
||||
// Balance the +1 retain from `[NSData alloc]`. The pasteboard retains
|
||||
// `data` in `setData:forType:`, so the object stays alive as needed.
|
||||
let _: () = msg_send![data, release];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&mut self) -> ClipboardContent {
|
||||
unsafe {
|
||||
// Try getting file paths from the clipboard. If we end up with an empty
|
||||
// array of file paths, fallback to getting the string from the pasteboard.
|
||||
let file_paths = getFilePathsFromPasteboard();
|
||||
let available_paths = file_paths.count();
|
||||
|
||||
let text = NSPasteboard::stringForType(self.0, NSPasteboardTypeString);
|
||||
let mut content = ClipboardContent::plain_text(if text != nil {
|
||||
CStr::from_ptr(text.UTF8String())
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
} else {
|
||||
String::from("")
|
||||
});
|
||||
|
||||
if available_paths > 0 {
|
||||
content.paths = Some(
|
||||
(0..available_paths)
|
||||
.map(|i| {
|
||||
let directory = file_paths.objectAtIndex(i);
|
||||
let slice = slice::from_raw_parts(
|
||||
directory.UTF8String() as *const c_uchar,
|
||||
directory.len(),
|
||||
);
|
||||
std::str::from_utf8_unchecked(slice).to_string()
|
||||
})
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
}
|
||||
|
||||
let html = NSPasteboard::stringForType(self.0, NSPasteboardTypeHTML);
|
||||
if html != nil {
|
||||
content.html = Some(
|
||||
CStr::from_ptr(html.UTF8String())
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
// Try to read image data from clipboard
|
||||
content.images = self.read_image_data_from_pasteboard();
|
||||
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
/// Reads image data from the macOS pasteboard.
|
||||
///
|
||||
/// Checks for supported image formats and returns the first available image
|
||||
/// data found, prioritizing common web-compatible formats.
|
||||
fn read_image_data_from_pasteboard(&self) -> Option<Vec<ImageData>> {
|
||||
unsafe {
|
||||
// Check for common image types on macOS pasteboard
|
||||
// macOS pasteboard type identifiers for supported image formats
|
||||
// Ordered by preference for web compatibility
|
||||
let supported_pasteboard_types = [
|
||||
make_nsstring("public.png"),
|
||||
make_nsstring("public.jpeg"),
|
||||
make_nsstring("public.gif"),
|
||||
make_nsstring("public.webp"),
|
||||
make_nsstring("public.svg-image"),
|
||||
make_nsstring("com.compuserve.gif"),
|
||||
];
|
||||
|
||||
let mut images = Vec::new();
|
||||
|
||||
for &pasteboard_type in &supported_pasteboard_types {
|
||||
let data = NSPasteboard::dataForType(self.0, pasteboard_type);
|
||||
if data != nil {
|
||||
let length = NSData::length(data);
|
||||
if length > 0 {
|
||||
let bytes_ptr = NSData::bytes(data) as *const u8;
|
||||
let bytes = slice::from_raw_parts(bytes_ptr, length as usize);
|
||||
|
||||
let mime_type = match CStr::from_ptr(pasteboard_type.UTF8String())
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
{
|
||||
"public.png" => "image/png",
|
||||
"public.jpeg" => "image/jpeg",
|
||||
"public.gif" | "com.compuserve.gif" => "image/gif",
|
||||
"public.webp" => "image/webp",
|
||||
"public.svg-image" => "image/svg+xml",
|
||||
_ => "image/unknown",
|
||||
};
|
||||
|
||||
// Try to extract filename from HTML content if available
|
||||
let filename = {
|
||||
let html = NSPasteboard::stringForType(self.0, NSPasteboardTypeHTML);
|
||||
if html != nil {
|
||||
let html_str =
|
||||
CStr::from_ptr(html.UTF8String()).to_str().unwrap_or("");
|
||||
if !html_str.is_empty() {
|
||||
crate::clipboard_utils::extract_filename_from_html(html_str)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
images.push(ImageData {
|
||||
data: bytes.to_vec(),
|
||||
mime_type: mime_type.to_string(),
|
||||
filename,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if images.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(images)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "clipboard_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Memory-behavior repro for APP-4154 batch 1.C (galaxyui-platform-nsstring).
|
||||
//!
|
||||
//! Exercises the `NSString::alloc(nil).init_str(...)` → `make_nsstring(...)` conversions
|
||||
//! applied to `pasteboard_type_for_image_mime_type` and related clipboard
|
||||
//! helpers. The helper is shared by every retained-NSString site in this file
|
||||
//! (6 in `read_image_data_from_pasteboard`, 2 in `Clipboard::write`, and this
|
||||
//! one in `pasteboard_type_for_image_mime_type`), so it is representative for
|
||||
//! the whole file.
|
||||
//!
|
||||
//! On master the raw `NSString::alloc(nil).init_str(...)` returns a string with
|
||||
//! a +1 retain count that no pool drain can clean up, so even inside an outer
|
||||
//! `NSAutoreleasePool` the string survives past `pool.drain()` and memory grows
|
||||
//! linearly with iteration count.
|
||||
//!
|
||||
//! On the PR branch `make_nsstring` autoreleases, so every string returned here
|
||||
//! is released when the surrounding pool drains and peak RSS stays flat across
|
||||
//! outer iterations.
|
||||
//!
|
||||
//! Run as:
|
||||
//! cargo test --release -p galaxyui \
|
||||
//! pasteboard_type_for_image_mime_type_memory_behavior -- --nocapture --ignored
|
||||
//! and measure peak RSS with `/usr/bin/time -l`.
|
||||
use cocoa::base::nil;
|
||||
use cocoa::foundation::NSAutoreleasePool;
|
||||
|
||||
use super::pasteboard_type_for_image_mime_type;
|
||||
|
||||
/// Number of outer pool cycles. Each cycle creates an `NSAutoreleasePool`,
|
||||
/// runs the inner loop, then drains. On master the retained NSStrings survive
|
||||
/// the drain, so memory usage grows proportionally to OUTER * INNER.
|
||||
const OUTER: usize = 60;
|
||||
|
||||
/// Number of inner iterations per pool cycle. Must be large enough to produce
|
||||
/// a measurable RSS delta but small enough to fit easily in memory on the
|
||||
/// branch side (where strings are reclaimed per cycle).
|
||||
const INNER: usize = 20_000;
|
||||
|
||||
const MIME_TYPES: &[&str] = &[
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
];
|
||||
|
||||
#[test]
|
||||
#[ignore = "memory repro; run with --ignored --nocapture in release mode"]
|
||||
fn pasteboard_type_for_image_mime_type_memory_behavior() {
|
||||
unsafe {
|
||||
for _ in 0..OUTER {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
for _ in 0..INNER {
|
||||
for mime in MIME_TYPES {
|
||||
let ns = pasteboard_type_for_image_mime_type(mime);
|
||||
assert!(ns.is_some(), "mime {mime} should be mapped");
|
||||
}
|
||||
}
|
||||
pool.drain();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
use super::app::create_native_platform_modal;
|
||||
use super::keycode::{modifier_code, Keycode};
|
||||
use super::utils::nsstring_as_str;
|
||||
use super::{app, make_nsstring, Clipboard, Window};
|
||||
use anyhow::Result;
|
||||
use cocoa::base::{BOOL, NO, YES};
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use cocoa::{
|
||||
appkit::{NSApp, NSRequestUserAttentionType},
|
||||
base::{id, nil},
|
||||
};
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use galaxyui_core::clipboard::InMemoryClipboard;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::modals::{AlertDialog, ModalId};
|
||||
use galaxyui_core::notification::{NotificationSendError, RequestPermissionsOutcome};
|
||||
use galaxyui_core::platform::{
|
||||
Cursor, FilePickerCallback, FilePickerConfiguration, MicrophoneAccessState,
|
||||
SendNotificationErrorCallback, TerminationMode,
|
||||
};
|
||||
use galaxyui_core::ApplicationBundleInfo;
|
||||
use galaxyui_core::{
|
||||
accessibility::AccessibilityContent, notification::UserNotification, platform, WindowId,
|
||||
};
|
||||
|
||||
// Functions implemented in objC files.
|
||||
extern "C" {
|
||||
// Requests permissions to send desktop notifications.
|
||||
fn requestNotificationPermissions(on_completion_callback: *const c_void);
|
||||
// Sends a desktop notification.
|
||||
fn sendNotification(
|
||||
title: id,
|
||||
body: id,
|
||||
data: id,
|
||||
on_error_callback: *const c_void,
|
||||
play_sound: BOOL,
|
||||
);
|
||||
fn isDarkMode() -> BOOL;
|
||||
fn registerGlobalHotkey(key_code: NSUInteger, modifiers_key: NSUInteger);
|
||||
fn unregisterGlobalHotkey(key_code: NSUInteger, modifiers_key: NSUInteger);
|
||||
fn executableInApplicationBundleWithIdentifier(bundle_path: id) -> id;
|
||||
fn absolutePathForApplicationBundleWithIdentifier(bundle_identifier: id) -> id;
|
||||
fn isVoiceOverEnabled() -> BOOL;
|
||||
}
|
||||
|
||||
type RequestNotificationPermissionsCallback = Box<dyn FnOnce(RequestPermissionsOutcome) + Send>;
|
||||
type NotificationSendErrorCallback = Box<dyn FnOnce(NotificationSendError) + Send>;
|
||||
|
||||
/// Delegator that wraps platform-specific calls in a common API.
|
||||
pub struct AppDelegate {
|
||||
clipboard: Clipboard,
|
||||
dispatch_delegate: Arc<DispatchDelegate>,
|
||||
}
|
||||
|
||||
pub struct IntegrationTestDelegate {
|
||||
app_delegate: AppDelegate,
|
||||
clipboard: InMemoryClipboard,
|
||||
}
|
||||
|
||||
impl IntegrationTestDelegate {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(IntegrationTestDelegate {
|
||||
app_delegate: AppDelegate::new()?,
|
||||
clipboard: InMemoryClipboard::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::Delegate for IntegrationTestDelegate {
|
||||
#[cfg(feature = "test-util")]
|
||||
fn get_cursor_shape(&self) -> Cursor {
|
||||
self.app_delegate.get_cursor_shape()
|
||||
}
|
||||
|
||||
fn set_cursor_shape(&self, cursor: Cursor) {
|
||||
self.app_delegate.set_cursor_shape(cursor)
|
||||
}
|
||||
|
||||
fn open_url(&self, _: &str) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
fn open_file_path(&self, _: &Path) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
fn open_file_path_in_explorer(&self, _: &Path) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
fn open_file_picker(
|
||||
&self,
|
||||
_callback: FilePickerCallback,
|
||||
_file_picker_config: FilePickerConfiguration,
|
||||
) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
fn open_save_file_picker(
|
||||
&self,
|
||||
_callback: platform::SaveFilePickerCallback,
|
||||
_config: platform::SaveFilePickerConfiguration,
|
||||
) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
fn application_bundle_info(&self, _: &str) -> Option<ApplicationBundleInfo<'_>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn close_ime_async(&self, _window_id: WindowId) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
fn is_ime_open(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn open_character_palette(&self) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
fn set_accessibility_contents(&self, _: AccessibilityContent) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
fn request_user_attention(&self, _window_id: WindowId) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
fn clipboard(&mut self) -> &mut dyn crate::Clipboard {
|
||||
&mut self.clipboard
|
||||
}
|
||||
|
||||
fn request_desktop_notification_permissions(
|
||||
&self,
|
||||
_on_completion: platform::RequestNotificationPermissionsCallback,
|
||||
) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
fn send_desktop_notification(
|
||||
&self,
|
||||
_notification_content: UserNotification,
|
||||
_window_id: WindowId,
|
||||
_on_error: SendNotificationErrorCallback,
|
||||
) {
|
||||
}
|
||||
|
||||
fn system_theme(&self) -> platform::SystemTheme {
|
||||
self.app_delegate.system_theme()
|
||||
}
|
||||
|
||||
fn dispatch_delegate(&self) -> Arc<dyn platform::DispatchDelegate> {
|
||||
self.app_delegate.dispatch_delegate()
|
||||
}
|
||||
|
||||
fn register_global_shortcut(&self, shortcut: Keystroke) {
|
||||
self.app_delegate.register_global_shortcut(shortcut)
|
||||
}
|
||||
|
||||
fn unregister_global_shortcut(&self, shortcut: &Keystroke) {
|
||||
self.app_delegate.unregister_global_shortcut(shortcut)
|
||||
}
|
||||
|
||||
fn terminate_app(&self, termination_mode: TerminationMode) {
|
||||
self.app_delegate.terminate_app(termination_mode);
|
||||
}
|
||||
|
||||
fn is_screen_reader_enabled(&self) -> Option<bool> {
|
||||
self.app_delegate.is_screen_reader_enabled()
|
||||
}
|
||||
|
||||
fn microphone_access_state(&self) -> MicrophoneAccessState {
|
||||
self.app_delegate.microphone_access_state()
|
||||
}
|
||||
|
||||
fn show_native_platform_modal(&self, _id: ModalId, _modal: AlertDialog) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DispatchDelegate;
|
||||
|
||||
impl AppDelegate {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(AppDelegate {
|
||||
clipboard: Clipboard::new()?,
|
||||
dispatch_delegate: Arc::new(DispatchDelegate),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::Delegate for AppDelegate {
|
||||
/// Sets the cursor shape
|
||||
/// See https://developer.apple.com/documentation/appkit/nscursor?language=objc
|
||||
fn set_cursor_shape(&self, cursor: Cursor) {
|
||||
unsafe {
|
||||
let cursor: id = match cursor {
|
||||
Cursor::Arrow => msg_send![class!(NSCursor), arrowCursor],
|
||||
Cursor::IBeam => msg_send![class!(NSCursor), IBeamCursor],
|
||||
Cursor::Crosshair => msg_send![class!(NSCursor), crosshairCursor],
|
||||
Cursor::OpenHand => msg_send![class!(NSCursor), openHandCursor],
|
||||
Cursor::NotAllowed => msg_send![class!(NSCursor), operationNotAllowedCursor],
|
||||
Cursor::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
|
||||
Cursor::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||
Cursor::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||
Cursor::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
|
||||
Cursor::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
|
||||
};
|
||||
let () = msg_send![cursor, set];
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-util")]
|
||||
fn get_cursor_shape(&self) -> Cursor {
|
||||
unimplemented!("only implemented in tests")
|
||||
}
|
||||
fn open_url(&self, url: &str) {
|
||||
Window::open_url(url);
|
||||
}
|
||||
|
||||
fn open_file_path(&self, path: &Path) {
|
||||
Window::open_file_path(path);
|
||||
}
|
||||
|
||||
fn open_file_path_in_explorer(&self, path: &Path) {
|
||||
Window::open_file_path_in_explorer(path);
|
||||
}
|
||||
|
||||
fn open_file_picker(
|
||||
&self,
|
||||
callback: FilePickerCallback,
|
||||
file_picker_config: FilePickerConfiguration,
|
||||
) {
|
||||
Window::open_file_picker(callback, file_picker_config);
|
||||
}
|
||||
|
||||
fn open_save_file_picker(
|
||||
&self,
|
||||
callback: platform::SaveFilePickerCallback,
|
||||
config: platform::SaveFilePickerConfiguration,
|
||||
) {
|
||||
Window::open_save_file_picker(callback, config);
|
||||
}
|
||||
|
||||
fn application_bundle_info(
|
||||
&self,
|
||||
bundle_identifier: &str,
|
||||
) -> Option<ApplicationBundleInfo<'_>> {
|
||||
let bundle_path = unsafe {
|
||||
let nsstring =
|
||||
absolutePathForApplicationBundleWithIdentifier(make_nsstring(bundle_identifier));
|
||||
|
||||
if nsstring == nil {
|
||||
return None;
|
||||
}
|
||||
|
||||
nsstring_as_str(nsstring).ok()?
|
||||
};
|
||||
|
||||
let executable_path = unsafe {
|
||||
let nsstring = executableInApplicationBundleWithIdentifier(make_nsstring(bundle_path));
|
||||
|
||||
if nsstring == nil {
|
||||
None
|
||||
} else {
|
||||
nsstring_as_str(nsstring).map(Path::new).ok()
|
||||
}
|
||||
};
|
||||
|
||||
Some(ApplicationBundleInfo {
|
||||
path: Path::new(bundle_path),
|
||||
executable: executable_path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Open the macOS character palette.
|
||||
fn open_character_palette(&self) {
|
||||
// Open the character palette in a async task on the main thread to
|
||||
// ensure we don't double-borrow the app.
|
||||
dispatch::Queue::main().exec_async(move || unsafe {
|
||||
// See https://developer.apple.com/documentation/appkit/nsapplication/1428455-orderfrontcharacterpalette.
|
||||
// If the `sender` argument is nil, the palette is shown relative to the
|
||||
// first responder's cursor location. In our case, that will be the Warp
|
||||
// host view, with a location set via the `active_cursor_position` API.
|
||||
let () = msg_send![NSApp(), orderFrontCharacterPalette: nil];
|
||||
});
|
||||
}
|
||||
|
||||
fn close_ime_async(&self, window_id: WindowId) {
|
||||
Window::close_ime_async(window_id);
|
||||
}
|
||||
|
||||
fn is_ime_open(&self) -> bool {
|
||||
Window::is_ime_open()
|
||||
}
|
||||
|
||||
fn set_accessibility_contents(&self, content: AccessibilityContent) {
|
||||
Window::set_accessibility_contents(content);
|
||||
}
|
||||
|
||||
fn request_user_attention(&self, _window_id: WindowId) {
|
||||
unsafe {
|
||||
let () = msg_send![
|
||||
NSApp(),
|
||||
requestUserAttention: NSRequestUserAttentionType::NSInformationalRequest
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
fn request_desktop_notification_permissions(
|
||||
&self,
|
||||
on_completion_callback: platform::RequestNotificationPermissionsCallback,
|
||||
) {
|
||||
unsafe {
|
||||
let callback: RequestNotificationPermissionsCallback = Box::new(|outcome| {
|
||||
app::callback_dispatcher().with_mutable_app_context(|ctx| {
|
||||
on_completion_callback(outcome, ctx);
|
||||
})
|
||||
});
|
||||
requestNotificationPermissions(Box::into_raw(Box::new(callback)) as *const c_void);
|
||||
};
|
||||
}
|
||||
|
||||
fn send_desktop_notification(
|
||||
&self,
|
||||
notification_content: UserNotification,
|
||||
_window_id: WindowId,
|
||||
on_error_callback: SendNotificationErrorCallback,
|
||||
) {
|
||||
unsafe {
|
||||
let callback: NotificationSendErrorCallback = Box::new(|error| {
|
||||
app::callback_dispatcher().with_mutable_app_context(|ctx| {
|
||||
on_error_callback(error, ctx);
|
||||
})
|
||||
});
|
||||
sendNotification(
|
||||
make_nsstring(notification_content.title()),
|
||||
make_nsstring(notification_content.body()),
|
||||
make_nsstring(notification_content.data().unwrap_or_default()),
|
||||
Box::into_raw(Box::new(callback)) as *const c_void,
|
||||
if notification_content.play_sound() {
|
||||
YES
|
||||
} else {
|
||||
NO
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
fn clipboard(&mut self) -> &mut dyn crate::Clipboard {
|
||||
&mut self.clipboard
|
||||
}
|
||||
|
||||
fn system_theme(&self) -> platform::SystemTheme {
|
||||
unsafe {
|
||||
let dark_mode = isDarkMode();
|
||||
if dark_mode == YES {
|
||||
platform::SystemTheme::Dark
|
||||
} else {
|
||||
platform::SystemTheme::Light
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_delegate(&self) -> Arc<dyn platform::DispatchDelegate> {
|
||||
self.dispatch_delegate.clone()
|
||||
}
|
||||
|
||||
fn show_native_platform_modal(&self, id: ModalId, modal: AlertDialog) {
|
||||
let alert = create_native_platform_modal(modal);
|
||||
unsafe {
|
||||
let _: () = msg_send![app::get_warp_app(), showModal: alert modalId: id];
|
||||
}
|
||||
}
|
||||
|
||||
fn register_global_shortcut(&self, shortcut: Keystroke) {
|
||||
unsafe {
|
||||
for shortcut_key in Keycode::keycodes_from_key_name(&shortcut.key) {
|
||||
registerGlobalHotkey(shortcut_key.0.into(), modifier_code(&shortcut).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unregister_global_shortcut(&self, shortcut: &Keystroke) {
|
||||
unsafe {
|
||||
for shortcut_key in Keycode::keycodes_from_key_name(&shortcut.key) {
|
||||
unregisterGlobalHotkey(shortcut_key.0.into(), modifier_code(shortcut).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn terminate_app(&self, termination_mode: TerminationMode) {
|
||||
// Execute `[NSApp terminate]` asynchronously on the main thread to
|
||||
// ensure we don't accidentally run into any double-borrow errors.
|
||||
dispatch::Queue::main().exec_async(move || unsafe {
|
||||
match termination_mode {
|
||||
// ContentTransferred windows have already moved their content to another
|
||||
// window (e.g. during tab drag), so they can close immediately without
|
||||
// prompting the user for confirmation.
|
||||
TerminationMode::ForceTerminate | TerminationMode::ContentTransferred => {
|
||||
let _: () = msg_send![NSApp(), setForceTermination];
|
||||
}
|
||||
TerminationMode::Cancellable => {}
|
||||
}
|
||||
let _: () = msg_send![NSApp(), terminate: nil];
|
||||
});
|
||||
}
|
||||
|
||||
fn is_screen_reader_enabled(&self) -> Option<bool> {
|
||||
unsafe { Some(isVoiceOverEnabled() == YES) }
|
||||
}
|
||||
|
||||
fn microphone_access_state(&self) -> MicrophoneAccessState {
|
||||
unsafe {
|
||||
let cls = class!(AVCaptureDevice);
|
||||
// "soun" is not a typo, it's the correct constant name.
|
||||
let media_type_audio = make_nsstring("soun");
|
||||
|
||||
// AVAuthorizationStatus constants:
|
||||
// 0 = AVAuthorizationStatusNotDetermined - User has not yet made a choice
|
||||
// 1 = AVAuthorizationStatusRestricted - Restricted by system settings/parental controls
|
||||
// 2 = AVAuthorizationStatusDenied - User explicitly denied access
|
||||
// 3 = AVAuthorizationStatusAuthorized - User granted access
|
||||
let status: i32 = msg_send![cls, authorizationStatusForMediaType: media_type_audio];
|
||||
match status {
|
||||
0 => MicrophoneAccessState::NotDetermined,
|
||||
1 => MicrophoneAccessState::Restricted,
|
||||
2 => MicrophoneAccessState::Denied,
|
||||
3 => MicrophoneAccessState::Authorized,
|
||||
_ => MicrophoneAccessState::NotDetermined, // fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
/// # Safety
|
||||
/// This function is marked unsafe because it retrieves the pointer to the callback
|
||||
/// function that we sent down to the Objective-C code.
|
||||
pub unsafe extern "C-unwind" fn warp_on_request_notification_permissions_completed(
|
||||
result_type: NSUInteger,
|
||||
result_msg: id,
|
||||
callback: *mut c_void,
|
||||
) {
|
||||
let outcome =
|
||||
super::notification::request_permissions_outcome_from_native(result_type, result_msg);
|
||||
if let Ok(outcome) = outcome {
|
||||
let callback = Box::from_raw(callback as *mut RequestNotificationPermissionsCallback);
|
||||
callback(outcome);
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
/// # Safety
|
||||
/// This function is marked unsafe because it retrieves the pointer to the callback
|
||||
/// function that we sent down to the Objective-C code.
|
||||
pub unsafe extern "C-unwind" fn warp_on_notification_send_error(
|
||||
error_type: NSUInteger,
|
||||
error_msg: id,
|
||||
callback: *mut c_void,
|
||||
) {
|
||||
let notification_error = super::notification::send_error_from_native(error_type, error_msg);
|
||||
if let Ok(notification_error) = notification_error {
|
||||
let callback = Box::from_raw(callback as *mut NotificationSendErrorCallback);
|
||||
callback(notification_error);
|
||||
}
|
||||
}
|
||||
|
||||
impl platform::DispatchDelegate for DispatchDelegate {
|
||||
fn is_main_thread(&self) -> bool {
|
||||
let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] };
|
||||
is_main_thread == YES
|
||||
}
|
||||
|
||||
fn run_on_main_thread(&self, task: async_task::Runnable) {
|
||||
dispatch::Queue::main().exec_async(move || {
|
||||
task.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use std::{ffi::CStr, os::raw::c_char};
|
||||
|
||||
use galaxyui_core::event::{KeyEventDetails, ModifiersState};
|
||||
use galaxyui_core::platform::keyboard::{KeyCode, PhysicalKey};
|
||||
use galaxyui_core::{keymap::Keystroke, Event};
|
||||
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventType},
|
||||
base::{id, YES},
|
||||
foundation::NSString,
|
||||
};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
|
||||
use super::{
|
||||
keycode::{scancode_to_physicalkey, Keycode},
|
||||
utils::unicode_char_to_key,
|
||||
};
|
||||
|
||||
// Unpublished but widely known and stable flags for distinguishing left/right alt.
|
||||
// Google "NX_DEVICELALTKEYMASK" for more.
|
||||
const LEFT_ALT_MASK: NSUInteger = 0x00000020;
|
||||
const RIGHT_ALT_MASK: NSUInteger = 0x00000040;
|
||||
|
||||
fn modifier_flags_to_state(flags: NSEventModifierFlags) -> ModifiersState {
|
||||
ModifiersState {
|
||||
alt: flags.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
||||
cmd: flags.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
shift: flags.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||
ctrl: flags.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||
func: flags.contains(NSEventModifierFlags::NSFunctionKeyMask),
|
||||
}
|
||||
}
|
||||
|
||||
fn native_key_code_to_key_code(native_key_code: u16) -> Option<KeyCode> {
|
||||
let physical_key = scancode_to_physicalkey(native_key_code as u32);
|
||||
match physical_key {
|
||||
PhysicalKey::Code(key_code) => Some(key_code),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// This code is only unsafe since it requires interfacing with platform code.
|
||||
/// Creates an event from a native event, taking in the current window_height and whether this is
|
||||
/// the first mouse event on an inactive window that is causing the window to activate.
|
||||
pub unsafe fn from_native(
|
||||
native_event: id,
|
||||
window_height: Option<f32>,
|
||||
is_first_mouse: bool,
|
||||
) -> Option<Event> {
|
||||
let event_type = native_event.eventType();
|
||||
|
||||
// Filter out event types that aren't in the NSEventType enum.
|
||||
// See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details.
|
||||
match event_type as u64 {
|
||||
0 | 21 | 32 | 33 | 35 | 36 | 37 => {
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let modifiers = modifier_flags_to_state(native_event.modifierFlags());
|
||||
|
||||
match event_type {
|
||||
NSEventType::NSKeyDown => {
|
||||
let native_modifiers = native_event.modifierFlags();
|
||||
|
||||
// Get the base character for this key without any modifiers (including Shift)
|
||||
// using UCKeyTranslate via the platform's keyCodeToChar function.
|
||||
// For example, Shift+1 on a US keyboard gives '!' as the key, but
|
||||
// key_without_modifiers will be '1'.
|
||||
let key_without_modifiers = Keycode(native_event.keyCode()).try_to_key_name(false);
|
||||
|
||||
let details = KeyEventDetails {
|
||||
left_alt: (native_modifiers.bits() & LEFT_ALT_MASK) != 0,
|
||||
right_alt: (native_modifiers.bits() & RIGHT_ALT_MASK) != 0,
|
||||
key_without_modifiers,
|
||||
};
|
||||
let unmodified_chars = native_event.charactersIgnoringModifiers();
|
||||
let unmodified_chars = CStr::from_ptr(unmodified_chars.UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.ok()?;
|
||||
|
||||
let unmodified_chars = if let Some(first_char) = unmodified_chars.chars().next() {
|
||||
unicode_char_to_key(first_char as u16).unwrap_or(unmodified_chars)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let keystroke = Keystroke {
|
||||
ctrl: native_modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
|
||||
alt: native_modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
|
||||
shift: native_modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||
cmd: native_modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
meta: false, /* handled separately */
|
||||
key: unmodified_chars.into(),
|
||||
};
|
||||
|
||||
let chars = native_event.characters().UTF8String() as *mut c_char;
|
||||
let chars = if chars.is_null() {
|
||||
// `UTF8String` can return null in some rare cases where the
|
||||
// string isn't valid UTF-8. For example, if the user
|
||||
// enters a UTF-8 surrogate character, e.g. U+DDDD, via the
|
||||
// Unicode Hex Input keyboard, the conversion will produce
|
||||
// null.
|
||||
String::new()
|
||||
} else {
|
||||
CStr::from_ptr(chars).to_str().ok()?.to_owned()
|
||||
};
|
||||
|
||||
Some(Event::KeyDown {
|
||||
keystroke,
|
||||
chars,
|
||||
details,
|
||||
is_composing: false,
|
||||
})
|
||||
}
|
||||
NSEventType::NSMouseMoved => window_height.map(|window_height| Event::MouseMoved {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
cmd: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
shift: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||
is_synthetic: false,
|
||||
}),
|
||||
NSEventType::NSFlagsChanged => {
|
||||
let key_code = native_key_code_to_key_code(native_event.keyCode());
|
||||
|
||||
window_height.map(|window_height| Event::ModifierStateChanged {
|
||||
mouse_position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
modifiers,
|
||||
key_code,
|
||||
})
|
||||
}
|
||||
NSEventType::NSLeftMouseDown => window_height.map(|window_height| {
|
||||
let position = vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
);
|
||||
let click_count = native_event.clickCount() as u32;
|
||||
|
||||
// ctrl-click should actually be registered as a right-click
|
||||
// https://support.apple.com/guide/mac-help/right-click-mh35853/mac
|
||||
if modifiers.ctrl {
|
||||
Event::RightMouseDown {
|
||||
position,
|
||||
cmd: modifiers.cmd,
|
||||
shift: modifiers.shift,
|
||||
click_count,
|
||||
}
|
||||
} else {
|
||||
Event::LeftMouseDown {
|
||||
position,
|
||||
modifiers,
|
||||
click_count,
|
||||
is_first_mouse,
|
||||
}
|
||||
}
|
||||
}),
|
||||
NSEventType::NSLeftMouseUp => window_height.map(|window_height| Event::LeftMouseUp {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
modifiers,
|
||||
}),
|
||||
NSEventType::NSLeftMouseDragged => {
|
||||
window_height.map(|window_height| Event::LeftMouseDragged {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
modifiers,
|
||||
})
|
||||
}
|
||||
// TODO: This option is deprecated by Apple in favour of NSEventTypeOtherMouseDown
|
||||
// but we'll likely need to update cocoa.
|
||||
// See https://developer.apple.com/documentation/appkit/nsothermousedown.
|
||||
NSEventType::NSOtherMouseDown => {
|
||||
let window_height = window_height?;
|
||||
let window_location = native_event.locationInWindow();
|
||||
let position = vec2f(
|
||||
window_location.x as f32,
|
||||
window_height - (window_location.y as f32),
|
||||
);
|
||||
let modifier_flags = native_event.modifierFlags();
|
||||
let cmd = modifier_flags.contains(NSEventModifierFlags::NSCommandKeyMask);
|
||||
let shift = modifier_flags.contains(NSEventModifierFlags::NSShiftKeyMask);
|
||||
let click_count = native_event.clickCount() as u32;
|
||||
|
||||
match native_event.buttonNumber() {
|
||||
2 => Some(Event::MiddleMouseDown {
|
||||
position,
|
||||
cmd,
|
||||
shift,
|
||||
click_count,
|
||||
}),
|
||||
3 => Some(Event::BackMouseDown {
|
||||
position,
|
||||
cmd,
|
||||
shift,
|
||||
click_count,
|
||||
}),
|
||||
4 => Some(Event::ForwardMouseDown {
|
||||
position,
|
||||
cmd,
|
||||
shift,
|
||||
click_count,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
// For trackpads, this event will get triggered by the user's secondary click setting.
|
||||
NSEventType::NSRightMouseDown => window_height.map(|window_height| Event::RightMouseDown {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
cmd: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSCommandKeyMask),
|
||||
shift: native_event
|
||||
.modifierFlags()
|
||||
.contains(NSEventModifierFlags::NSShiftKeyMask),
|
||||
click_count: native_event.clickCount() as u32,
|
||||
}),
|
||||
NSEventType::NSScrollWheel => window_height.map(|window_height| Event::ScrollWheel {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
delta: vec2f(
|
||||
native_event.scrollingDeltaX() as f32,
|
||||
native_event.scrollingDeltaY() as f32,
|
||||
),
|
||||
precise: native_event.hasPreciseScrollingDeltas() == YES,
|
||||
modifiers,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,656 @@
|
||||
use super::text_layout::{layout_line, layout_text};
|
||||
use crate::fonts::font_kit::{properties_to_font_kit, Rasterizer};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use core_foundation::array::{CFArray, CFArrayRef};
|
||||
use core_foundation::base::{CFType, ItemRef, TCFType};
|
||||
use core_foundation::dictionary::CFDictionary;
|
||||
use core_foundation::string::{CFString, CFStringRef, UniChar};
|
||||
use core_graphics::display::CGSize;
|
||||
use core_graphics::font::CGGlyph;
|
||||
use core_text::font::{cascade_list_for_languages as ct_cascade_list_for_languages, CTFont};
|
||||
use core_text::font_descriptor::{
|
||||
kCTFontFamilyNameAttribute, kCTFontLanguagesAttribute, kCTFontNameAttribute,
|
||||
kCTFontOrientationHorizontal, CTFontDescriptor, CTFontDescriptorCopyAttribute,
|
||||
SymbolicTraitAccessors, TraitAccessors,
|
||||
};
|
||||
use core_text::{font, font_collection, font_descriptor};
|
||||
use dashmap::{mapref::entry::Entry, DashMap};
|
||||
use font_kit::font::Font;
|
||||
use font_kit::loaders::core_text::NativeFont;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::FutureExt as _;
|
||||
use itertools::Itertools as _;
|
||||
use ordered_float::OrderedFloat;
|
||||
use pathfinder_geometry::rect::RectI;
|
||||
use pathfinder_geometry::vector::{Vector2F, Vector2I};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use galaxyui_core::fonts::{
|
||||
canvas::RasterFormat, FamilyId, FontId, FontInfo, GlyphId, Metrics, Properties,
|
||||
RasterizedGlyph, SubpixelAlignment,
|
||||
};
|
||||
use galaxyui_core::platform::{self, FontDB as _, LineStyle, TextLayoutSystem};
|
||||
use galaxyui_core::rendering;
|
||||
use galaxyui_core::text_layout::{ClipConfig, StyleAndFont, TextAlignment, TextFrame};
|
||||
|
||||
struct FontFamily {
|
||||
name: String,
|
||||
fonts: Vec<Font>,
|
||||
}
|
||||
|
||||
mod loader {
|
||||
use super::*;
|
||||
|
||||
// Font-kit loads fonts by copying their font file into the running memory, which
|
||||
// is extremely inefficient. Thus, for system fonts, we load these fonts by reference
|
||||
// through CTFontDescriptorCreateWithAttributes function and create a dummy font-kit
|
||||
// Font interface to access its functions.
|
||||
pub fn load_system_font(font_family: &str) -> Result<FontFamily> {
|
||||
let Some(descriptors) = FontDB::descriptors_for_family(font_family) else {
|
||||
bail!(
|
||||
"could not find a non-empty font family matching one of the given names {:?}",
|
||||
font_family
|
||||
);
|
||||
};
|
||||
let mut fonts = Vec::with_capacity(descriptors.len() as usize);
|
||||
for fontdesc in descriptors.into_iter() {
|
||||
// The font size here does not affect our rendering. In CTFont, pt_size
|
||||
// is used to calculate font metrics like ascent, descent, etc. However,
|
||||
// font-kit creates its own layer of calculating font metrics at render time
|
||||
// so we just need a place-holder here for getting the CTFont object. Use
|
||||
// 16.0 here as it is consistent with https://docs.rs/core-text/19.2.0/src/core_text/font.rs.html#130
|
||||
let font = Font::from_ct_font(font::new_from_descriptor(&fontdesc, DEFAULT_FONT_SIZE));
|
||||
|
||||
let glyph_id = font.glyph_for_char('m');
|
||||
if glyph_id.is_none() {
|
||||
return Err(anyhow!("font must contain a glyph for the 'm' character"));
|
||||
}
|
||||
|
||||
fonts.push(font);
|
||||
}
|
||||
Ok(FontFamily {
|
||||
fonts,
|
||||
name: font_family.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_all_system_fonts() -> LoadedSystemFonts {
|
||||
let collection = font_collection::create_for_all_families();
|
||||
let Some(descriptors) = collection.get_descriptors() else {
|
||||
return LoadedSystemFonts(vec![]);
|
||||
};
|
||||
|
||||
let mut fonts: Vec<(FontInfo, FontFamily)> = Vec::with_capacity(descriptors.len() as usize);
|
||||
for descriptor in descriptors.iter() {
|
||||
let name = match unsafe { FontDB::get_family_name(&descriptor) } {
|
||||
Some(family) => family,
|
||||
None => {
|
||||
log::warn!("Failed to load the font as it does not have a valid family name.");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let internal_name = match unsafe { FontDB::get_font_name(&descriptor) } {
|
||||
Some(family) => family,
|
||||
None => {
|
||||
log::warn!("Failed to load the font as it does not have a valid font name.");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// We should only load languages that support english
|
||||
if !FontDB::supports_english(&descriptor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(idx) = fonts.iter().position(|font| font.0.family_name == name) {
|
||||
// Some font families (e.g. Osaka) could contains both monospace
|
||||
// and variable-width fonts. To make sure the returned font family
|
||||
// info is consistent everytime, we set is_monospace to true if the
|
||||
// family contains one font that is monospace.
|
||||
if descriptor.traits().symbolic_traits().is_monospace()
|
||||
&& !fonts[idx].0.is_monospace
|
||||
{
|
||||
// Updating here since font family names are guaranteed to be unique.
|
||||
fonts[idx].0.is_monospace = true;
|
||||
}
|
||||
// Since we keep track of font families, add this font as a possible style.
|
||||
fonts[idx].0.font_names.push(internal_name)
|
||||
} else {
|
||||
let font_family = match load_system_font(&name) {
|
||||
Ok(font) => font,
|
||||
Err(err) => {
|
||||
log::debug!("Failed to load {}: {:?}", name.as_str(), err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
fonts.push((
|
||||
FontInfo {
|
||||
family_name: name,
|
||||
font_names: vec![internal_name],
|
||||
is_monospace: descriptor.traits().symbolic_traits().is_monospace(),
|
||||
},
|
||||
font_family,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
LoadedSystemFonts(fonts)
|
||||
}
|
||||
|
||||
// We use font-kit's family handle to load fonts that come with Warp as
|
||||
// these binaries are already in memory and won't increase our memory load.
|
||||
pub fn load_font_family_from_bytes(name: &str, font_bytes: Vec<Vec<u8>>) -> Result<FontFamily> {
|
||||
let mut fonts = Vec::with_capacity(font_bytes.len());
|
||||
|
||||
for font in font_bytes {
|
||||
let Ok(font) = Font::from_bytes(Arc::new(font), 0) else {
|
||||
log::info!("Unable to parse font bytes for font {name:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
let glyph_id = font.glyph_for_char('m');
|
||||
if glyph_id.is_none() {
|
||||
return Err(anyhow!("font must contain a glyph for the 'm' character"));
|
||||
}
|
||||
fonts.push(font);
|
||||
}
|
||||
|
||||
Ok(FontFamily {
|
||||
fonts,
|
||||
name: name.to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadedSystemFonts(Vec<(FontInfo, FontFamily)>);
|
||||
|
||||
impl platform::LoadedSystemFonts for LoadedSystemFonts {
|
||||
fn as_any(self: Box<Self>) -> Box<dyn Any> {
|
||||
self as Box<dyn Any>
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FontDB {
|
||||
next_family_id: AtomicUsize,
|
||||
families: HashMap<FamilyId, Family>,
|
||||
next_font_id: AtomicUsize,
|
||||
rasterizer: Rasterizer,
|
||||
font_names: DashMap<FontId, Arc<String>>,
|
||||
native_fonts: DashMap<(FontId, OrderedFloat<f32>), NativeFont>,
|
||||
fonts_by_name: DashMap<Arc<String>, FontId>,
|
||||
fallback_fonts: DashMap<FontId, Arc<Vec<FontId>>>,
|
||||
metrics: DashMap<FontId, Metrics>,
|
||||
font_selections: DashMap<(FamilyId, Properties), FontId>,
|
||||
space_advances: DashMap<FontId, Option<f64>>,
|
||||
}
|
||||
|
||||
struct Family {
|
||||
name: String,
|
||||
font_ids: Vec<FontId>,
|
||||
}
|
||||
|
||||
impl Default for FontDB {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_FONT_SIZE: f64 = 16.0;
|
||||
|
||||
// Returns the horizontal advance (in points) of a single space in the given font.
|
||||
fn space_advance_width(font: &CTFont) -> Option<f64> {
|
||||
let space_char: UniChar = ' ' as u16;
|
||||
let mut glyph: CGGlyph = 0;
|
||||
|
||||
let ok =
|
||||
unsafe { font.get_glyphs_for_characters(&space_char as *const UniChar, &mut glyph, 1) };
|
||||
if !ok || glyph == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut advance = CGSize {
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
};
|
||||
unsafe {
|
||||
font.get_advances_for_glyphs(
|
||||
kCTFontOrientationHorizontal,
|
||||
&glyph as *const CGGlyph,
|
||||
&mut advance as *mut CGSize,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
let width = advance.width;
|
||||
(width.is_finite() && width > 0.0).then_some(width)
|
||||
}
|
||||
|
||||
impl FontDB {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
next_family_id: Default::default(),
|
||||
families: Default::default(),
|
||||
next_font_id: Default::default(),
|
||||
rasterizer: Rasterizer::new(),
|
||||
font_names: Default::default(),
|
||||
native_fonts: Default::default(),
|
||||
fonts_by_name: Default::default(),
|
||||
fallback_fonts: Default::default(),
|
||||
metrics: Default::default(),
|
||||
font_selections: Default::default(),
|
||||
space_advances: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// This functions the same as the family_name method in core text font descriptor, but it returns
|
||||
// None instead of panicking when the descriptor does not include the family_name attribute.
|
||||
unsafe fn get_family_name(descriptor: &ItemRef<CTFontDescriptor>) -> Option<String> {
|
||||
let value = CTFontDescriptorCopyAttribute(
|
||||
descriptor.as_concrete_TypeRef(),
|
||||
kCTFontFamilyNameAttribute,
|
||||
);
|
||||
if value.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let value = CFType::wrap_under_create_rule(value);
|
||||
let s = CFString::wrap_under_get_rule(value.as_CFTypeRef() as CFStringRef);
|
||||
Some(s.to_string())
|
||||
}
|
||||
|
||||
unsafe fn get_font_name(descriptor: &ItemRef<CTFontDescriptor>) -> Option<String> {
|
||||
let value =
|
||||
CTFontDescriptorCopyAttribute(descriptor.as_concrete_TypeRef(), kCTFontNameAttribute);
|
||||
if value.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let value = CFType::wrap_under_create_rule(value);
|
||||
let s = CFString::wrap_under_get_rule(value.as_CFTypeRef() as CFStringRef);
|
||||
Some(s.to_string())
|
||||
}
|
||||
|
||||
pub fn fallback_fonts(&self, font_id: FontId) -> Vec<FontId> {
|
||||
self.fallback_fonts
|
||||
.get(&font_id)
|
||||
.expect("Font fallback should not be empty")
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
// Check if the font family supports english.
|
||||
fn supports_english(descriptor: &ItemRef<CTFontDescriptor>) -> bool {
|
||||
unsafe {
|
||||
let value = CTFontDescriptorCopyAttribute(
|
||||
descriptor.as_concrete_TypeRef(),
|
||||
kCTFontLanguagesAttribute,
|
||||
) as CFArrayRef;
|
||||
|
||||
if value.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let languages: CFArray<CFString> = CFArray::wrap_under_create_rule(value);
|
||||
languages.iter().any(|s| *s == "en")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_family_name_from_id(&self, id: FamilyId) -> Option<String> {
|
||||
self.families.get(&id).map(|s| s.name.clone())
|
||||
}
|
||||
|
||||
fn create_new_family_id(&self) -> FamilyId {
|
||||
FamilyId(self.next_family_id.fetch_add(1, Ordering::SeqCst))
|
||||
}
|
||||
|
||||
/// Return fallback descriptors for font/language list.
|
||||
/// Heavily inspired by crossfont's implementation:
|
||||
/// https://github.com/alacritty/crossfont/blob/d3515de22494c6fa70d84d2a9264c10097e303bd/src/darwin/mod.rs#L288
|
||||
fn cascade_list_for_languages(&self, ct_font: &CTFont, languages: &[&str]) -> Vec<FontId> {
|
||||
// Convert language type &Vec<String> -> CFArray.
|
||||
let langarr: CFArray<CFString> = {
|
||||
let tmp: Vec<CFString> = languages
|
||||
.iter()
|
||||
.map(|language| CFString::new(language))
|
||||
.collect();
|
||||
CFArray::from_CFTypes(&tmp)
|
||||
};
|
||||
|
||||
// CFArray of CTFontDescriptorRef (again).
|
||||
let list = ct_cascade_list_for_languages(ct_font, &langarr);
|
||||
|
||||
let mut fallback_fonts: Vec<FontId> = list
|
||||
.into_iter()
|
||||
.filter_map(|fontdesc| self.descriptor_to_font_id(fontdesc))
|
||||
.collect();
|
||||
|
||||
// While .Apple Symbols Fallback is not a valid font. Apple Symbols is and it provides
|
||||
// many fallback characters. This implementation is consistent with Alacritty:
|
||||
// See: https://github.com/alacritty/crossfont/blob/d3515de22494c6fa70d84d2a9264c10097e303bd/src/darwin/mod.rs#L91
|
||||
if let Some(font) = FontDB::descriptors_for_family("Apple Symbols")
|
||||
.as_ref()
|
||||
.and_then(|descriptor| descriptor.into_iter().next())
|
||||
.and_then(|font_descriptor| self.descriptor_to_font_id(font_descriptor))
|
||||
{
|
||||
fallback_fonts.push(font);
|
||||
}
|
||||
|
||||
fallback_fonts
|
||||
}
|
||||
|
||||
// Get a list of CTFontDescriptors for a font family.
|
||||
fn descriptors_for_family(name: &str) -> Option<CFArray<CTFontDescriptor>> {
|
||||
let attributes: CFDictionary<CFString, CFType> = CFDictionary::from_CFType_pairs(&[(
|
||||
CFString::new("NSFontFamilyAttribute"),
|
||||
CFString::new(name).as_CFType(),
|
||||
)]);
|
||||
|
||||
let descriptor = font_descriptor::new_from_attributes(&attributes);
|
||||
let collection_descriptors = &CFArray::from_CFTypes(&[descriptor]);
|
||||
let collection = font_collection::new_from_descriptors(collection_descriptors);
|
||||
|
||||
collection.get_descriptors()
|
||||
}
|
||||
|
||||
// Convert a CTFontDescriptor to font_id. This function does not load fallback fonts
|
||||
// and assumes the descriptor refers to a valid system font.
|
||||
fn descriptor_to_font_id(&self, fontdesc: ItemRef<CTFontDescriptor>) -> Option<FontId> {
|
||||
let name = match unsafe { FontDB::get_family_name(&fontdesc) } {
|
||||
Some(family) => family,
|
||||
None => {
|
||||
log::warn!("Failed to load the font as it does not have a valid family name.");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let font_name = match unsafe { FontDB::get_font_name(&fontdesc) } {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
log::warn!("Failed to load the font as it does not have a valid name.");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// We should not load fonts with name that starts with dot
|
||||
// https://developer.apple.com/videos/play/wwdc2019/227/?time=200
|
||||
(!name.starts_with('.')).then(|| {
|
||||
// Check if the fallback font is in cache.
|
||||
match self.fonts_by_name.entry(Arc::new(font_name)) {
|
||||
Entry::Occupied(entry) => return *entry.get(),
|
||||
Entry::Vacant(_) => (),
|
||||
}
|
||||
|
||||
// We need to push font after releasing the entry of the dashmap to prevent deadlocks.
|
||||
self.push_font(Font::from_ct_font(font::new_from_descriptor(
|
||||
&fontdesc,
|
||||
DEFAULT_FONT_SIZE,
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_font(&self, family_id: FamilyId, properties: Properties) -> FontId {
|
||||
match self.font_selections.entry((family_id, properties)) {
|
||||
Entry::Occupied(entry) => *entry.get(),
|
||||
Entry::Vacant(entry) => {
|
||||
let family = &self
|
||||
.families
|
||||
.get(&family_id)
|
||||
.expect("FamilyId must correspond to a valid family");
|
||||
let candidates = family
|
||||
.font_ids
|
||||
.iter()
|
||||
.map(|font_id| self.font(*font_id).properties())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let font_id = {
|
||||
if let Ok(idx) = font_kit::matching::find_best_match(
|
||||
&candidates,
|
||||
&properties_to_font_kit(properties),
|
||||
) {
|
||||
self.font(family.font_ids[idx]).properties();
|
||||
family.font_ids[idx]
|
||||
} else {
|
||||
font_kit::matching::find_best_match(&candidates, &Default::default())
|
||||
.map(|idx| family.font_ids[idx])
|
||||
.unwrap_or(family.font_ids[0])
|
||||
}
|
||||
};
|
||||
|
||||
// Make sure we've loaded fallback fonts for the selected font.
|
||||
if !self.fallback_fonts.contains_key(&font_id) {
|
||||
self.fallback_fonts.insert(
|
||||
font_id,
|
||||
Arc::new(self.cascade_list_for_languages(
|
||||
&self.font(font_id).native_font(),
|
||||
&["en"],
|
||||
)),
|
||||
);
|
||||
}
|
||||
*entry.insert(font_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn font(&self, font_id: FontId) -> Arc<Font> {
|
||||
self.rasterizer.font_for_id(font_id)
|
||||
}
|
||||
|
||||
pub fn native_font(&self, font_id: FontId, size: f32) -> NativeFont {
|
||||
match self.native_fonts.entry((font_id, OrderedFloat(size))) {
|
||||
Entry::Occupied(entry) => entry.get().clone(),
|
||||
Entry::Vacant(entry) => entry
|
||||
.insert(
|
||||
self.rasterizer
|
||||
.font_for_id(font_id)
|
||||
.native_font()
|
||||
.clone_with_font_size(size as f64),
|
||||
)
|
||||
.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the horizontal advance of a space character at the given size, or `None` if the
|
||||
/// advance could not be measured. Uses a cached reference advance (measured at
|
||||
/// `DEFAULT_FONT_SIZE`) scaled linearly to `size`.
|
||||
pub fn space_advance_width(&self, font_id: FontId, size: f32) -> Option<f64> {
|
||||
let stored = *self.space_advances.get(&font_id)?;
|
||||
stored.map(|a| a * size as f64 / DEFAULT_FONT_SIZE)
|
||||
}
|
||||
|
||||
pub fn font_id_for_native_font(&self, native_font: NativeFont) -> FontId {
|
||||
let postscript_name = native_font.postscript_name();
|
||||
if let Some(font_id) = self.fonts_by_name.get(&postscript_name).as_ref() {
|
||||
return *font_id.value();
|
||||
}
|
||||
|
||||
self.push_font(Font::from_ct_font(native_font))
|
||||
}
|
||||
|
||||
fn push_font(&self, font: Font) -> FontId {
|
||||
let name = Arc::new(font.postscript_name().unwrap());
|
||||
let font_id = FontId(self.next_font_id.fetch_add(1, Ordering::SeqCst));
|
||||
|
||||
let ct_font = font.native_font().clone_with_font_size(DEFAULT_FONT_SIZE);
|
||||
let advance = space_advance_width(&ct_font);
|
||||
|
||||
self.rasterizer.insert(font_id, Arc::new(font));
|
||||
self.font_names.insert(font_id, name.clone());
|
||||
self.fonts_by_name.insert(name, font_id);
|
||||
self.space_advances.insert(font_id, advance);
|
||||
font_id
|
||||
}
|
||||
|
||||
fn insert_font_family(&mut self, font_family: FontFamily) -> Result<FamilyId> {
|
||||
if let Some(family_id) = self.family_id_for_name(&font_family.name) {
|
||||
return Ok(family_id);
|
||||
}
|
||||
|
||||
let font_ids = font_family
|
||||
.fonts
|
||||
.into_iter()
|
||||
.map(|font| self.push_font(font));
|
||||
|
||||
let family_id = self.create_new_family_id();
|
||||
self.families.insert(
|
||||
family_id,
|
||||
Family {
|
||||
name: font_family.name,
|
||||
font_ids: font_ids.collect(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(family_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::platform::FontDB for FontDB {
|
||||
fn load_from_bytes(&mut self, name: &str, bytes: Vec<Vec<u8>>) -> Result<FamilyId> {
|
||||
let family = loader::load_font_family_from_bytes(name, bytes)?;
|
||||
self.insert_font_family(family)
|
||||
}
|
||||
|
||||
fn load_from_system(&mut self, font_family: &str) -> Result<FamilyId> {
|
||||
let family = loader::load_system_font(font_family)?;
|
||||
self.insert_font_family(family)
|
||||
}
|
||||
|
||||
fn load_all_system_fonts(&self) -> BoxFuture<'static, Box<dyn platform::LoadedSystemFonts>> {
|
||||
async { Box::new(loader::load_all_system_fonts()) as Box<dyn platform::LoadedSystemFonts> }
|
||||
.boxed()
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
loaded_system_fonts
|
||||
.0
|
||||
.into_iter()
|
||||
.flat_map(|(font_info, family)| {
|
||||
let family_id = self.insert_font_family(family).ok()?;
|
||||
Some((Some(family_id), font_info))
|
||||
})
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
fn fallback_fonts(&self, _ch: char, font_id: FontId) -> Vec<FontId> {
|
||||
self.fallback_fonts(font_id)
|
||||
}
|
||||
|
||||
fn load_family_name_from_id(&self, id: FamilyId) -> Option<String> {
|
||||
self.load_family_name_from_id(id)
|
||||
}
|
||||
|
||||
fn select_font(&self, family_id: FamilyId, properties: Properties) -> FontId {
|
||||
self.select_font(family_id, properties)
|
||||
}
|
||||
|
||||
fn font_metrics(&self, font_id: FontId) -> Metrics {
|
||||
match self.metrics.entry(font_id) {
|
||||
Entry::Occupied(entry) => *entry.get(),
|
||||
Entry::Vacant(entry) => *entry.insert(self.font(font_id).metrics().into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn glyph_advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Vector2I> {
|
||||
Ok(self.font(font_id).advance(glyph_id)?.to_i32())
|
||||
}
|
||||
|
||||
fn glyph_raster_bounds(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
point_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
scale: Vector2F,
|
||||
glyph_config: &rendering::GlyphConfig,
|
||||
) -> Result<RectI> {
|
||||
self.rasterizer
|
||||
.glyph_raster_bounds(font_id, point_size, glyph_id, scale, glyph_config)
|
||||
}
|
||||
|
||||
fn glyph_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<RectI> {
|
||||
Ok(self.font(font_id).typographic_bounds(glyph_id)?.to_i32())
|
||||
}
|
||||
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
point_size: f32,
|
||||
glyph_id: GlyphId,
|
||||
scale: Vector2F,
|
||||
subpixel_alignment: SubpixelAlignment,
|
||||
glyph_config: &rendering::GlyphConfig,
|
||||
format: RasterFormat,
|
||||
) -> Result<RasterizedGlyph> {
|
||||
self.rasterizer.rasterize_glyph(
|
||||
font_id,
|
||||
point_size,
|
||||
glyph_id,
|
||||
scale,
|
||||
subpixel_alignment,
|
||||
glyph_config,
|
||||
format,
|
||||
)
|
||||
}
|
||||
|
||||
fn glyph_for_char(&self, font: FontId, char: char) -> Option<GlyphId> {
|
||||
self.font(font).glyph_for_char(char)
|
||||
}
|
||||
|
||||
fn family_id_for_name(&self, name: &str) -> Option<FamilyId> {
|
||||
self.families
|
||||
.iter()
|
||||
.find(|(_, f)| f.name == name)
|
||||
.map(|(id, _)| *id)
|
||||
}
|
||||
|
||||
fn text_layout_system(&self) -> &dyn TextLayoutSystem {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::platform::TextLayoutSystem for FontDB {
|
||||
fn layout_line(
|
||||
&self,
|
||||
text: &str,
|
||||
line_style: LineStyle,
|
||||
style_runs: &[(Range<usize>, StyleAndFont)],
|
||||
_max_width: f32,
|
||||
clip_config: ClipConfig,
|
||||
) -> crate::text_layout::Line {
|
||||
layout_line(text, line_style, style_runs, self, clip_config)
|
||||
}
|
||||
|
||||
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 {
|
||||
layout_text(
|
||||
text,
|
||||
line_style,
|
||||
style_runs,
|
||||
self,
|
||||
max_width,
|
||||
max_height,
|
||||
alignment,
|
||||
first_line_head_indent,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use cocoa::foundation::{NSPoint, NSRect, NSSize};
|
||||
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
|
||||
pub trait Vector2FExt {
|
||||
fn to_ns_point(&self) -> NSPoint;
|
||||
fn to_ns_size(&self) -> NSSize;
|
||||
}
|
||||
|
||||
pub trait RectFExt {
|
||||
fn to_ns_rect(&self) -> NSRect;
|
||||
}
|
||||
|
||||
impl Vector2FExt for Vector2F {
|
||||
fn to_ns_point(&self) -> NSPoint {
|
||||
NSPoint::new(self.x() as f64, self.y() as f64)
|
||||
}
|
||||
|
||||
fn to_ns_size(&self) -> NSSize {
|
||||
NSSize::new(self.x() as f64, self.y() as f64)
|
||||
}
|
||||
}
|
||||
|
||||
impl RectFExt for RectF {
|
||||
fn to_ns_rect(&self) -> NSRect {
|
||||
NSRect::new(self.origin().to_ns_point(), self.size().to_ns_size())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
use std::slice;
|
||||
|
||||
use cocoa::{
|
||||
base::{id, nil, BOOL},
|
||||
foundation::{NSArray, NSString, NSUInteger},
|
||||
};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::keyboard::{KeyCode, NativeKeyCode, PhysicalKey};
|
||||
|
||||
use super::make_nsstring;
|
||||
|
||||
// Modifier key mask values for the Carbon API.
|
||||
pub const CMD_KEY: u16 = 256;
|
||||
pub const SHIFT_KEY: u16 = 512;
|
||||
pub const OPTION_KEY: u16 = 2048;
|
||||
pub const CONTROL_KEY: u16 = 4096;
|
||||
|
||||
extern "C" {
|
||||
fn charToKeyCodes(keyChar: id) -> id;
|
||||
fn keyCodeToChar(keyCode: NSUInteger, shifted: BOOL) -> id;
|
||||
}
|
||||
|
||||
pub struct Keycode(pub u16);
|
||||
|
||||
impl Keycode {
|
||||
pub fn try_to_key_name(self, shift_key_pressed: bool) -> Option<String> {
|
||||
unsafe {
|
||||
// The underlying core-foundation library interprets objc BOOL type as bool
|
||||
// in aarch machines but as i8 in intel machines so we need to call .into here.
|
||||
// But clippy isn't smart enough to know that so we silence it here for now.
|
||||
#[allow(clippy::useless_conversion)]
|
||||
let key = keyCodeToChar(self.0 as u64, shift_key_pressed.into());
|
||||
|
||||
if key == nil {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cstr = key.UTF8String() as *const u8;
|
||||
std::str::from_utf8(slice::from_raw_parts(cstr, key.len()))
|
||||
.ok()
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// There could have multiple keycodes mapping to one virtual key. Return an iterator
|
||||
// to all possible values of keycode here.
|
||||
pub fn keycodes_from_key_name(key_name: &str) -> impl Iterator<Item = Keycode> {
|
||||
unsafe {
|
||||
let keycodes: id = charToKeyCodes(make_nsstring(key_name));
|
||||
let keycodes_length = keycodes.count();
|
||||
|
||||
(0..keycodes_length).map(move |i| {
|
||||
let keycode: NSUInteger =
|
||||
msg_send![keycodes.objectAtIndex(i), unsignedIntegerValue];
|
||||
Self(keycode as u16)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert modifier flags to Carbon style modifier key mask.
|
||||
pub fn modifier_code(keystroke: &Keystroke) -> u16 {
|
||||
let mut code = 0;
|
||||
if keystroke.alt {
|
||||
code |= OPTION_KEY;
|
||||
}
|
||||
|
||||
if keystroke.cmd {
|
||||
code |= CMD_KEY;
|
||||
}
|
||||
|
||||
if keystroke.shift {
|
||||
code |= SHIFT_KEY;
|
||||
}
|
||||
|
||||
if keystroke.ctrl {
|
||||
code |= CONTROL_KEY;
|
||||
}
|
||||
|
||||
code
|
||||
}
|
||||
|
||||
// The following types and functions are taken from winit's appkit implementation.
|
||||
// We redefine them here to avoid needing to include the entirety of winit as a dependency for MacOS.
|
||||
// --------------------------------------------------------------------------------------------------------
|
||||
|
||||
/// Converts a scancode to a physical key. Logic is taken from winit appkit code.
|
||||
pub(crate) fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey {
|
||||
// Follows what Chromium and Firefox do:
|
||||
// https://chromium.googlesource.com/chromium/src.git/+/3e1a26c44c024d97dc9a4c09bbc6a2365398ca2c/ui/events/keycodes/dom/dom_code_data.inc
|
||||
// https://searchfox.org/mozilla-central/rev/c597e9c789ad36af84a0370d395be066b7dc94f4/widget/NativeKeyToDOMCodeName.h
|
||||
//
|
||||
// See also:
|
||||
// Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
|
||||
//
|
||||
// Also see https://developer.apple.com/documentation/appkit/function-key-unicode-values:
|
||||
//
|
||||
// > the system handles some function keys at a lower level and your app never sees them.
|
||||
// > Examples include the Volume Up key, Volume Down key, Volume Mute key, Eject key, and
|
||||
// > Function key found on many Macs.
|
||||
//
|
||||
// So the handling of some of these is mostly for show.
|
||||
PhysicalKey::Code(match scancode {
|
||||
0x00 => KeyCode::KeyA,
|
||||
0x01 => KeyCode::KeyS,
|
||||
0x02 => KeyCode::KeyD,
|
||||
0x03 => KeyCode::KeyF,
|
||||
0x04 => KeyCode::KeyH,
|
||||
0x05 => KeyCode::KeyG,
|
||||
0x06 => KeyCode::KeyZ,
|
||||
0x07 => KeyCode::KeyX,
|
||||
0x08 => KeyCode::KeyC,
|
||||
0x09 => KeyCode::KeyV,
|
||||
// This key is typically located near LeftShift key, roughly the same location as backquote
|
||||
// (`) on Windows' US layout.
|
||||
//
|
||||
// The keycap varies on international keyboards.
|
||||
0x0a => KeyCode::IntlBackslash,
|
||||
0x0b => KeyCode::KeyB,
|
||||
0x0c => KeyCode::KeyQ,
|
||||
0x0d => KeyCode::KeyW,
|
||||
0x0e => KeyCode::KeyE,
|
||||
0x0f => KeyCode::KeyR,
|
||||
0x10 => KeyCode::KeyY,
|
||||
0x11 => KeyCode::KeyT,
|
||||
0x12 => KeyCode::Digit1,
|
||||
0x13 => KeyCode::Digit2,
|
||||
0x14 => KeyCode::Digit3,
|
||||
0x15 => KeyCode::Digit4,
|
||||
0x16 => KeyCode::Digit6,
|
||||
0x17 => KeyCode::Digit5,
|
||||
0x18 => KeyCode::Equal,
|
||||
0x19 => KeyCode::Digit9,
|
||||
0x1a => KeyCode::Digit7,
|
||||
0x1b => KeyCode::Minus,
|
||||
0x1c => KeyCode::Digit8,
|
||||
0x1d => KeyCode::Digit0,
|
||||
0x1e => KeyCode::BracketRight,
|
||||
0x1f => KeyCode::KeyO,
|
||||
0x20 => KeyCode::KeyU,
|
||||
0x21 => KeyCode::BracketLeft,
|
||||
0x22 => KeyCode::KeyI,
|
||||
0x23 => KeyCode::KeyP,
|
||||
0x24 => KeyCode::Enter,
|
||||
0x25 => KeyCode::KeyL,
|
||||
0x26 => KeyCode::KeyJ,
|
||||
0x27 => KeyCode::Quote,
|
||||
0x28 => KeyCode::KeyK,
|
||||
0x29 => KeyCode::Semicolon,
|
||||
0x2a => KeyCode::Backslash,
|
||||
0x2b => KeyCode::Comma,
|
||||
0x2c => KeyCode::Slash,
|
||||
0x2d => KeyCode::KeyN,
|
||||
0x2e => KeyCode::KeyM,
|
||||
0x2f => KeyCode::Period,
|
||||
0x30 => KeyCode::Tab,
|
||||
0x31 => KeyCode::Space,
|
||||
0x32 => KeyCode::Backquote,
|
||||
0x33 => KeyCode::Backspace,
|
||||
// 0x34 => unknown, // kVK_Powerbook_KeypadEnter
|
||||
0x35 => KeyCode::Escape,
|
||||
0x36 => KeyCode::SuperRight,
|
||||
0x37 => KeyCode::SuperLeft,
|
||||
0x38 => KeyCode::ShiftLeft,
|
||||
0x39 => KeyCode::CapsLock,
|
||||
0x3a => KeyCode::AltLeft,
|
||||
0x3b => KeyCode::ControlLeft,
|
||||
0x3c => KeyCode::ShiftRight,
|
||||
0x3d => KeyCode::AltRight,
|
||||
0x3e => KeyCode::ControlRight,
|
||||
0x3f => KeyCode::Fn,
|
||||
0x40 => KeyCode::F17,
|
||||
0x41 => KeyCode::NumpadDecimal,
|
||||
// 0x42 -> unknown,
|
||||
0x43 => KeyCode::NumpadMultiply,
|
||||
// 0x44 => unknown,
|
||||
0x45 => KeyCode::NumpadAdd,
|
||||
// 0x46 => unknown,
|
||||
0x47 => KeyCode::NumLock, // kVK_ANSI_KeypadClear
|
||||
0x48 => KeyCode::AudioVolumeUp,
|
||||
0x49 => KeyCode::AudioVolumeDown,
|
||||
0x4a => KeyCode::AudioVolumeMute,
|
||||
0x4b => KeyCode::NumpadDivide,
|
||||
0x4c => KeyCode::NumpadEnter,
|
||||
// 0x4d => unknown,
|
||||
0x4e => KeyCode::NumpadSubtract,
|
||||
0x4f => KeyCode::F18,
|
||||
0x50 => KeyCode::F19,
|
||||
0x51 => KeyCode::NumpadEqual,
|
||||
0x52 => KeyCode::Numpad0,
|
||||
0x53 => KeyCode::Numpad1,
|
||||
0x54 => KeyCode::Numpad2,
|
||||
0x55 => KeyCode::Numpad3,
|
||||
0x56 => KeyCode::Numpad4,
|
||||
0x57 => KeyCode::Numpad5,
|
||||
0x58 => KeyCode::Numpad6,
|
||||
0x59 => KeyCode::Numpad7,
|
||||
0x5a => KeyCode::F20,
|
||||
0x5b => KeyCode::Numpad8,
|
||||
0x5c => KeyCode::Numpad9,
|
||||
0x5d => KeyCode::IntlYen,
|
||||
0x5e => KeyCode::IntlRo,
|
||||
0x5f => KeyCode::NumpadComma,
|
||||
0x60 => KeyCode::F5,
|
||||
0x61 => KeyCode::F6,
|
||||
0x62 => KeyCode::F7,
|
||||
0x63 => KeyCode::F3,
|
||||
0x64 => KeyCode::F8,
|
||||
0x65 => KeyCode::F9,
|
||||
0x66 => KeyCode::Lang2,
|
||||
0x67 => KeyCode::F11,
|
||||
0x68 => KeyCode::Lang1,
|
||||
0x69 => KeyCode::F13,
|
||||
0x6a => KeyCode::F16,
|
||||
0x6b => KeyCode::F14,
|
||||
// 0x6c => unknown,
|
||||
0x6d => KeyCode::F10,
|
||||
0x6e => KeyCode::ContextMenu,
|
||||
0x6f => KeyCode::F12,
|
||||
// 0x70 => unknown,
|
||||
0x71 => KeyCode::F15,
|
||||
0x72 => KeyCode::Insert,
|
||||
0x73 => KeyCode::Home,
|
||||
0x74 => KeyCode::PageUp,
|
||||
0x75 => KeyCode::Delete,
|
||||
0x76 => KeyCode::F4,
|
||||
0x77 => KeyCode::End,
|
||||
0x78 => KeyCode::F2,
|
||||
0x79 => KeyCode::PageDown,
|
||||
0x7a => KeyCode::F1,
|
||||
0x7b => KeyCode::ArrowLeft,
|
||||
0x7c => KeyCode::ArrowRight,
|
||||
0x7d => KeyCode::ArrowDown,
|
||||
0x7e => KeyCode::ArrowUp,
|
||||
0x7f => KeyCode::Power, // On 10.7 and 10.8 only
|
||||
_ => return PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode as u16)),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
use cocoa::appkit::{NSApp, NSEventModifierFlags, NSMenu, NSMenuItem};
|
||||
use cocoa::base::selector;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
NSDownArrowFunctionKey, NSEndFunctionKey, NSF10FunctionKey, NSF11FunctionKey,
|
||||
NSF12FunctionKey, NSF13FunctionKey, NSF14FunctionKey, NSF15FunctionKey, NSF16FunctionKey,
|
||||
NSF17FunctionKey, NSF18FunctionKey, NSF19FunctionKey, NSF1FunctionKey, NSF20FunctionKey,
|
||||
NSF2FunctionKey, NSF3FunctionKey, NSF4FunctionKey, NSF5FunctionKey, NSF6FunctionKey,
|
||||
NSF7FunctionKey, NSF8FunctionKey, NSF9FunctionKey, NSHomeFunctionKey, NSInsertFunctionKey,
|
||||
NSLeftArrowFunctionKey, NSPageDownFunctionKey, NSPageUpFunctionKey,
|
||||
NSRightArrowFunctionKey, NSUpArrowFunctionKey,
|
||||
},
|
||||
base::{id, nil},
|
||||
foundation::{NSArray, NSAutoreleasePool, NSInteger},
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use objc::runtime::{NO, YES};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use std::{boxed::Box, cell::RefCell, collections::HashMap, ffi::c_void, rc::Rc};
|
||||
use galaxyui_core::actions::StandardAction;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::menu::{
|
||||
ItemTriggeredCallback, Menu, MenuBar, MenuItem, MenuItemProperties, MenuItemPropertyChanges,
|
||||
UpdateMenuItemCallback,
|
||||
};
|
||||
|
||||
use super::app::callback_dispatcher;
|
||||
use super::make_nsstring;
|
||||
|
||||
lazy_static! {
|
||||
/// A mac-menu-specific map of key names to special characters used for the keyboard shortcuts
|
||||
/// in the mac menus
|
||||
static ref MENU_KEY_EQUIVALENTS: HashMap<&'static str, char> = {
|
||||
fn to_char(key: u16) -> char {
|
||||
char::from_u32(key.into()).unwrap()
|
||||
}
|
||||
|
||||
HashMap::from([
|
||||
("up", to_char(NSUpArrowFunctionKey)),
|
||||
("down", to_char(NSDownArrowFunctionKey)),
|
||||
("left", to_char(NSLeftArrowFunctionKey)),
|
||||
("right", to_char(NSRightArrowFunctionKey)),
|
||||
("home", to_char(NSHomeFunctionKey)),
|
||||
("end", to_char(NSEndFunctionKey)),
|
||||
("pageup", to_char(NSPageUpFunctionKey)),
|
||||
("pagedown", to_char(NSPageDownFunctionKey)),
|
||||
("enter", '\n'),
|
||||
("tab", '\t'),
|
||||
("insert", to_char(NSInsertFunctionKey)),
|
||||
("f1", to_char(NSF1FunctionKey)),
|
||||
("f2", to_char(NSF2FunctionKey)),
|
||||
("f3", to_char(NSF3FunctionKey)),
|
||||
("f4", to_char(NSF4FunctionKey)),
|
||||
("f5", to_char(NSF5FunctionKey)),
|
||||
("f6", to_char(NSF6FunctionKey)),
|
||||
("f7", to_char(NSF7FunctionKey)),
|
||||
("f8", to_char(NSF8FunctionKey)),
|
||||
("f9", to_char(NSF9FunctionKey)),
|
||||
("f10", to_char(NSF10FunctionKey)),
|
||||
("f11", to_char(NSF11FunctionKey)),
|
||||
("f12", to_char(NSF12FunctionKey)),
|
||||
("f13", to_char(NSF13FunctionKey)),
|
||||
("f14", to_char(NSF14FunctionKey)),
|
||||
("f15", to_char(NSF15FunctionKey)),
|
||||
("f16", to_char(NSF16FunctionKey)),
|
||||
("f17", to_char(NSF17FunctionKey)),
|
||||
("f18", to_char(NSF18FunctionKey)),
|
||||
("f19", to_char(NSF19FunctionKey)),
|
||||
("f20", to_char(NSF20FunctionKey)),
|
||||
// The following values are the inverse of `ui/src/platform/mac/event.rs` mappings
|
||||
("numpadenter", to_char(0x03)),
|
||||
("escape", to_char(0x1b)),
|
||||
// Note: Backspace and Delete have different characters for the menu key equivalents
|
||||
// than they send when they are pressed. See the discussion in the Apple docs:
|
||||
// https://developer.apple.com/documentation/appkit/nsmenuitem/1514842-keyequivalent?language=objc
|
||||
("backspace", to_char(0x08)),
|
||||
("delete", to_char(0x7F)),
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
/// Data associated with a custom NSMenuItem.
|
||||
struct MenuItemData {
|
||||
/// Properties of the menu item.
|
||||
/// These could be computed from the menu item but we trust AppKit does not change them.
|
||||
props: RefCell<MenuItemProperties>,
|
||||
|
||||
/// Callback when the menu item is triggered by the user.
|
||||
triggered: ItemTriggeredCallback,
|
||||
|
||||
/// Callback when the menu item needs updating.
|
||||
update: UpdateMenuItemCallback,
|
||||
}
|
||||
|
||||
impl MenuItemData {
|
||||
/// Convert self to a Cocoa context pointer, including the refcount.
|
||||
/// This should be balanced by consume_cocoa_context.
|
||||
fn into_context(self: Rc<MenuItemData>) -> *mut c_void {
|
||||
Box::into_raw(Box::new(self)) as *mut c_void
|
||||
}
|
||||
|
||||
/// Read out from the Cocoa context pointer, without consuming its refcount.
|
||||
fn read_context(ctx: *const c_void) -> Rc<MenuItemData> {
|
||||
unsafe {
|
||||
let ptr = &*(ctx as *const Rc<MenuItemData>);
|
||||
ptr.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Balances a call from to_cocoa_context.
|
||||
fn consume_context(ctx: *mut c_void) {
|
||||
unsafe { std::mem::drop(Box::from_raw(ctx as *mut Rc<MenuItemData>)) }
|
||||
}
|
||||
}
|
||||
|
||||
/// We hand Cocoa a void* which is really an unwrapped Box<Rc<MenuItemData>>.
|
||||
/// The NSMenuItem logically holds a reference count on this Rc, which is balanced in our dealloc callback below.
|
||||
/// The following functions are invoked from Cocoa.
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_menu_item_needs_update(item: id, ctx: *mut c_void) {
|
||||
let ctx = MenuItemData::read_context(ctx);
|
||||
let props: MenuItemProperties = ctx.props.borrow().clone();
|
||||
let func = &ctx.update;
|
||||
|
||||
let mut updated_properties = callback_dispatcher().update_menu_item(|ctx| func(&props, ctx));
|
||||
|
||||
// Always re-apply the disabled state even when the updater has no opinion.
|
||||
// AppKit's modal sessions (e.g. [NSAlert runModal]) can externally disable
|
||||
// menu items, and items whose updaters return `disabled: None` would never
|
||||
// call setEnabled: to restore the correct state. On macOS with the quake
|
||||
// mode (non-activating panel) window, this results in permanently disabled
|
||||
// items after a modal is dismissed. Default to enabled — updaters that want
|
||||
// an item disabled must say so explicitly.
|
||||
if updated_properties.disabled.is_none() {
|
||||
updated_properties.disabled = Some(false);
|
||||
}
|
||||
|
||||
// Update any changed properties.
|
||||
ctx.props.borrow_mut().apply(&updated_properties);
|
||||
unsafe { apply_changes(updated_properties, item) };
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_menu_item_triggered(_item: id, ctx: *mut c_void) {
|
||||
let func = &MenuItemData::read_context(ctx).triggered;
|
||||
callback_dispatcher().menu_item_triggered(func);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C-unwind" fn warp_menu_item_deallocated(ctx: *mut c_void) {
|
||||
MenuItemData::consume_context(ctx)
|
||||
}
|
||||
|
||||
// Declarations of functions implemented in ObjC files.
|
||||
// These signatures must be manually synced - there's no type checking here.
|
||||
extern "C" {
|
||||
fn make_delegated_menu(title: id) -> id;
|
||||
fn make_warp_custom_menu_item(ctx: *mut c_void) -> id;
|
||||
fn set_menu_item_submenu(item: id, submenu: id);
|
||||
fn make_services_menu_item() -> id;
|
||||
}
|
||||
|
||||
struct StandardMenuItemProperties {
|
||||
title: &'static str, // menu item title
|
||||
action: &'static str, // the selector name
|
||||
shortcut: &'static str, // the key equivalent string, or empty for none
|
||||
modifiers: NSEventModifierFlags,
|
||||
}
|
||||
|
||||
// Get properties from a standard action.
|
||||
fn resolve_standard_action(action: StandardAction) -> StandardMenuItemProperties {
|
||||
let cmd = NSEventModifierFlags::NSCommandKeyMask;
|
||||
let option = NSEventModifierFlags::NSAlternateKeyMask;
|
||||
let ctrl = NSEventModifierFlags::NSControlKeyMask;
|
||||
let none = NSEventModifierFlags::empty();
|
||||
|
||||
fn make(
|
||||
title: &'static str,
|
||||
action: &'static str,
|
||||
modifiers: NSEventModifierFlags,
|
||||
shortcut: &'static str,
|
||||
) -> StandardMenuItemProperties {
|
||||
StandardMenuItemProperties {
|
||||
title,
|
||||
action,
|
||||
shortcut,
|
||||
modifiers,
|
||||
}
|
||||
}
|
||||
|
||||
match action {
|
||||
StandardAction::Close => make("Close Window", "performClose:", none, ""),
|
||||
StandardAction::Quit => make("Quit Warp", "terminate:", cmd, "q"),
|
||||
StandardAction::Hide => make("Hide Warp", "hide:", cmd, "h"),
|
||||
StandardAction::HideOtherApps => {
|
||||
make("Hide Others", "hideOtherApplications:", cmd | option, "h")
|
||||
}
|
||||
StandardAction::ShowAllApps => make("Show All", "unhideAllApplications:", none, ""),
|
||||
StandardAction::Minimize => make("Minimize", "performMiniaturize:", cmd, "m"),
|
||||
StandardAction::Zoom => make("Zoom", "performZoom:", none, ""),
|
||||
StandardAction::BringAllToFront => make("Bring All to Front", "arrangeInFront:", none, ""),
|
||||
StandardAction::ToggleFullScreen => {
|
||||
make("ToggleFullScreen", "toggleFullScreen:", cmd | ctrl, "f")
|
||||
}
|
||||
StandardAction::Paste => make("Paste", "paste:", none, ""),
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine the key equivalent for the given keystroke
|
||||
fn resolve_key_equivalent(keystroke: Option<&Keystroke>) -> (id, NSEventModifierFlags) {
|
||||
let mut flags = NSEventModifierFlags::empty();
|
||||
|
||||
let keystroke = match keystroke {
|
||||
Some(value) => value,
|
||||
None => return (make_nsstring(""), flags),
|
||||
};
|
||||
|
||||
let key_equivalent = match MENU_KEY_EQUIVALENTS.get(keystroke.key.as_str()) {
|
||||
Some(c) => make_nsstring(String::from(*c)),
|
||||
None => make_nsstring(&keystroke.key),
|
||||
};
|
||||
|
||||
for (is_set, flag) in [
|
||||
(keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
|
||||
(keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
|
||||
(keystroke.shift, NSEventModifierFlags::NSShiftKeyMask),
|
||||
(keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
|
||||
] {
|
||||
if is_set {
|
||||
flags |= flag
|
||||
}
|
||||
}
|
||||
|
||||
(key_equivalent, flags)
|
||||
}
|
||||
|
||||
// Apply any differences between the two states to the menu item.
|
||||
unsafe fn apply_changes(changes: MenuItemPropertyChanges, item: id) {
|
||||
// Wrap in a local autorelease pool: AppKit invokes `warp_menu_item_needs_update`
|
||||
// on every menu validation (per menu open and per keystroke for shortcut matching),
|
||||
// so this is a hot path. A local pool bounds peak memory for the NSString temporaries
|
||||
// created here (item title, key equivalent) without relying on the outer AppKit pool.
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
if let Some(name) = changes.name {
|
||||
let _: () = msg_send![item, setTitle: make_nsstring(name)];
|
||||
}
|
||||
if let Some(keystroke) = changes.keystroke {
|
||||
let (key_equivalent, modifiers) = resolve_key_equivalent(keystroke.as_ref());
|
||||
let _: () = msg_send![item, setKeyEquivalent: key_equivalent];
|
||||
let _: () = msg_send![item, setKeyEquivalentModifierMask: modifiers];
|
||||
}
|
||||
if let Some(disabled) = changes.disabled {
|
||||
let enabled = if disabled { NO } else { YES };
|
||||
let _: () = msg_send![item, setEnabled: enabled];
|
||||
}
|
||||
if let Some(checked) = changes.checked {
|
||||
// NSControlStateValue has Off as 0, On as 1, Mixed as -1.
|
||||
let control_state: NSInteger = i64::from(checked);
|
||||
let _: () = msg_send![item, setState: control_state];
|
||||
}
|
||||
if let Some(submenu) = changes.submenu {
|
||||
let nsmenu = submenu
|
||||
.map(|menu_items| make_submenu(menu_items))
|
||||
.unwrap_or(nil);
|
||||
set_menu_item_submenu(item, nsmenu);
|
||||
}
|
||||
pool.drain();
|
||||
}
|
||||
|
||||
unsafe fn make_submenu(menu_items: Vec<MenuItem>) -> id {
|
||||
let nsmenu = make_delegated_menu(make_nsstring(""));
|
||||
for menu_item in menu_items {
|
||||
nsmenu.addItem_(make_menu_item(menu_item));
|
||||
}
|
||||
nsmenu
|
||||
}
|
||||
|
||||
unsafe fn make_menu_item(menu_item: MenuItem) -> id {
|
||||
match menu_item {
|
||||
MenuItem::Custom(custom_menu_item) => {
|
||||
let props = custom_menu_item.properties;
|
||||
let data = Rc::new(MenuItemData {
|
||||
props: RefCell::new(props.clone()),
|
||||
triggered: custom_menu_item.callback,
|
||||
update: custom_menu_item.updater,
|
||||
});
|
||||
|
||||
let nsmenu_item = make_warp_custom_menu_item(MenuItemData::into_context(data));
|
||||
|
||||
// Set initial properties for the item.
|
||||
apply_changes(
|
||||
MenuItemPropertyChanges::for_new_item(props, custom_menu_item.submenu),
|
||||
nsmenu_item,
|
||||
);
|
||||
|
||||
nsmenu_item
|
||||
}
|
||||
MenuItem::Standard(standard_action) => {
|
||||
let properties = resolve_standard_action(standard_action);
|
||||
let nsmenu_item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
make_nsstring(properties.title),
|
||||
selector(properties.action),
|
||||
make_nsstring(properties.shortcut),
|
||||
)
|
||||
.autorelease();
|
||||
nsmenu_item.setKeyEquivalentModifierMask_(properties.modifiers);
|
||||
let _: id = msg_send![nsmenu_item, setTag: standard_action as libc::c_long];
|
||||
nsmenu_item
|
||||
}
|
||||
MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
||||
MenuItem::Services => make_services_menu_item(),
|
||||
}
|
||||
}
|
||||
|
||||
/// \return an autoreleased NSMenuItem with a submenu represented by \p menu.
|
||||
// This supports creating the top-level menu bar.
|
||||
unsafe fn make_top_level_menu_item(menu: Menu) -> id {
|
||||
let nsmenu = make_delegated_menu(make_nsstring(&menu.title));
|
||||
|
||||
if menu.is_window_menu() {
|
||||
// `setWindowsMenu` gives us all the default window menu items like
|
||||
// 'Enter Full Screen' and 'Tile Window to Left of Screen'.
|
||||
let () = msg_send![NSApp(), setWindowsMenu: nsmenu];
|
||||
}
|
||||
|
||||
for menu_item in menu.menu_items {
|
||||
nsmenu.addItem_(make_menu_item(menu_item));
|
||||
}
|
||||
|
||||
let menuitem = NSMenuItem::alloc(nil).init().autorelease();
|
||||
menuitem.setSubmenu_(nsmenu);
|
||||
menuitem
|
||||
}
|
||||
|
||||
/// \return an autoreleased NSMenu representing the given menu bar.
|
||||
pub unsafe fn make_main_menu(menubar: MenuBar) -> id {
|
||||
let main_menu = NSMenu::alloc(nil).init().autorelease();
|
||||
for menu in menubar.menus {
|
||||
main_menu.addItem_(make_top_level_menu_item(menu));
|
||||
}
|
||||
main_menu
|
||||
}
|
||||
|
||||
/// \return an autoreleased NSMenu representing the given dock menu.
|
||||
pub unsafe fn make_dock_menu(menu: Menu) -> id {
|
||||
let dock_menu = NSMenu::alloc(nil).init().autorelease();
|
||||
for item in menu.menu_items {
|
||||
dock_menu.addItem_(make_menu_item(item));
|
||||
}
|
||||
dock_menu
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "menus_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Memory-behavior repros for APP-4154 batch 1.C (galaxyui-platform-nsstring).
|
||||
//!
|
||||
//! Covers two kinds of fix in `menus.rs`:
|
||||
//!
|
||||
//! 1. Retain → autorelease conversion (line 298, `make_menu_item` standard
|
||||
//! action): the key-equivalent NSString used to be `NSString::alloc(nil)
|
||||
//! .init_str(...)`, which returns a +1 retained reference. The PR uses
|
||||
//! `make_nsstring`, which autoreleases. Covered by
|
||||
//! [`make_menu_item_standard_action_memory_behavior`].
|
||||
//!
|
||||
//! 2. Local `NSAutoreleasePool` wrapper around `apply_changes` body. The
|
||||
//! NSString temporaries produced by `make_nsstring(name)` and inside
|
||||
//! `resolve_key_equivalent` used to go into whatever ambient pool AppKit
|
||||
//! had set up (or leak if called from a Rust thread with no active pool).
|
||||
//! The PR drains them per call. Covered by
|
||||
//! [`apply_changes_local_pool_memory_behavior`], which deliberately runs
|
||||
//! WITHOUT any outer `NSAutoreleasePool` so the local-pool drain is the
|
||||
//! only thing that can release the temporaries.
|
||||
use cocoa::appkit::NSMenuItem;
|
||||
use cocoa::base::nil;
|
||||
use cocoa::foundation::NSAutoreleasePool;
|
||||
use objc::runtime::Object;
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use galaxyui_core::actions::StandardAction;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::menu::{MenuItem, MenuItemPropertyChanges};
|
||||
|
||||
use super::{apply_changes, make_menu_item};
|
||||
|
||||
/// How many outer pool cycles for the retain → autorelease test.
|
||||
const MENU_ITEM_OUTER: usize = 40;
|
||||
/// Inner iterations per outer cycle. Each one allocates one NSMenuItem plus
|
||||
/// (on master) one retained NSString for the key equivalent.
|
||||
const MENU_ITEM_INNER: usize = 10_000;
|
||||
|
||||
/// Driver for the local-pool wrapper test. `apply_changes` creates a handful
|
||||
/// of NSString temporaries per call; without an outer pool, master accumulates
|
||||
/// them all, while the branch drains them per iteration.
|
||||
const APPLY_CHANGES_ITERS: usize = 200_000;
|
||||
|
||||
/// Reproduces the per-call NSString leak fixed by switching the key-equivalent
|
||||
/// argument to `make_nsstring` on line 298. Each outer cycle gets its own
|
||||
/// autorelease pool; the branch reclaims everything on drain, master keeps the
|
||||
/// retained key-equivalent strings alive.
|
||||
#[test]
|
||||
fn make_menu_item_standard_action_memory_behavior() {
|
||||
unsafe {
|
||||
for _ in 0..MENU_ITEM_OUTER {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
for _ in 0..MENU_ITEM_INNER {
|
||||
// `Quit` has a non-empty key equivalent ("q"); `Close Window`
|
||||
// has an empty one. Mix the two so we cover both branches.
|
||||
let _ = make_menu_item(MenuItem::Standard(StandardAction::Quit));
|
||||
let _ = make_menu_item(MenuItem::Standard(StandardAction::Close));
|
||||
}
|
||||
pool.drain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reproduces the accumulation that the `apply_changes` local pool prevents.
|
||||
/// Note the deliberate absence of an outer `NSAutoreleasePool` — this is what
|
||||
/// makes the local-pool wrapper observable.
|
||||
#[test]
|
||||
fn apply_changes_local_pool_memory_behavior() {
|
||||
unsafe {
|
||||
// Hold a single menu item for the entire loop so that the only growth
|
||||
// we measure is the NSString temporaries inside `apply_changes`, not
|
||||
// the menu item objects themselves.
|
||||
let outer_pool = NSAutoreleasePool::new(nil);
|
||||
let item: *mut Object = msg_send![NSMenuItem::alloc(nil), init];
|
||||
// Retain so we can freely drain the outer pool after constructing it.
|
||||
let _: *mut Object = msg_send![item, retain];
|
||||
outer_pool.drain();
|
||||
|
||||
for _ in 0..APPLY_CHANGES_ITERS {
|
||||
let changes = MenuItemPropertyChanges {
|
||||
name: Some("Warp Menu Item".to_string()),
|
||||
keystroke: Some(Some(Keystroke {
|
||||
cmd: true,
|
||||
key: "k".to_string(),
|
||||
..Default::default()
|
||||
})),
|
||||
disabled: Some(false),
|
||||
checked: Some(false),
|
||||
submenu: None,
|
||||
};
|
||||
apply_changes(changes, item);
|
||||
}
|
||||
|
||||
// Balance the manual retain above.
|
||||
let _: () = msg_send![item, release];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
mod app;
|
||||
pub mod clipboard;
|
||||
pub mod delegate;
|
||||
mod event;
|
||||
pub(crate) mod fonts;
|
||||
mod geometry;
|
||||
mod keycode;
|
||||
mod menus;
|
||||
mod notification;
|
||||
pub(super) mod rendering;
|
||||
mod text_layout;
|
||||
pub mod utils;
|
||||
mod window;
|
||||
|
||||
pub use app::{App, AppExt};
|
||||
pub use delegate::{AppDelegate, IntegrationTestDelegate};
|
||||
pub use fonts::FontDB;
|
||||
pub use rendering::is_low_power_gpu_available;
|
||||
pub use window::Window;
|
||||
pub use window::WindowExt;
|
||||
|
||||
use clipboard::*;
|
||||
|
||||
use geometry::*;
|
||||
|
||||
use cocoa::{
|
||||
base::{id, nil},
|
||||
foundation::{NSAutoreleasePool, NSString},
|
||||
};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
|
||||
/// Create an autoreleased NSString from a string reference.
|
||||
pub fn make_nsstring<S>(s: S) -> id
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
unsafe { NSString::alloc(nil).init_str(s.as_ref()).autorelease() }
|
||||
}
|
||||
|
||||
/// Holds a Cocoa autorelease pool and drains it when the guard is dropped.
|
||||
///
|
||||
/// Many Cocoa APIs temporarily hold on to objects that only get freed when an
|
||||
/// enclosing autorelease pool is drained. AppKit's main event loop and GCD
|
||||
/// blocks create one of these pools around each callback, so most code doesn't
|
||||
/// have to think about it. But code that runs during app startup, on a thread
|
||||
/// Rust created itself, or in a tight loop inside a single event can't rely on
|
||||
/// the outer pool: objects accumulate in memory until that outer pool drains,
|
||||
/// which can be a long time.
|
||||
///
|
||||
/// Create a `AutoreleasePoolGuard` in that scope to open your own pool. The
|
||||
/// guard drains the pool automatically when it goes out of scope, whether the
|
||||
/// function returns normally, returns early via `?`, or unwinds due to a
|
||||
/// panic.
|
||||
pub struct AutoreleasePoolGuard(id);
|
||||
|
||||
impl AutoreleasePoolGuard {
|
||||
/// Creates a fresh `NSAutoreleasePool` whose lifetime is tied to the guard.
|
||||
pub fn new() -> Self {
|
||||
// SAFETY: `NSAutoreleasePool::new` is infallible and produces a pool
|
||||
// that is valid for the current thread until the guard drains it on
|
||||
// `Drop`.
|
||||
Self(unsafe { NSAutoreleasePool::new(nil) })
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AutoreleasePoolGuard {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AutoreleasePoolGuard {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: `self.0` was produced by `NSAutoreleasePool::new` in
|
||||
// `Self::new` and is drained at most once here.
|
||||
unsafe {
|
||||
let _: () = msg_send![self.0, drain];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::DateTime;
|
||||
use cocoa::base::id;
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use galaxyui_core::notification::{
|
||||
NotificationResponse, NotificationSendError, RequestPermissionsOutcome,
|
||||
};
|
||||
|
||||
use super::utils::nsstring_as_str;
|
||||
|
||||
/// Build a Notification Response from a native notification event
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The `data` parameter must be a valid pointer to Objective-C string data
|
||||
pub unsafe fn response_from_native(
|
||||
seconds_from_epoch: i32,
|
||||
data: id,
|
||||
) -> Result<NotificationResponse> {
|
||||
let data = nsstring_as_str(data)?;
|
||||
|
||||
// Only set the data if it's not an empty string.
|
||||
let data = (!data.is_empty()).then_some(data);
|
||||
|
||||
let timestamp = DateTime::from_timestamp(seconds_from_epoch as i64, 0)
|
||||
.ok_or_else(|| anyhow!("failed to convert time"))?;
|
||||
Ok(NotificationResponse::new(
|
||||
timestamp.naive_utc(),
|
||||
data.map(Into::into),
|
||||
))
|
||||
}
|
||||
|
||||
/// Build a Notification send error from a native notification event
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The `error_message` parameter must be a valid pointer to Objective-C string data
|
||||
pub unsafe fn send_error_from_native(
|
||||
error_type: NSUInteger,
|
||||
error_message: id,
|
||||
) -> Result<NotificationSendError> {
|
||||
let error_message = nsstring_as_str(error_message)?.to_owned();
|
||||
|
||||
Ok(match error_type {
|
||||
0 => NotificationSendError::PermissionsDenied,
|
||||
1 => NotificationSendError::Other { error_message },
|
||||
_ => NotificationSendError::Other { error_message },
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a Notification request permissions outcome from a native notification event
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The `outcome_message` parameter must be a valid pointer to Objective-C string data
|
||||
pub unsafe fn request_permissions_outcome_from_native(
|
||||
outcome_type: NSUInteger,
|
||||
outcome_message: id,
|
||||
) -> Result<RequestPermissionsOutcome> {
|
||||
let outcome_message = nsstring_as_str(outcome_message)?.to_owned();
|
||||
|
||||
Ok(match outcome_type {
|
||||
0 => RequestPermissionsOutcome::Accepted,
|
||||
1 => RequestPermissionsOutcome::PermissionsDenied,
|
||||
2 => RequestPermissionsOutcome::OtherError {
|
||||
error_message: outcome_message,
|
||||
},
|
||||
_ => RequestPermissionsOutcome::OtherError {
|
||||
error_message: outcome_message,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
BasedOnStyle: Google
|
||||
IndentWidth: 4
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
NSModalResponse configureAndRunModal(NSAlert* alert, NSApplication* app);
|
||||
@@ -0,0 +1,16 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
#import "app.h"
|
||||
|
||||
NSModalResponse configureAndRunModal(NSAlert *alert, NSApplication *app) {
|
||||
alert.showsSuppressionButton = YES;
|
||||
|
||||
// It is generally frowned-upon to be overly assertive about putting our windows
|
||||
// in the user's face. However, it is reasonable to do this before showing our modal.
|
||||
// If we don't make ourselves the top active app, our modal might show up BEHIND an-
|
||||
// other app's window.
|
||||
[app activateIgnoringOtherApps:YES];
|
||||
NSModalResponse response = [alert runModal];
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <Carbon/Carbon.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
|
||||
// Our NSApplication subclass.
|
||||
@interface WarpApplication : NSApplication
|
||||
@end
|
||||
|
||||
// WarpDelegate is the delegate of the NSApp and also all menus.
|
||||
@interface WarpDelegate
|
||||
: NSObject <NSApplicationDelegate, NSMenuDelegate, UNUserNotificationCenterDelegate>
|
||||
|
||||
@property(strong) NSMenu *dockMenu;
|
||||
|
||||
@end
|
||||
|
||||
// Functions implemented in Rust.
|
||||
void warp_app_will_finish_launching(id app);
|
||||
void warp_app_did_become_active(id app);
|
||||
void warp_app_did_resign_active(id app);
|
||||
void warp_app_will_terminate(id app);
|
||||
void warp_app_open_files(id app, id filenames);
|
||||
void warp_app_send_global_keybinding(id app, NSUInteger modifiers, NSUInteger key_code);
|
||||
void warp_app_new_window(id app);
|
||||
void warp_app_window_did_resize(id app);
|
||||
void warp_app_window_did_move(id app);
|
||||
void warp_app_window_will_close(id app, id window);
|
||||
void warp_app_screen_did_change(id app);
|
||||
void cpu_awakened(id app);
|
||||
void cpu_will_sleep(id app);
|
||||
void warp_app_active_window_changed(id app);
|
||||
void warp_app_notification_clicked(id app, double date, id data);
|
||||
void warp_app_open_urls(id app, id urls);
|
||||
void warp_app_os_appearance_changed(id app);
|
||||
BOOL warp_app_should_terminate_app(id app);
|
||||
BOOL warp_app_should_close_window(id app, id window);
|
||||
BOOL warp_app_are_key_bindings_disabled_for_window(id app, id window);
|
||||
BOOL warp_app_has_binding_for_keystroke(id app, id event);
|
||||
BOOL warp_app_has_custom_action_for_keystroke(id app, id event);
|
||||
void warp_app_disable_warning_modal(id app);
|
||||
void warp_app_internet_reachability_changed(id app, BOOL can_reach);
|
||||
void warp_app_process_modal_response(id app, NSUInteger modal_id, NSModalResponse response,
|
||||
BOOL disable_modal);
|
||||
@@ -0,0 +1,552 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <Carbon/Carbon.h>
|
||||
#import <ServiceManagement/ServiceManagement.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
|
||||
#import "alert.h"
|
||||
#import "app.h"
|
||||
#import "host_view.h"
|
||||
#import "hotkey.h"
|
||||
#import "menus.h"
|
||||
|
||||
#import "reachability.h"
|
||||
|
||||
static void *NSAppThemeChangeContext = &NSAppThemeChangeContext;
|
||||
|
||||
NSMutableDictionary<NSNumber *, WarpHotKey *> *_hotKeys;
|
||||
UInt32 _nextHotKeyID;
|
||||
|
||||
OSStatus HotkeyPressedHandler(EventHandlerCallRef _inCaller __unused, EventRef inEvent,
|
||||
void *inUserData);
|
||||
OSStatus HotkeyPressedHandler(EventHandlerCallRef _inCaller __unused, EventRef inEvent,
|
||||
void *inUserData) {
|
||||
EventHotKeyID hotKeyID;
|
||||
|
||||
// Get the hotKeyID corresponding to the pressed hot key.
|
||||
if (GetEventParameter(inEvent, kEventParamDirectObject, typeEventHotKeyID, nil,
|
||||
sizeof(EventHotKeyID), nil, &hotKeyID)) {
|
||||
return eventNotHandledErr;
|
||||
}
|
||||
|
||||
WarpHotKey *hotkey = _hotKeys[@(hotKeyID.id)];
|
||||
if (hotkey) {
|
||||
warp_app_send_global_keybinding((NSApplication *)inUserData, hotkey->_modifierKeys,
|
||||
hotkey->_keyCode);
|
||||
return noErr;
|
||||
}
|
||||
|
||||
return eventNotHandledErr;
|
||||
}
|
||||
|
||||
BOOL isDarkMode() {
|
||||
NSAppearanceName name = [NSApp.effectiveAppearance
|
||||
bestMatchFromAppearancesWithNames:@[ NSAppearanceNameAqua, NSAppearanceNameDarkAqua ]];
|
||||
return name == NSAppearanceNameDarkAqua;
|
||||
}
|
||||
|
||||
NSArray *getFilePathsFromPasteboard() {
|
||||
NSPasteboard *pb = [NSPasteboard generalPasteboard];
|
||||
NSArray *types = [pb types];
|
||||
|
||||
if ([types containsObject:NSPasteboardTypeFileURL]) {
|
||||
return [pb getFilePaths];
|
||||
}
|
||||
|
||||
return [NSArray array];
|
||||
}
|
||||
|
||||
void *registerGlobalHotkey(NSUInteger key, NSUInteger modifiers) {
|
||||
EventHotKeyRef hotKeyRef = NULL;
|
||||
EventHotKeyID hotKeyID = {0, _nextHotKeyID};
|
||||
if (RegisterEventHotKey((UInt32)key, (UInt32)modifiers, hotKeyID, GetEventDispatcherTarget(), 0,
|
||||
&hotKeyRef)) {
|
||||
return nil;
|
||||
};
|
||||
[_hotKeys setObject:[[[WarpHotKey alloc] initWithEventHotKey:hotKeyRef
|
||||
keyCode:key
|
||||
modifierKeys:modifiers] autorelease]
|
||||
forKey:@(hotKeyID.id)];
|
||||
_nextHotKeyID++;
|
||||
return nil;
|
||||
}
|
||||
|
||||
void *unregisterGlobalHotkey(NSUInteger key, NSUInteger modifiers) {
|
||||
NSNumber *keyIdx;
|
||||
BOOL found = NO;
|
||||
|
||||
for (NSNumber *hotKeyID in _hotKeys) {
|
||||
if ([[_hotKeys objectForKey:hotKeyID] hotKeyKeyAndModifierEquals:key
|
||||
modifierKeys:modifiers]) {
|
||||
keyIdx = hotKeyID;
|
||||
found = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
UnregisterEventHotKey([_hotKeys objectForKey:keyIdx]->_eventHotKey);
|
||||
[_hotKeys removeObjectForKey:keyIdx];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSRect screenFrame() { return [[NSScreen mainScreen] frame]; }
|
||||
|
||||
NSUInteger activeScreenId() {
|
||||
return [[[[NSScreen mainScreen] deviceDescription] objectForKey:@"NSScreenNumber"]
|
||||
unsignedIntegerValue];
|
||||
}
|
||||
|
||||
@interface WarpMenuItemDelegate : NSObject <NSMenuDelegate> {
|
||||
// Rust expects an ivar with this name.
|
||||
void *rustWrapper;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation WarpDelegate {
|
||||
// Rust expects an ivar with this name.
|
||||
void *rustWrapper;
|
||||
|
||||
// Whether we have a pending active window change notification.
|
||||
BOOL hasPendingActiveWindowChange;
|
||||
|
||||
// Internet reachability.
|
||||
Reachability *internetReachable;
|
||||
|
||||
// Track the current reachable state so we don't double fire reachability state
|
||||
// changed events.
|
||||
NSNumber *isReachable;
|
||||
|
||||
// Whether we should force termination.
|
||||
BOOL forceTermination;
|
||||
|
||||
// Whether we should terminate the application upon the app
|
||||
// being hidden. This allows us to hide the app before running any
|
||||
// slower termination logic.
|
||||
BOOL terminateOnHide;
|
||||
}
|
||||
|
||||
- (id)init {
|
||||
[super init];
|
||||
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
|
||||
[defaultCenter addObserver:self
|
||||
selector:@selector(keyWindowChanged:)
|
||||
name:NSWindowDidBecomeKeyNotification
|
||||
object:nil];
|
||||
[defaultCenter addObserver:self
|
||||
selector:@selector(keyWindowChanged:)
|
||||
name:NSWindowDidResignKeyNotification
|
||||
object:nil];
|
||||
|
||||
[defaultCenter addObserver:self
|
||||
selector:@selector(windowMoved:)
|
||||
name:NSWindowDidMoveNotification
|
||||
object:nil];
|
||||
[defaultCenter addObserver:self
|
||||
selector:@selector(windowResized:)
|
||||
name:NSWindowDidResizeNotification
|
||||
object:nil];
|
||||
[defaultCenter addObserver:self
|
||||
selector:@selector(screenChanged:)
|
||||
name:NSApplicationDidChangeScreenParametersNotification
|
||||
object:nil];
|
||||
|
||||
// For the following notifications, we need to register them on the workspace
|
||||
// notification center, which is different from the default NSNotificationCenter.
|
||||
// See here for more details: https://developer.apple.com/library/archive/qa/qa1340/_index.html.
|
||||
NSNotificationCenter *workspaceCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
[workspaceCenter addObserver:self
|
||||
selector:@selector(cpuAwakened:)
|
||||
name:NSWorkspaceDidWakeNotification
|
||||
object:nil];
|
||||
[workspaceCenter addObserver:self
|
||||
selector:@selector(cpuWillSleep:)
|
||||
name:NSWorkspaceWillSleepNotification
|
||||
object:nil];
|
||||
|
||||
// Tell the shared notification center to use the current view as the
|
||||
// `UNUserNotificationCenterDelegate` delegate. We only do this if the application
|
||||
// is bundled, otherwise the app will crash when trying to set the delegate. This allows
|
||||
// warpui to still be run via `cargo run` since the app is not bundled in this case. Note this
|
||||
// has no functional change in the non-bundled case since the app must be bundled for
|
||||
// notifications to actually be sent/received.
|
||||
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
|
||||
if (bundleIdentifier != nil && ![bundleIdentifier isEqualToString:(@"")]) {
|
||||
UNUserNotificationCenter *user_notification_center =
|
||||
[UNUserNotificationCenter currentNotificationCenter];
|
||||
user_notification_center.delegate = self;
|
||||
|
||||
// Create and register the notification category.
|
||||
UNNotificationCategory *CustomizedNotification = [UNNotificationCategory
|
||||
categoryWithIdentifier:@"CUSTOMIZED_NOTIFICATION"
|
||||
actions:@[]
|
||||
intentIdentifiers:@[]
|
||||
options:UNNotificationCategoryOptionCustomDismissAction];
|
||||
|
||||
[user_notification_center
|
||||
setNotificationCategories:[NSSet setWithObjects:CustomizedNotification, nil]];
|
||||
}
|
||||
|
||||
// Initiate the global hotkey handlers first so we could register them at the rust
|
||||
// side callback.
|
||||
EventTypeSpec eventType = {kEventClassKeyboard, kEventHotKeyPressed};
|
||||
InstallApplicationEventHandler(HotkeyPressedHandler, 1, &eventType, self, NULL);
|
||||
_hotKeys = [[NSMutableDictionary alloc] init];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[NSApp removeObserver:self forKeyPath:@"effectiveAppearance" context:NSAppThemeChangeContext];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
[internetReachable stopNotifier];
|
||||
[internetReachable release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (void)applicationWillFinishLaunching:(NSNotification *)note {
|
||||
// On macOS 26, the autofill heuristic controller causes significant slowdowns.
|
||||
// It's not clear why, but other apps which use custom text inputs have reported
|
||||
// the same issue. See:
|
||||
// * Ghostty: https://github.com/ghostty-org/ghostty/pull/8625
|
||||
// * Zed: https://github.com/zed-industries/zed/issues/33182
|
||||
// * Twitter thread discussing the issue: https://x.com/mitchellh/status/1967324131801915875
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
[defaults setBool:NO forKey:@"NSAutoFillHeuristicControllerEnabled"];
|
||||
|
||||
if (rustWrapper) warp_app_will_finish_launching(note.object);
|
||||
}
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)note {
|
||||
[NSApp addObserver:self
|
||||
forKeyPath:@"effectiveAppearance"
|
||||
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
|
||||
context:NSAppThemeChangeContext];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
ofObject:(id)object
|
||||
change:(NSDictionary *)change
|
||||
context:(void *)context {
|
||||
if (context == NSAppThemeChangeContext) {
|
||||
if (rustWrapper) warp_app_os_appearance_changed(self);
|
||||
} else {
|
||||
// Any unrecognized context must belong to super
|
||||
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)note {
|
||||
if (rustWrapper) warp_app_did_become_active(note.object);
|
||||
}
|
||||
|
||||
- (void)setForceTermination {
|
||||
forceTermination = YES;
|
||||
}
|
||||
|
||||
// Unfullscreens any windows that are currently fullscreen.
|
||||
- (void)unfullscreenAllWindows:(NSApplication *)application {
|
||||
for (NSWindow *window in [application windows]) {
|
||||
if ((window.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) {
|
||||
[window toggleFullScreen:nil];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)application {
|
||||
BOOL okToTerminate = YES;
|
||||
|
||||
// If this is the second termination attempt after we've already hidden the app, we can go ahead
|
||||
// and terminate.
|
||||
if (terminateOnHide) {
|
||||
return NSTerminateNow;
|
||||
}
|
||||
|
||||
if (!forceTermination) {
|
||||
// Make sure the rust app doesn't have any reasons to interrupt quit, e.g. needs to relaunch
|
||||
// for autoupdate, but launching the new process failed.
|
||||
okToTerminate = warp_app_should_terminate_app(application);
|
||||
}
|
||||
|
||||
if (okToTerminate) {
|
||||
// We want to hide the application before we start the teardown
|
||||
// process, to ensure the user isn't affected by any slow teardown
|
||||
// steps. The tricky part here is that a call to `[NSApp hide]` isn't
|
||||
// handled synchronously, it is processed on the event loop.
|
||||
//
|
||||
// To work around this, we enqueue the hide on the event loop and set
|
||||
// some state so we know to resume termination of the application when
|
||||
// the hide takes effect. As a fallback, if we never get notified that
|
||||
// the application was hidden, we always resume termination after 5s.
|
||||
// We deliberately do _not_ return `NSTerminateLater` here because it enables a special mode
|
||||
// of the event loop that is meant specifically for handling modals. We also make sure to
|
||||
// first exit any fullscreen windows before we hide--`NSApplication#hide` is a NOOP if there
|
||||
// are any full screen windows.
|
||||
|
||||
[self unfullscreenAllWindows:application];
|
||||
[application hide:nil];
|
||||
terminateOnHide = YES;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC),
|
||||
dispatch_get_main_queue(), ^{
|
||||
[application terminate:nil];
|
||||
});
|
||||
}
|
||||
return NSTerminateCancel;
|
||||
}
|
||||
|
||||
- (void)applicationDidHide:(NSNotification *)note {
|
||||
if (terminateOnHide) {
|
||||
NSApplication *app = note.object;
|
||||
[app terminate:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidResignActive:(NSNotification *)note {
|
||||
if (rustWrapper) warp_app_did_resign_active(note.object);
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)note {
|
||||
if (rustWrapper) warp_app_will_terminate(note.object);
|
||||
}
|
||||
|
||||
- (void)application:(NSApplication *)sender openFiles:(NSArray<NSString *> *)filenames {
|
||||
if (rustWrapper) warp_app_open_files(sender, filenames);
|
||||
}
|
||||
|
||||
- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
|
||||
if (rustWrapper) warp_app_open_urls(application, urls);
|
||||
}
|
||||
|
||||
// This is called when clicking on the app in the Dock or from Finder.
|
||||
// If there's no visible windows, we will open one.
|
||||
- (BOOL)applicationShouldHandleReopen:(NSApplication *)app hasVisibleWindows:(BOOL)flag {
|
||||
if (rustWrapper && !flag) {
|
||||
warp_app_new_window(app);
|
||||
return NO; // do nothing
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)keyWindowChanged:(NSNotification *)note {
|
||||
// We use an async dispatch here for two reasons:
|
||||
// 1. When the active window changes, this will be called twice (once for resign, once for
|
||||
// activated). We can coalesce these calls.
|
||||
// 2. When a new window is created, warp will activate it; if we recursively call back into
|
||||
// warp then we will cause the app to be mutably borrowed while already borrowed.
|
||||
if (!hasPendingActiveWindowChange) {
|
||||
hasPendingActiveWindowChange = YES;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self->hasPendingActiveWindowChange = NO;
|
||||
if (self->rustWrapper) warp_app_active_window_changed(self);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)windowMoved:(NSNotification *)note {
|
||||
// We need to use async dispatch here because the event loop in appkit calls the
|
||||
// app notification first before calling the window notification. Since we are updating
|
||||
// the window properties within the window notification, we need to make sure this
|
||||
// callback gets triggered after the window notification. Thus using the async dispatch
|
||||
// here ensures we always save the most up-to-date value within the database.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self->rustWrapper) warp_app_window_did_move(self);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)windowResized:(NSNotification *)note {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self->rustWrapper) warp_app_window_did_resize(self);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)screenChanged:(NSNotification *)note {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self->rustWrapper) warp_app_screen_did_change(self);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)cpuAwakened:(NSNotification *)note {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self->rustWrapper) cpu_awakened(self);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)cpuWillSleep:(NSNotification *)note {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self->rustWrapper) cpu_will_sleep(self);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)menuNeedsUpdate:(NSMenu *)menu {
|
||||
// Trigger warp_menu_item_needs_update for every item with our class set as its represented
|
||||
// object.
|
||||
Class warpHandlerClass = [WarpCustomMenuItemHandler class];
|
||||
for (NSMenuItem *item in menu.itemArray) {
|
||||
id obj = item.representedObject;
|
||||
if ([obj isKindOfClass:warpHandlerClass]) {
|
||||
[obj itemNeedsUpdate:item];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setReachabilityListener {
|
||||
internetReachable = [[Reachability reachabilityWithHostname:@"0.0.0.0"] retain];
|
||||
|
||||
// Internet is reachable.
|
||||
internetReachable.reachableBlock = ^(Reachability *reach __unused) {
|
||||
// Update the UI on the main thread.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self->isReachable == nil || [self->isReachable intValue] == 0) {
|
||||
self->isReachable = [NSNumber numberWithBool:YES];
|
||||
if (self->rustWrapper) warp_app_internet_reachability_changed(self, YES);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Internet is not reachable.
|
||||
internetReachable.unreachableBlock = ^(Reachability *reach __unused) {
|
||||
// Update the UI on the main thread.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self->isReachable == nil || [self->isReachable intValue] > 0) {
|
||||
self->isReachable = [NSNumber numberWithBool:NO];
|
||||
if (self->rustWrapper) warp_app_internet_reachability_changed(self, NO);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Dispatch an initial call to check internet reachability so app could get notified
|
||||
// of the reachability status it starts in.
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
|
||||
BOOL internetIsReachable = [internetReachable isReachable];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self->isReachable == nil) {
|
||||
self->isReachable = [NSNumber numberWithBool:internetIsReachable];
|
||||
if (self->rustWrapper)
|
||||
warp_app_internet_reachability_changed(self, internetIsReachable);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
[internetReachable startNotifier];
|
||||
}
|
||||
|
||||
// Returns a new NSMenu in the mac dock. Gets called every time we pull up the dock menu
|
||||
- (NSMenu *)applicationDockMenu:(NSApplication *)sender {
|
||||
return self.dockMenu;
|
||||
}
|
||||
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||
didReceiveNotificationResponse:(UNNotificationResponse *)response
|
||||
withCompletionHandler:(void (^)(void))completionHandler {
|
||||
// Handle what happens when the user clicks the notification. Warp doesn't support any actions
|
||||
// other than the default action currently.
|
||||
if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) {
|
||||
NSDictionary *userInfo = response.notification.request.content.userInfo;
|
||||
NSString *data = userInfo[@"DATA"];
|
||||
|
||||
if (rustWrapper) {
|
||||
warp_app_notification_clicked(self, response.notification.date.timeIntervalSince1970,
|
||||
data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation WarpApplication {
|
||||
// Rust expects an ivar with this name.
|
||||
void *rustWrapper;
|
||||
}
|
||||
|
||||
- (void)setForceTermination {
|
||||
WarpDelegate *delegate = (WarpDelegate *)self.delegate;
|
||||
[delegate setForceTermination];
|
||||
}
|
||||
|
||||
- (void)showModal:(NSAlert *)alert modalId:(NSUInteger)modalId {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSModalResponse response = configureAndRunModal(alert, self);
|
||||
|
||||
BOOL disable_modal = alert.suppressionButton.state == NSControlStateValueOn;
|
||||
// Subtracting `NSAlertFirstButtonReturn` from `response` yields the 0-based index of the
|
||||
// button that was actually clicked.
|
||||
warp_app_process_modal_response(self, modalId, response - NSAlertFirstButtonReturn,
|
||||
disable_modal);
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
WarpApplication *get_warp_app() {
|
||||
// Set up the delegate (once).
|
||||
// The delegate is deliberately leaked.
|
||||
WarpApplication *app = [WarpApplication sharedApplication];
|
||||
static dispatch_once_t once;
|
||||
static id sharedDelegate;
|
||||
dispatch_once(&once, ^{
|
||||
sharedDelegate = [[WarpDelegate alloc] init];
|
||||
[app setDelegate:sharedDelegate];
|
||||
|
||||
// Hack to work around the fact that warp is frequently tested as a
|
||||
// standalone (unbundled) binary.
|
||||
app.activationPolicy = NSApplicationActivationPolicyRegular;
|
||||
});
|
||||
return app;
|
||||
}
|
||||
|
||||
// \return an empty NSMenu with the given title, setting up the delegate appropriately.
|
||||
// The result is autoreleased.
|
||||
NSMenu *make_delegated_menu(NSString *title) {
|
||||
NSMenu *result = [[[NSMenu alloc] initWithTitle:title] autorelease];
|
||||
result.delegate = (WarpDelegate *)[[WarpApplication sharedApplication] delegate];
|
||||
return result;
|
||||
}
|
||||
|
||||
// Create Services, a system-defined standard menu on macOS
|
||||
// The result is autoreleased.
|
||||
NSMenuItem *make_services_menu_item() {
|
||||
// Create the services menu. `servicesMenu` retains, so autorelease our +1 ownership.
|
||||
NSApp.servicesMenu = [[[NSMenu alloc] init] autorelease];
|
||||
|
||||
// Create menu item for it
|
||||
NSMenuItem *servicesItem = [[[NSMenuItem alloc] init] autorelease];
|
||||
servicesItem.title = @"Services";
|
||||
servicesItem.submenu = NSApp.servicesMenu;
|
||||
|
||||
return servicesItem;
|
||||
}
|
||||
|
||||
// \return a new menu item that wraps the given context pointer.
|
||||
// The pointer will be provided back to Warp in the callbacks (see menus.h).
|
||||
// The result is autoreleased.
|
||||
NSMenuItem *make_warp_custom_menu_item(void *context) {
|
||||
WarpCustomMenuItemHandler *handler =
|
||||
[[[WarpCustomMenuItemHandler alloc] initWithContext:context] autorelease];
|
||||
|
||||
// Sets action to NULL if menu item has submenu, so the menu doesn't close when item is clicked
|
||||
NSMenuItem *item = [[[NSMenuItem alloc] initWithTitle:@""
|
||||
action:@selector(itemWasTriggered:)
|
||||
keyEquivalent:@""] autorelease];
|
||||
item.representedObject = handler;
|
||||
item.target = handler;
|
||||
return item;
|
||||
}
|
||||
|
||||
NSString *executableInApplicationBundleWithIdentifier(NSString *bundle_path) {
|
||||
NSBundle *bundle = [NSBundle bundleWithPath:bundle_path];
|
||||
NSString *executable = [bundle.bundlePath stringByAppendingPathComponent:@"Contents/MacOS"];
|
||||
executable = [executable
|
||||
stringByAppendingPathComponent:[bundle
|
||||
objectForInfoDictionaryKey:(id)kCFBundleExecutableKey]];
|
||||
return executable;
|
||||
}
|
||||
|
||||
NSString *absolutePathForApplicationBundleWithIdentifier(NSString *bundle_identifier) {
|
||||
NSURL *url =
|
||||
[[NSWorkspace sharedWorkspace] URLForApplicationWithBundleIdentifier:bundle_identifier];
|
||||
return url.path;
|
||||
}
|
||||
|
||||
BOOL isVoiceOverEnabled() { return [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]; }
|
||||
@@ -0,0 +1,8 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
// Enforces that multiple windows don't transition to fullscreen at
|
||||
// the same time.
|
||||
@interface FullscreenWindowManager : NSObject
|
||||
// Queues a window to be transitioned to fullscreen. Not thread-safe.
|
||||
- (void)enqueueWindow:(NSWindow *)window;
|
||||
@end
|
||||
@@ -0,0 +1,60 @@
|
||||
#import "fullscreen_queue.h"
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
@implementation FullscreenWindowManager {
|
||||
// A LIFO queue of windows that want to transition to fullscreen.
|
||||
NSMutableArray<NSWindow *> *fullscreenQueue;
|
||||
|
||||
// Whether or not there is currently a window transitioning to fullscreen. Note
|
||||
// that the absence of a locking mechanism makes the FullscreenWindowManager not
|
||||
// thread-safe.
|
||||
BOOL activeTransition;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
fullscreenQueue = [[NSMutableArray alloc] init];
|
||||
activeTransition = NO;
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(windowWillTransitionToFullscreen:)
|
||||
name:NSWindowWillEnterFullScreenNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(windowDidTransitionToFullscreen:)
|
||||
name:NSWindowDidEnterFullScreenNotification
|
||||
object:nil];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)enqueueWindow:(NSWindow *)window {
|
||||
[fullscreenQueue addObject:window];
|
||||
[self transitionNextWindowInQueue];
|
||||
}
|
||||
|
||||
- (void)transitionNextWindowInQueue {
|
||||
if (activeTransition == YES) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([fullscreenQueue count] > 0) {
|
||||
NSWindow *window = fullscreenQueue.firstObject;
|
||||
[fullscreenQueue removeObjectAtIndex:0];
|
||||
|
||||
[window performSelector:@selector(toggleFullScreen:)];
|
||||
}
|
||||
}
|
||||
|
||||
// Callback for when a window starts a fullscreen transition.
|
||||
- (void)windowWillTransitionToFullscreen:(NSNotification *)notification {
|
||||
activeTransition = YES;
|
||||
}
|
||||
|
||||
// Callback for when a window ends a fullscreen transition.
|
||||
- (void)windowDidTransitionToFullscreen:(NSNotification *)notification {
|
||||
activeTransition = NO;
|
||||
[self transitionNextWindowInQueue];
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
@interface NSPasteboard (Warp)
|
||||
- (NSArray *)getFilePaths;
|
||||
@end
|
||||
|
||||
/// WarpHostView is the Content view of a Warp window.
|
||||
// It is backed by a Metal CALayer.
|
||||
@interface WarpHostView : NSView <CALayerDelegate, NSTextInputClient>
|
||||
- (WarpHostView *)initWithFrame:(NSRect)frame
|
||||
metalDevice:(id)metalDevice
|
||||
enableTitlebarDrag:(BOOL)enableTitlebarDrag
|
||||
testMode:(BOOL)testMode;
|
||||
- (void)setAsyncCallback:(BOOL)shouldAsync;
|
||||
- (BOOL)keyDownImpl:(NSEvent *)event;
|
||||
@end
|
||||
@@ -0,0 +1,496 @@
|
||||
#import "host_view.h"
|
||||
|
||||
#import <Metal/Metal.h>
|
||||
|
||||
void warp_view_did_change_backing_properties(WarpHostView *, BOOL);
|
||||
void warp_view_set_frame_size(WarpHostView *, NSSize, BOOL);
|
||||
void warp_update_layer(WarpHostView *);
|
||||
BOOL warp_handle_view_event(WarpHostView *, NSEvent *, BOOL);
|
||||
BOOL warp_handle_first_mouse_event(WarpHostView *, NSEvent *);
|
||||
void warp_handle_insert_text(WarpHostView *, id);
|
||||
void warp_update_ime_state(WarpHostView *, BOOL);
|
||||
void warp_handle_drag_and_drop(WarpHostView *, NSArray *, NSPoint);
|
||||
void warp_handle_file_drag(WarpHostView *, NSPoint);
|
||||
void warp_handle_file_drag_exit(WarpHostView *);
|
||||
NSRect warp_ime_position(WarpHostView *, NSRect *);
|
||||
id warp_get_accessibility_contents(WarpHostView *);
|
||||
void warp_marked_text_updated(WarpHostView *, NSString *, NSRange);
|
||||
void warp_marked_text_cleared(WarpHostView *);
|
||||
|
||||
@implementation NSPasteboard (Warp)
|
||||
|
||||
- (NSArray *)getFilePaths {
|
||||
NSMutableArray *paths = [NSMutableArray array];
|
||||
NSArray<NSURL *> *urls = [self readObjectsForClasses:@[ [NSURL class] ] options:0];
|
||||
for (NSURL *url in urls) {
|
||||
NSString *path = url.path;
|
||||
if (path) {
|
||||
[paths addObject:path];
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation WarpHostView {
|
||||
// The windowState is managed on the Rust side.
|
||||
// Note Rust expects this name even though we are not a window.
|
||||
void *windowState;
|
||||
|
||||
// Whether we start a window drag on an unhandled mouseDown event inside the title bar
|
||||
BOOL titlebarDragEnabled;
|
||||
|
||||
// Whether we are in test mode, which suppresses drawing.
|
||||
BOOL testMode;
|
||||
|
||||
// The metal device for our layer.
|
||||
id metalDevice;
|
||||
|
||||
NSMutableAttributedString *markedText;
|
||||
NSMutableString *textToInsert;
|
||||
|
||||
// Whether to have resize event callback called asynchronously.
|
||||
BOOL asyncCallback;
|
||||
|
||||
// Whether we're in the middle of a call to interpretKeyEvents.
|
||||
BOOL interpretingKeyEvents;
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstResponder {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)mouseDownCanMoveWindow {
|
||||
return !titlebarDragEnabled;
|
||||
}
|
||||
|
||||
- (BOOL)readyForWarp {
|
||||
return windowState != NULL;
|
||||
}
|
||||
|
||||
/// Returns the height of the titlebar.
|
||||
- (CGFloat)titlebarHeight {
|
||||
NSButton *closeButton = [self.window standardWindowButton:NSWindowCloseButton];
|
||||
NSView *titlebar = [closeButton superview];
|
||||
return titlebar.frame.size.height;
|
||||
}
|
||||
|
||||
- (BOOL)mouseInTitleBar:(NSEvent *)event {
|
||||
NSPoint windowLoc = [self convertPoint:event.locationInWindow fromView:nil];
|
||||
// windowLoc.y is the distance from the bottom of the window to the cursor
|
||||
// NSHeight(window.frame) will be the height of the whole window, so
|
||||
// NSHeight - titlebarHeight will be the bottom border of the titlebar
|
||||
return NSHeight(self.window.frame) - [self titlebarHeight] <= windowLoc.y;
|
||||
}
|
||||
|
||||
// See if the user double clicked in the titlebar. If so, do whatever
|
||||
// action is given by preferences.
|
||||
// \return true if handled, false otherwise.
|
||||
- (BOOL)handleTitleBarDoubleClick:(NSEvent *)event {
|
||||
NSWindow *window = self.window;
|
||||
NSWindowStyleMask styleMask = window.styleMask;
|
||||
// Was this a double click in a full-sized content view, not in full screen?
|
||||
if (event.clickCount != 2) return NO;
|
||||
if (!(styleMask & NSWindowStyleMaskFullSizeContentView)) return NO;
|
||||
if (styleMask & NSWindowStyleMaskFullScreen) return NO;
|
||||
|
||||
// See if our point is in the titlebar of the window.
|
||||
if (![self mouseInTitleBar:event]) return NO;
|
||||
|
||||
// Ok, do the action.
|
||||
NSString *action =
|
||||
[[NSUserDefaults standardUserDefaults] objectForKey:@"AppleActionOnDoubleClick"];
|
||||
|
||||
// When user has not explicitly ticked or unticked the `Double-click the window's
|
||||
// title bar to` option in system preferences, the NSUserDefaults will not have the key
|
||||
// "AppleActionOnDoubleClick", despite in system preferences the default is to "Zoom".
|
||||
// To make the behavior consistent, when the key is nil, we set performZoom as the
|
||||
// default behavior here.
|
||||
if ([action isEqualToString:@"Minimize"]) {
|
||||
[window performMiniaturize:nil];
|
||||
return YES;
|
||||
} else if (action == nil || [action isEqualToString:@"Maximize"]) {
|
||||
[window performZoom:nil];
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)viewDidChangeBackingProperties {
|
||||
if (self.readyForWarp) warp_view_did_change_backing_properties(self, asyncCallback);
|
||||
[super viewDidChangeBackingProperties];
|
||||
}
|
||||
|
||||
- (void)setFrameSize:(NSSize)size {
|
||||
BOOL changed = !NSEqualSizes(size, self.frame.size);
|
||||
// We could receive invalid frame sizes when the window is moved offscreen.
|
||||
// Validate the size against the minimum drawable size of the window before
|
||||
// passing to the rust side.
|
||||
if (size.height >= self.window.minSize.height && size.width >= self.window.minSize.width) {
|
||||
[super setFrameSize:size];
|
||||
// It's an important optimization to only invoke this if the size changed.
|
||||
if (self.readyForWarp && changed) {
|
||||
warp_view_set_frame_size(self, size, asyncCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)displayLayer:(CALayer *)layer {
|
||||
if (!testMode && self.readyForWarp) {
|
||||
warp_update_layer(self);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAsyncCallback:(BOOL)shouldAsync {
|
||||
asyncCallback = shouldAsync;
|
||||
}
|
||||
|
||||
- (void)keyDown:(NSEvent *)event {
|
||||
[self keyDownImpl:event];
|
||||
}
|
||||
|
||||
- (BOOL)keyDownImpl:(NSEvent *)event {
|
||||
BOOL wasComposing = [self hasMarkedText];
|
||||
[textToInsert setString:@""];
|
||||
|
||||
// Interpret the key events here so we could check whether user is composing
|
||||
// text within the IME and pass the state down to the KeyDown events.
|
||||
interpretingKeyEvents = YES;
|
||||
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
|
||||
interpretingKeyEvents = NO;
|
||||
|
||||
BOOL handled = NO;
|
||||
if (self.readyForWarp) {
|
||||
handled = warp_handle_view_event(self, event, wasComposing || [self hasMarkedText]);
|
||||
}
|
||||
|
||||
// It's possible to have keybinding conflicts between terminal apps which use the meta key and
|
||||
// MacOS "dead keys". Dead keys are used to add diacritical marks to other characters, and they
|
||||
// start composing marked text. To detect if a keybinding was triggered in the app, `handled`
|
||||
// will be true. If that is the case, we don't want MacOS to also start composing because we
|
||||
// already handled that keydown elsewhere. So, if `justStartedComposing` is also true, clear
|
||||
// out the marked text.
|
||||
// https://support.apple.com/guide/mac-help/enter-characters-with-accent-marks-on-mac-mh27474/mac#mchl45cdda7f
|
||||
BOOL justStartedComposing = !wasComposing && [self hasMarkedText];
|
||||
if (handled && justStartedComposing) {
|
||||
NSTextInputContext *inputContext = [self inputContext];
|
||||
[inputContext discardMarkedText];
|
||||
[self unmarkText];
|
||||
}
|
||||
|
||||
// Dispatch TypedCharacter event after KeyDown has been dispatched.
|
||||
if ([textToInsert length] > 0 && !handled) {
|
||||
warp_handle_insert_text(self, (NSString *)textToInsert);
|
||||
[self unmarkText];
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstMouse:(NSEvent *)event {
|
||||
// We want to receive mouseDown events even if the window is not key
|
||||
// and we explicity fire the event here so that Warp can handle it.
|
||||
if (self.readyForWarp) warp_handle_first_mouse_event(self, event);
|
||||
|
||||
// We return NO though so that the event is not fired twice (returning YES
|
||||
// would result in the event being passed to the mouseDown handler).
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)mouseDown:(NSEvent *)event {
|
||||
if (self.readyForWarp) {
|
||||
BOOL eventHandled = warp_handle_view_event(self, event, NO);
|
||||
if (self->titlebarDragEnabled && !eventHandled && [self mouseInTitleBar:event]) {
|
||||
// If Warp doesn't do anything with the event, indicated by returning `false`, and
|
||||
// if the drag starts in the titlebar, begin dragging the window
|
||||
[self.window performWindowDragWithEvent:event];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mouseUp:(NSEvent *)event {
|
||||
// Our content view is full-size so we don't get the default behavior
|
||||
// on titlebar clicks. Implement it manually.
|
||||
BOOL warp_handled = NO;
|
||||
if (self.readyForWarp) {
|
||||
warp_handled = warp_handle_view_event(self, event, NO);
|
||||
}
|
||||
if (!warp_handled) {
|
||||
[self handleTitleBarDoubleClick:event];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)otherMouseDown:(NSEvent *)event {
|
||||
if (self.readyForWarp) warp_handle_view_event(self, event, NO);
|
||||
}
|
||||
|
||||
- (void)rightMouseDown:(NSEvent *)event {
|
||||
if (self.readyForWarp) warp_handle_view_event(self, event, NO);
|
||||
}
|
||||
|
||||
- (void)mouseDragged:(NSEvent *)event {
|
||||
if (self.readyForWarp) warp_handle_view_event(self, event, NO);
|
||||
}
|
||||
|
||||
- (void)scrollWheel:(NSEvent *)event {
|
||||
if (self.readyForWarp) warp_handle_view_event(self, event, NO);
|
||||
}
|
||||
|
||||
- (void)mouseMoved:(NSEvent *)event {
|
||||
if (self.readyForWarp) warp_handle_view_event(self, event, NO);
|
||||
}
|
||||
|
||||
- (void)flagsChanged:(NSEvent *)event {
|
||||
if (self.readyForWarp) warp_handle_view_event(self, event, NO);
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[markedText release];
|
||||
[textToInsert release];
|
||||
[metalDevice release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (CALayer *)makeBackingLayer {
|
||||
CAMetalLayer *layer = [CAMetalLayer layer];
|
||||
layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
|
||||
layer.device = metalDevice;
|
||||
layer.allowsNextDrawableTimeout = NO;
|
||||
layer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
|
||||
layer.needsDisplayOnBoundsChange = YES;
|
||||
layer.presentsWithTransaction = YES;
|
||||
layer.delegate = self;
|
||||
layer.opaque = NO;
|
||||
return layer;
|
||||
}
|
||||
|
||||
- (WarpHostView *)initWithFrame:(NSRect)frame
|
||||
metalDevice:(id)device
|
||||
enableTitlebarDrag:(BOOL)enableTitlebarDrag
|
||||
testMode:(BOOL)testModeFlag {
|
||||
NSAssert(testModeFlag || device, @"Nil metal device not in test mode");
|
||||
[super initWithFrame:frame];
|
||||
|
||||
// Register here so we could receive drag and drop events.
|
||||
[self registerForDraggedTypes:@[
|
||||
NSPasteboardTypeFileURL,
|
||||
]];
|
||||
self->testMode = testModeFlag;
|
||||
self->titlebarDragEnabled = enableTitlebarDrag;
|
||||
self->metalDevice = [device retain];
|
||||
self->markedText = [[NSMutableAttributedString alloc] init];
|
||||
self->textToInsert = [[NSMutableString alloc] init];
|
||||
self->asyncCallback = YES;
|
||||
self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
self.wantsLayer = YES;
|
||||
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
|
||||
return self;
|
||||
}
|
||||
|
||||
// Entry point for drag & drop. Check whether the source is an acceptable type and if so
|
||||
// pass it down to performDragOperaion.
|
||||
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
|
||||
NSDragOperation sourceMask = [sender draggingSourceOperationMask];
|
||||
|
||||
BOOL pasteOK =
|
||||
!![[sender draggingPasteboard] availableTypeFromArray:@[ NSPasteboardTypeFileURL ]];
|
||||
if (pasteOK && (sourceMask & NSDragOperationCopy)) {
|
||||
return NSDragOperationCopy;
|
||||
}
|
||||
return NSDragOperationNone;
|
||||
}
|
||||
|
||||
// Called continuously while the drag operation is occurring within the view
|
||||
- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
|
||||
NSPoint dragPoint = [sender draggingLocation];
|
||||
NSPoint localPoint = [self convertPoint:dragPoint fromView:nil];
|
||||
|
||||
NSPasteboard *pasteboard = [sender draggingPasteboard];
|
||||
if (self.readyForWarp) {
|
||||
NSArray *types = [pasteboard types];
|
||||
if ([types containsObject:NSPasteboardTypeFileURL]) {
|
||||
warp_handle_file_drag(self, localPoint);
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NSDragOperationNone;
|
||||
}
|
||||
|
||||
- (void)draggingExited:(id<NSDraggingInfo>)sender {
|
||||
if (self.readyForWarp) {
|
||||
warp_handle_file_drag_exit(self);
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
|
||||
NSPasteboard *pasteboard = [sender draggingPasteboard];
|
||||
NSDragOperation dragOperation = [sender draggingSourceOperationMask];
|
||||
|
||||
NSPoint dragPoint = [sender draggingLocation];
|
||||
NSPoint localPoint = [self convertPoint:dragPoint fromView:nil];
|
||||
|
||||
if (self.readyForWarp && (dragOperation & NSDragOperationCopy)) {
|
||||
NSArray *types = [pasteboard types];
|
||||
if ([types containsObject:NSPasteboardTypeFileURL]) {
|
||||
warp_handle_drag_and_drop(self, [pasteboard getFilePaths], localPoint);
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)closeIMEAsync {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSTextInputContext *inputContext = [self inputContext];
|
||||
[inputContext discardMarkedText];
|
||||
|
||||
[self unmarkText];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Accessibility
|
||||
- (BOOL)isAccessibilityElement {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSAccessibilityRole)accessibilityRole {
|
||||
return NSAccessibilityTextAreaRole;
|
||||
}
|
||||
|
||||
- (NSString *)accessibilityRoleDescription {
|
||||
return NSAccessibilityRoleDescriptionForUIElement(self);
|
||||
}
|
||||
|
||||
- (BOOL)isAccessibilityFocused {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (id)accessibilityValue {
|
||||
return warp_get_accessibility_contents(self);
|
||||
}
|
||||
|
||||
- (NSInteger)accessibilityNumberOfCharacters {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (NSInteger)accessibilityInsertionPointLineNumber {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (NSString *)accessibilityDocument {
|
||||
return nil;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// NSTextInputClient protocol implementation
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (nullable NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
|
||||
actualRange:
|
||||
(nullable NSRangePointer)actualRange {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint {
|
||||
return (NSUInteger)0;
|
||||
}
|
||||
|
||||
// This is a no-op as we will be handling control characters in KeyDown events.
|
||||
- (void)doCommandBySelector:(SEL)selector {
|
||||
}
|
||||
|
||||
- (NSRect)firstRectForCharacterRange:(NSRange)range
|
||||
actualRange:(nullable NSRangePointer)actualRange {
|
||||
NSWindow *window = self.window;
|
||||
if (self.readyForWarp) {
|
||||
NSRect contentRect = [window contentRectForFrameRect:[window frame]];
|
||||
NSRect rect = warp_ime_position(self, &contentRect);
|
||||
return rect;
|
||||
} else {
|
||||
return NSZeroRect;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)hasMarkedText {
|
||||
return [markedText length] > 0;
|
||||
}
|
||||
|
||||
// Referenced glfw for this implementation.
|
||||
// https://github.com/glfw/glfw/blob/7ef34eb06de54dd9186d3d21a401b2ef819b59e7/src/cocoa_window.m#L814
|
||||
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange {
|
||||
if (self.readyForWarp) {
|
||||
NSMutableString *characters = [[NSMutableString alloc] init];
|
||||
|
||||
if ([string isKindOfClass:[NSAttributedString class]]) {
|
||||
// We are appending rather than replacing here because sometimes insertText
|
||||
// could be fired multiple times in a row. For example, when user types
|
||||
// Option-E followed by g, insertText will fire ´ first and then g.
|
||||
[characters appendString:[string string]];
|
||||
} else {
|
||||
[characters appendString:(NSString *)string];
|
||||
}
|
||||
|
||||
// If we're in the middle of a call to interpretKeyEvents, batch up all
|
||||
// inserted text, as we may handle the event during `keyDown`. If this
|
||||
// call to `insertText` is not in a call stack underneath `keyDown`
|
||||
// (e.g.: when inserting an emoji from the emoji composer), just insert
|
||||
// the text directly.
|
||||
if (interpretingKeyEvents) {
|
||||
[textToInsert appendString:characters];
|
||||
} else {
|
||||
warp_handle_insert_text(self, (NSString *)characters);
|
||||
}
|
||||
|
||||
[characters release];
|
||||
}
|
||||
// When handling the key down Enter, we might need to rely on the IME being open
|
||||
// to accept the marked text as-is and so can't call unmarkText.
|
||||
if (!interpretingKeyEvents) {
|
||||
[self unmarkText];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSRange)markedRange {
|
||||
if ([markedText length] > 0)
|
||||
return NSMakeRange(0, [markedText length]);
|
||||
else
|
||||
return NSMakeRange(NSNotFound, 0);
|
||||
}
|
||||
|
||||
- (NSRange)selectedRange {
|
||||
return NSMakeRange(0, 0);
|
||||
}
|
||||
|
||||
- (void)setMarkedText:(id)string
|
||||
selectedRange:(NSRange)selectedRange
|
||||
replacementRange:(NSRange)replacementRange {
|
||||
[markedText release];
|
||||
if ([string isKindOfClass:[NSAttributedString class]])
|
||||
markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string];
|
||||
else
|
||||
markedText = [[NSMutableAttributedString alloc] initWithString:string];
|
||||
|
||||
if (self.readyForWarp) {
|
||||
warp_marked_text_updated(self, markedText.string, selectedRange);
|
||||
if ([markedText length] > 0) {
|
||||
warp_update_ime_state(self, YES);
|
||||
} else {
|
||||
warp_update_ime_state(self, NO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)unmarkText {
|
||||
[[markedText mutableString] setString:@""];
|
||||
if (self.readyForWarp) {
|
||||
warp_update_ime_state(self, NO);
|
||||
warp_marked_text_cleared(self);
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)validAttributesForMarkedText {
|
||||
return [NSArray array];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <Carbon/Carbon.h>
|
||||
|
||||
@interface WarpHotKey : NSObject {
|
||||
@public
|
||||
EventHotKeyRef _eventHotKey;
|
||||
@public
|
||||
NSUInteger _keyCode;
|
||||
@public
|
||||
NSUInteger _modifierKeys;
|
||||
}
|
||||
|
||||
- (instancetype)initWithEventHotKey:(EventHotKeyRef)eventHotKey
|
||||
keyCode:(NSUInteger)keyCode
|
||||
modifierKeys:(NSUInteger)modifierKeys;
|
||||
|
||||
- (BOOL)hotKeyKeyAndModifierEquals:(NSUInteger)keyCode modifierKeys:(NSUInteger)modifierKeys;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,24 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <Carbon/Carbon.h>
|
||||
|
||||
#import "hotkey.h"
|
||||
|
||||
@implementation WarpHotKey
|
||||
|
||||
- (instancetype)initWithEventHotKey:(EventHotKeyRef)eventHotKey
|
||||
keyCode:(NSUInteger)keyCode
|
||||
modifierKeys:(NSUInteger)modifierKeys {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_eventHotKey = eventHotKey;
|
||||
_keyCode = keyCode;
|
||||
_modifierKeys = modifierKeys;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)hotKeyKeyAndModifierEquals:(NSUInteger)keyCode modifierKeys:(NSUInteger)modifierKeys {
|
||||
return keyCode == _keyCode && modifierKeys == _modifierKeys;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,212 @@
|
||||
#include <AppKit/AppKit.h>
|
||||
#include <Carbon/Carbon.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
// One virtual key could map to multiple physical keys on the keyboard. So we keep
|
||||
// a mapping from key name to an array of keycodes.
|
||||
NSMutableDictionary<NSString*, NSMutableArray<NSNumber*>*>* keycodeDict;
|
||||
|
||||
BOOL IsUnicodeControl(unichar c) {
|
||||
// C0 control characters: http://unicode.org/charts/PDF/U0000.pdf
|
||||
// C1 control characters: http://unicode.org/charts/PDF/U0080.pdf
|
||||
return c <= 0x1F || (c >= 0x7F && c <= 0x9F);
|
||||
}
|
||||
|
||||
// Control character naming needs to be in sync with the corresponding rust definition in
|
||||
// `event.rs`: see
|
||||
// https://github.com/warpdotdev/warp-internal/blob/master/ui/src/platform/mac/utils.rs#L42
|
||||
// The list of control characters are referenced from chromium code here:
|
||||
// https://chromium.googlesource.com/chromium/src/+/lkgr/ui/events/keycodes/keyboard_code_conversion_mac.mm#329
|
||||
NSString* KeyFromControlKeyCode(unsigned short keyCode) {
|
||||
switch (keyCode) {
|
||||
case kVK_ANSI_KeypadEnter:
|
||||
return @"numpadenter";
|
||||
case kVK_Return:
|
||||
return @"enter";
|
||||
case kVK_Tab:
|
||||
return @"tab";
|
||||
case kVK_Delete:
|
||||
return @"backspace";
|
||||
case kVK_Escape:
|
||||
return @"escape";
|
||||
case kVK_F1:
|
||||
return @"f1";
|
||||
case kVK_F2:
|
||||
return @"f2";
|
||||
case kVK_F3:
|
||||
return @"f3";
|
||||
case kVK_F4:
|
||||
return @"f4";
|
||||
case kVK_F5:
|
||||
return @"f5";
|
||||
case kVK_F6:
|
||||
return @"f6";
|
||||
case kVK_F7:
|
||||
return @"f7";
|
||||
case kVK_F8:
|
||||
return @"f8";
|
||||
case kVK_F9:
|
||||
return @"f9";
|
||||
case kVK_F10:
|
||||
return @"f10";
|
||||
case kVK_F11:
|
||||
return @"f11";
|
||||
case kVK_F12:
|
||||
return @"f12";
|
||||
case kVK_F13:
|
||||
return @"f13";
|
||||
case kVK_F14:
|
||||
return @"f14";
|
||||
case kVK_F15:
|
||||
return @"f15";
|
||||
case kVK_F16:
|
||||
return @"f16";
|
||||
case kVK_F17:
|
||||
return @"f17";
|
||||
case kVK_F18:
|
||||
return @"f18";
|
||||
case kVK_F19:
|
||||
return @"f19";
|
||||
case kVK_F20:
|
||||
return @"f20";
|
||||
case kVK_ForwardDelete:
|
||||
return @"delete";
|
||||
case kVK_Help:
|
||||
return @"insert";
|
||||
case kVK_Home:
|
||||
return @"home";
|
||||
case kVK_PageUp:
|
||||
return @"pageup";
|
||||
case kVK_End:
|
||||
return @"end";
|
||||
case kVK_PageDown:
|
||||
return @"pagedown";
|
||||
case kVK_LeftArrow:
|
||||
return @"left";
|
||||
case kVK_RightArrow:
|
||||
return @"right";
|
||||
case kVK_DownArrow:
|
||||
return @"down";
|
||||
case kVK_UpArrow:
|
||||
return @"up";
|
||||
default:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get the keyboard layout data
|
||||
CFDataRef GetKeyboardLayoutData() {
|
||||
TISInputSourceRef source = TISCopyCurrentKeyboardInputSource();
|
||||
CFDataRef layout_data =
|
||||
(CFDataRef)(TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData));
|
||||
if (!layout_data) {
|
||||
// TISGetInputSourceProperty returns null with some keyboard layouts (e.g. Japanese and
|
||||
// Chinese). Using TISCopyCurrentKeyboardLayoutInputSource to fix NULL return.
|
||||
source = TISCopyCurrentKeyboardLayoutInputSource();
|
||||
layout_data =
|
||||
(CFDataRef)(TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData));
|
||||
}
|
||||
return layout_data;
|
||||
}
|
||||
|
||||
// Referenced from chromium:
|
||||
// https://chromium.googlesource.com/chromium/src/+/lkgr/ui/events/keycodes/keyboard_code_conversion_mac.mm
|
||||
// Here we take the keyboard layout, keycode, modifier keys, and keyboard type to
|
||||
// determine the output character.
|
||||
// Notice that we don't yet handle multiple character case here. But this should
|
||||
// be minor given Chrome also doesn't support it.
|
||||
UniChar TranslatedUnicodeCharFromKeyCode(CFDataRef layout_data, UInt16 key_code,
|
||||
UInt32 modifier_key_state, UInt32 keyboard_type) {
|
||||
if (!layout_data) return 0xFFFD; // REPLACEMENT CHARACTER
|
||||
|
||||
const UCKeyboardLayout* keyboardLayout = (const UCKeyboardLayout*)CFDataGetBytePtr(layout_data);
|
||||
|
||||
UInt32 deadKeyState = 0;
|
||||
UniCharCount maxStringLength = 255;
|
||||
UniCharCount actualStringLength = 0;
|
||||
UniChar unicodeString[maxStringLength];
|
||||
|
||||
UCKeyTranslate(keyboardLayout, key_code, kUCKeyActionDown, modifier_key_state, keyboard_type,
|
||||
kUCKeyTranslateNoDeadKeysBit, &deadKeyState, maxStringLength,
|
||||
&actualStringLength, unicodeString);
|
||||
// TODO(kevin): Handle multiple character case. Should be rare.
|
||||
return unicodeString[0];
|
||||
}
|
||||
|
||||
// Convert keycode to its corresponding character on the keyboard.
|
||||
NSString* keyCodeToChar(UInt16 keyCode, BOOL shifted) {
|
||||
UInt32 modifier_key_state = 0;
|
||||
|
||||
// The shift key representation in Carbon is 1 << 9.
|
||||
// However, UCKeyTranslate takes the modifier keys and shift them by 8 bits. So we
|
||||
// only need to pass in 1 << 1 here.
|
||||
if (shifted) {
|
||||
modifier_key_state = 1 << 1;
|
||||
}
|
||||
|
||||
CFDataRef layout_data = GetKeyboardLayoutData();
|
||||
UniChar translated_char =
|
||||
TranslatedUnicodeCharFromKeyCode(layout_data, keyCode, modifier_key_state, LMGetKbdLast());
|
||||
|
||||
// UCKeyTranslate can't translate control characters like function keys and arrow
|
||||
// keys. We keep a separate mapping for this case. This is the same behavior as chromium:
|
||||
// https://chromium.googlesource.com/chromium/src/+/lkgr/ui/events/keycodes/keyboard_code_conversion_mac.mm#873
|
||||
if (IsUnicodeControl(translated_char)) {
|
||||
return KeyFromControlKeyCode(keyCode);
|
||||
} else {
|
||||
return [NSString stringWithFormat:@"%C", translated_char];
|
||||
}
|
||||
}
|
||||
|
||||
NSArray<NSNumber*>* charToKeyCodes(NSString* keyChar) {
|
||||
if (keycodeDict == nil) {
|
||||
keycodeDict = [[NSMutableDictionary alloc] init];
|
||||
CFDataRef layout_data = GetKeyboardLayoutData();
|
||||
|
||||
// For every keycode.
|
||||
size_t i;
|
||||
for (i = 0; i < 128; ++i) {
|
||||
UInt32 shift_key = 1 << 1;
|
||||
|
||||
// Compute a shifted and unshifted version for one keycode.
|
||||
UniChar unshifted =
|
||||
TranslatedUnicodeCharFromKeyCode(layout_data, (UInt16)i, 0, LMGetKbdLast());
|
||||
UniChar shifted =
|
||||
TranslatedUnicodeCharFromKeyCode(layout_data, (UInt16)i, shift_key, LMGetKbdLast());
|
||||
|
||||
NSString* unshifted_str;
|
||||
if (IsUnicodeControl(unshifted)) {
|
||||
unshifted_str = KeyFromControlKeyCode(i);
|
||||
} else {
|
||||
unshifted_str = [NSString stringWithFormat:@"%C", unshifted];
|
||||
}
|
||||
|
||||
NSString* shifted_str;
|
||||
if (IsUnicodeControl(shifted)) {
|
||||
shifted_str = KeyFromControlKeyCode(i);
|
||||
} else {
|
||||
shifted_str = [NSString stringWithFormat:@"%C", shifted];
|
||||
}
|
||||
|
||||
if (unshifted_str != nil && [unshifted_str length] > 0) {
|
||||
if ([keycodeDict objectForKey:unshifted_str] == nil) {
|
||||
[keycodeDict setObject:[[[NSMutableArray alloc] init] autorelease]
|
||||
forKey:unshifted_str];
|
||||
}
|
||||
NSMutableArray* keycodes = [keycodeDict objectForKey:unshifted_str];
|
||||
[keycodes addObject:[NSNumber numberWithInt:i]];
|
||||
}
|
||||
|
||||
if (shifted_str != nil && [shifted_str length] > 0) {
|
||||
if ([keycodeDict objectForKey:shifted_str] == nil) {
|
||||
[keycodeDict setObject:[[[NSMutableArray alloc] init] autorelease]
|
||||
forKey:shifted_str];
|
||||
}
|
||||
NSMutableArray* keycodes = [keycodeDict objectForKey:shifted_str];
|
||||
[keycodes addObject:[NSNumber numberWithInt:i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [keycodeDict objectForKey:keyChar];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
// WarpCustomMenuItemHandler is set as both the target and represented object of NSMenuItem.
|
||||
// It gives the Rust side a chance to dynamically update menu items, and
|
||||
// respond to their actions.
|
||||
@interface WarpCustomMenuItemHandler : NSObject <NSMenuItemValidation> {
|
||||
void *rustContext;
|
||||
}
|
||||
|
||||
// Init, wrapping a pointer which is significant to Rust.
|
||||
- (id)initWithContext:(void *)wrapper;
|
||||
|
||||
// Action set on menu items.
|
||||
- (void)itemWasTriggered:(NSMenuItem *)item;
|
||||
|
||||
// Called when the menu item needs updating.
|
||||
- (void)itemNeedsUpdate:(NSMenuItem *)item;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,42 @@
|
||||
#import "menus.h"
|
||||
|
||||
void warp_menu_item_needs_update(NSMenuItem *, void *);
|
||||
void warp_menu_item_triggered(NSMenuItem *, void *);
|
||||
void warp_menu_item_deallocated(void *);
|
||||
|
||||
@implementation WarpCustomMenuItemHandler
|
||||
|
||||
- (id)initWithContext:(void *)context {
|
||||
self = [super init];
|
||||
rustContext = context;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)itemWasTriggered:(NSMenuItem *)item {
|
||||
if (rustContext && ![item hasSubmenu]) warp_menu_item_triggered(item, rustContext);
|
||||
}
|
||||
|
||||
- (void)itemNeedsUpdate:(NSMenuItem *)item {
|
||||
if (rustContext) warp_menu_item_needs_update(item, rustContext);
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (rustContext) warp_menu_item_deallocated(rustContext);
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
/// Our custom menu items set their enabled state in menuNeedsUpdate:, so do nothing here.
|
||||
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
|
||||
return menuItem.isEnabled;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
void set_menu_item_submenu(NSMenuItem *item, NSMenu *submenu) {
|
||||
if (submenu == nil) {
|
||||
[item setAction:@selector(itemWasTriggered:)];
|
||||
} else {
|
||||
[item setAction:NULL];
|
||||
}
|
||||
[item setSubmenu:submenu];
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# MacOS User Notifcations
|
||||
|
||||
The Apple framework we use to support notifications is `UNUserNotifications`: https://developer.apple.com/documentation/usernotifications?language=objc
|
||||
|
||||
## Developing notifications locally
|
||||
The framework needs a signed app to be able to request authorization and schedule notifications to Apple's Notification Center. For this reason, it is not enough to `cargo build && cargo run`. Instead, there are a couple of options:
|
||||
|
||||
### 1. Bundle the app
|
||||
This takes a longer time than option 2, but it is more stable, and it is what the user will ultimately experience.
|
||||
|
||||
1. Run `script/user_notifications --nouniversal --open`
|
||||
|
||||
If you want to test the authorization flow specifically, you will have to:
|
||||
1. Delete all the `WarpDev` apps you have installed locally
|
||||
2. Ensure that `WarpDev` isn't an app in your Notification Center by checking which apps show up in `Notifications` in your System Preferences.
|
||||
3. Log out*
|
||||
4. Log back in and bundle&run the app again.
|
||||
|
||||
### 2. Nosign the local build (script)
|
||||
This is less stable than option 1 and is not recommended if you're testing the authorization flow.
|
||||
|
||||
1. Ensure that you have a WarpDev app installed, _and in your Applications folder_. It's important to have the app in your Applications, or Apple won't be able to find the app while testing notifications.
|
||||
2. Run `script/local_build_and_sign`
|
||||
|
||||
If you want to test the authorization flow specifically, you will have to:
|
||||
1. Delete all the `WarpDev` apps you have installed locally, including the one in your Applications folder.
|
||||
2. Ensure that `WarpDev` isn't an app in your Notification Center by checking which apps show up in `Notifications` in your System Preferences.
|
||||
3. Log out* and log back in.
|
||||
4. Move the `WarpDev` from `Bin` to `Applications` (i.e. install `WarpDev`).
|
||||
5. Run the script to nosign and run the app again: `script/local_build_and_sign`.
|
||||
|
||||
*NB: If you already have all the permissions to send notifications, and you're not testing the authorization flow, you should be able to do the following instead of logging out & in:
|
||||
1. Delete all `WarpDev` apps.
|
||||
2. Run `sudo lsof | grep usernoted | grep db2` to find the path to a database that Notification Center uses.
|
||||
3. Run `killall usernoted && killall NotificationCenter`
|
||||
4. Run `rm <path-to-notification-center-db>`
|
||||
5. Build and run the app again (if you're not bundling, you still have to move `WarpDev` back to your `Applications` folder).
|
||||
|
||||
## Debugging notifications
|
||||
Some useful methods for debugging errors / if things aren't working as expected:
|
||||
- Check Notification Center to figure out if `WarpDev` is a registered app and to play around with the settings (e.g. turning on/off, enabling sound, etc)
|
||||
- Use `NSLog` to print debug statements in local builds
|
||||
- Use the Console app for more helpful framework errors - this is particularly helpful when you don't see any error from your own logs. Filter the messages by `NotificationCenter` or `usernoted` or `dev`
|
||||
- When in doubt, delete all `WarpDev` apps and restart your laptop. Sometimes Notification Center needs a gentle nudge.
|
||||
@@ -0,0 +1,16 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
// Requests authorization for notifications.
|
||||
void requestNotificationPermissions(void* callback);
|
||||
|
||||
// This method, implemented in Rust, invokes the callback to allow the App to
|
||||
// take action when the user has responded to the permissions request.
|
||||
void warp_on_request_notification_permissions_completed(NSUInteger outcome_type, id outcome_msg,
|
||||
void* callback);
|
||||
|
||||
// Sends a desktop notification.
|
||||
void sendNotification(id, id, id, void*, BOOL);
|
||||
|
||||
// This method, implemented in Rust, invokes the callback to allow the App to
|
||||
// take action when a notification fails to send.
|
||||
void warp_on_notification_send_error(NSUInteger error_type, id error_msg, void* callback);
|
||||
@@ -0,0 +1,101 @@
|
||||
#import "notifications.h"
|
||||
#import "../app.h"
|
||||
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
|
||||
void requestNotificationPermissionsWithCompletionHandler(
|
||||
void (^completion_handler)(NSUInteger outcome_type, id outcome_msg)) {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
|
||||
[center
|
||||
requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UNAuthorizationOptionSound +
|
||||
UNAuthorizationOptionBadge)
|
||||
completionHandler:^(BOOL granted, NSError *_Nullable error) {
|
||||
if (!granted) {
|
||||
completion_handler(1, @"User denied request to receive notifications.");
|
||||
} else if (error != nil) {
|
||||
completion_handler(2, error.localizedDescription);
|
||||
} else {
|
||||
// Create and register the notification category.
|
||||
UNNotificationCategory *CustomizedNotification = [UNNotificationCategory
|
||||
categoryWithIdentifier:@"CUSTOMIZED_NOTIFICATION"
|
||||
actions:@[]
|
||||
intentIdentifiers:@[]
|
||||
options:
|
||||
UNNotificationCategoryOptionCustomDismissAction];
|
||||
|
||||
[center
|
||||
setNotificationCategories:[NSSet
|
||||
setWithObjects:CustomizedNotification,
|
||||
nil]];
|
||||
completion_handler(0,
|
||||
@"User accepted request to receive notifications.");
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
void requestNotificationPermissions(void *on_completion_callback) {
|
||||
requestNotificationPermissionsWithCompletionHandler(^(NSUInteger outcome_type, id outcome_msg) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
warp_on_request_notification_permissions_completed(outcome_type, outcome_msg,
|
||||
on_completion_callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void sendNotificationWithErrorHandler(NSString *title, NSString *body, NSString *data,
|
||||
void (^error_handler)(NSUInteger error_type, id error_msg),
|
||||
BOOL playSound) {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) {
|
||||
if (settings.authorizationStatus == UNAuthorizationStatusDenied) {
|
||||
error_handler(0, @"User turned permissions off in system preferences.");
|
||||
} else {
|
||||
// Create the notification content.
|
||||
// `autorelease` balances the +1 retain from `alloc`; the enclosing UserNotifications
|
||||
// completion block runs on a GCD-dispatched queue that drains an ambient pool.
|
||||
UNMutableNotificationContent *content =
|
||||
[[[UNMutableNotificationContent alloc] init] autorelease];
|
||||
content.title = [NSString localizedUserNotificationStringForKey:title arguments:nil];
|
||||
content.body = [NSString localizedUserNotificationStringForKey:body arguments:nil];
|
||||
|
||||
// Only play sound if the user setting allows it
|
||||
if (playSound) {
|
||||
content.sound = [UNNotificationSound defaultSound];
|
||||
}
|
||||
|
||||
content.userInfo = @{
|
||||
@"DATA" : data,
|
||||
};
|
||||
|
||||
// Configure the trigger to send the notification after 1 second.
|
||||
UNTimeIntervalNotificationTrigger *trigger =
|
||||
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO];
|
||||
|
||||
// Create the request object.
|
||||
UNNotificationRequest *request =
|
||||
[UNNotificationRequest requestWithIdentifier:@"CUSTOMIZED_NOTIFICATION"
|
||||
content:content
|
||||
trigger:trigger];
|
||||
|
||||
// Schedule the notification.
|
||||
[center addNotificationRequest:request
|
||||
withCompletionHandler:^(NSError *_Nullable err) {
|
||||
if (err != nil) {
|
||||
error_handler(1, err.localizedDescription);
|
||||
}
|
||||
}];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
void sendNotification(id title, id body, id data, void *on_error_callback, BOOL playSound) {
|
||||
sendNotificationWithErrorHandler(
|
||||
title, body, data,
|
||||
^(NSUInteger error_type, id error_msg) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
warp_on_notification_send_error(error_type, error_msg, on_error_callback);
|
||||
});
|
||||
},
|
||||
playSound);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
|
||||
//! Project version number for MacOSReachability.
|
||||
FOUNDATION_EXPORT double ReachabilityVersionNumber;
|
||||
|
||||
//! Project version string for MacOSReachability.
|
||||
FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[];
|
||||
|
||||
/**
|
||||
* Create NS_ENUM macro if it does not exist on the targeted version of iOS or OS X.
|
||||
*
|
||||
* @see http://nshipster.com/ns_enum-ns_options/
|
||||
**/
|
||||
#ifndef NS_ENUM
|
||||
#define NS_ENUM(_type, _name) \
|
||||
enum _name : _type _name; \
|
||||
enum _name : _type
|
||||
#endif
|
||||
|
||||
extern NSString *const kReachabilityChangedNotification;
|
||||
|
||||
typedef NS_ENUM(NSInteger, NetworkStatus) {
|
||||
// Apple NetworkStatus Compatible Names.
|
||||
NotReachable = 0,
|
||||
ReachableViaWiFi = 2,
|
||||
ReachableViaWWAN = 1
|
||||
};
|
||||
|
||||
@class Reachability;
|
||||
|
||||
typedef void (^NetworkReachable)(Reachability *reachability);
|
||||
typedef void (^NetworkUnreachable)(Reachability *reachability);
|
||||
typedef void (^NetworkReachability)(Reachability *reachability, SCNetworkConnectionFlags flags);
|
||||
|
||||
@interface Reachability : NSObject
|
||||
|
||||
@property(nonatomic, copy) NetworkReachable reachableBlock;
|
||||
@property(nonatomic, copy) NetworkUnreachable unreachableBlock;
|
||||
@property(nonatomic, copy) NetworkReachability reachabilityBlock;
|
||||
|
||||
@property(nonatomic, assign) BOOL reachableOnWWAN;
|
||||
|
||||
+ (instancetype)reachabilityWithHostname:(NSString *)hostname;
|
||||
// This is identical to the function above, but is here to maintain
|
||||
// compatibility with Apples original code. (see .m)
|
||||
+ (instancetype)reachabilityWithHostName:(NSString *)hostname;
|
||||
+ (instancetype)reachabilityForInternetConnection;
|
||||
+ (instancetype)reachabilityWithAddress:(void *)hostAddress;
|
||||
+ (instancetype)reachabilityForLocalWiFi;
|
||||
+ (instancetype)reachabilityWithURL:(NSURL *)url;
|
||||
|
||||
- (instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref;
|
||||
|
||||
- (BOOL)startNotifier;
|
||||
- (void)stopNotifier;
|
||||
|
||||
- (BOOL)isReachable;
|
||||
- (BOOL)isReachableViaWWAN;
|
||||
- (BOOL)isReachableViaWiFi;
|
||||
|
||||
// WWAN may be available, but not active until a connection has been established.
|
||||
// WiFi may require a connection for VPN on Demand.
|
||||
- (BOOL)isConnectionRequired; // Identical DDG variant.
|
||||
- (BOOL)connectionRequired; // Apple's routine.
|
||||
// Dynamic, on demand connection?
|
||||
- (BOOL)isConnectionOnDemand;
|
||||
// Is user intervention required?
|
||||
- (BOOL)isInterventionRequired;
|
||||
|
||||
- (NetworkStatus)currentReachabilityStatus;
|
||||
- (SCNetworkReachabilityFlags)reachabilityFlags;
|
||||
- (NSString *)currentReachabilityString;
|
||||
- (NSString *)currentReachabilityFlags;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,443 @@
|
||||
/*
|
||||
Copyright (c) 2011, Tony Million.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "reachability.h"
|
||||
|
||||
#import <arpa/inet.h>
|
||||
#import <ifaddrs.h>
|
||||
#import <netdb.h>
|
||||
#import <netinet/in.h>
|
||||
#import <netinet6/in6.h>
|
||||
#import <sys/socket.h>
|
||||
|
||||
NSString *const kReachabilityChangedNotification = @"kReachabilityChangedNotification";
|
||||
|
||||
@interface Reachability ()
|
||||
|
||||
@property(nonatomic, assign) SCNetworkReachabilityRef reachabilityRef;
|
||||
@property(nonatomic, strong) dispatch_queue_t reachabilitySerialQueue;
|
||||
@property(nonatomic, strong) id reachabilityObject;
|
||||
|
||||
- (void)reachabilityChanged:(SCNetworkReachabilityFlags)flags;
|
||||
- (BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags;
|
||||
|
||||
@end
|
||||
|
||||
static NSString *reachabilityFlags(SCNetworkReachabilityFlags flags) {
|
||||
return [NSString
|
||||
stringWithFormat:@"%c%c %c%c%c%c%c%c%c",
|
||||
#if TARGET_OS_IPHONE
|
||||
(flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-',
|
||||
#else
|
||||
'X',
|
||||
#endif
|
||||
(flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-',
|
||||
(flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-'];
|
||||
}
|
||||
|
||||
// Start listening for reachability notifications on the current run loop
|
||||
static void TMReachabilityCallback(SCNetworkReachabilityRef target,
|
||||
SCNetworkReachabilityFlags flags, void *info) {
|
||||
#pragma unused(target)
|
||||
|
||||
Reachability *reachability = ((__bridge Reachability *)info);
|
||||
|
||||
// We probably don't need an autoreleasepool here, as GCD docs state each queue has its own
|
||||
// autorelease pool, but what the heck eh?
|
||||
@autoreleasepool {
|
||||
[reachability reachabilityChanged:flags];
|
||||
}
|
||||
}
|
||||
|
||||
@implementation Reachability
|
||||
|
||||
#pragma mark - Class Constructor Methods
|
||||
|
||||
+ (instancetype)reachabilityWithHostName:(NSString *)hostname {
|
||||
return [Reachability reachabilityWithHostname:hostname];
|
||||
}
|
||||
|
||||
+ (instancetype)reachabilityWithHostname:(NSString *)hostname {
|
||||
SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [hostname UTF8String]);
|
||||
if (ref) {
|
||||
id reachability = [[[self alloc] initWithReachabilityRef:ref] autorelease];
|
||||
|
||||
return reachability;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (instancetype)reachabilityWithAddress:(void *)hostAddress {
|
||||
SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(
|
||||
kCFAllocatorDefault, (const struct sockaddr *)hostAddress);
|
||||
if (ref) {
|
||||
id reachability = [[[self alloc] initWithReachabilityRef:ref] autorelease];
|
||||
|
||||
return reachability;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (instancetype)reachabilityForInternetConnection {
|
||||
struct sockaddr_in zeroAddress;
|
||||
bzero(&zeroAddress, sizeof(zeroAddress));
|
||||
zeroAddress.sin_len = sizeof(zeroAddress);
|
||||
zeroAddress.sin_family = AF_INET;
|
||||
|
||||
return [self reachabilityWithAddress:&zeroAddress];
|
||||
}
|
||||
|
||||
+ (instancetype)reachabilityForLocalWiFi {
|
||||
struct sockaddr_in localWifiAddress;
|
||||
bzero(&localWifiAddress, sizeof(localWifiAddress));
|
||||
localWifiAddress.sin_len = sizeof(localWifiAddress);
|
||||
localWifiAddress.sin_family = AF_INET;
|
||||
// IN_LINKLOCALNETNUM is defined in <netinet/in.h> as 169.254.0.0
|
||||
localWifiAddress.sin_addr.s_addr = htonl(IN_LINKLOCALNETNUM);
|
||||
|
||||
return [self reachabilityWithAddress:&localWifiAddress];
|
||||
}
|
||||
|
||||
+ (instancetype)reachabilityWithURL:(NSURL *)url {
|
||||
id reachability;
|
||||
|
||||
NSString *host = url.host;
|
||||
BOOL isIpAddress = [self isIpAddress:host];
|
||||
|
||||
if (isIpAddress) {
|
||||
NSNumber *port = url.port ?: [url.scheme isEqualToString:@"https"] ? @(443) : @(80);
|
||||
|
||||
struct sockaddr_in address;
|
||||
address.sin_len = sizeof(address);
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons([port intValue]);
|
||||
address.sin_addr.s_addr = inet_addr([host UTF8String]);
|
||||
|
||||
reachability = [self reachabilityWithAddress:&address];
|
||||
} else {
|
||||
reachability = [self reachabilityWithHostname:host];
|
||||
}
|
||||
|
||||
return reachability;
|
||||
}
|
||||
|
||||
+ (BOOL)isIpAddress:(NSString *)host {
|
||||
struct in_addr pin;
|
||||
return 1 == inet_aton([host UTF8String], &pin);
|
||||
}
|
||||
|
||||
// Initialization methods
|
||||
|
||||
- (instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
self.reachableOnWWAN = YES;
|
||||
self.reachabilityRef = ref;
|
||||
|
||||
// We need to create a serial queue.
|
||||
// We allocate this once for the lifetime of the notifier.
|
||||
|
||||
self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stopNotifier];
|
||||
|
||||
if (self.reachabilityRef) {
|
||||
CFRelease(self.reachabilityRef);
|
||||
self.reachabilityRef = nil;
|
||||
}
|
||||
|
||||
self.reachableBlock = nil;
|
||||
self.unreachableBlock = nil;
|
||||
self.reachabilityBlock = nil;
|
||||
self.reachabilitySerialQueue = nil;
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
#pragma mark - Notifier Methods
|
||||
|
||||
// Notifier
|
||||
// NOTE: This uses GCD to trigger the blocks - they *WILL NOT* be called on THE MAIN THREAD
|
||||
// - In other words DO NOT DO ANY UI UPDATES IN THE BLOCKS.
|
||||
// INSTEAD USE dispatch_async(dispatch_get_main_queue(), ^{UISTUFF}) (or dispatch_sync if you
|
||||
// want)
|
||||
|
||||
- (BOOL)startNotifier {
|
||||
// allow start notifier to be called multiple times
|
||||
if (self.reachabilityObject && (self.reachabilityObject == self)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
SCNetworkReachabilityContext context = {0, NULL, NULL, NULL, NULL};
|
||||
context.info = (__bridge void *)self;
|
||||
|
||||
if (SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context)) {
|
||||
// Set it as our reachability queue, which will retain the queue
|
||||
if (SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef,
|
||||
self.reachabilitySerialQueue)) {
|
||||
// this should do a retain on ourself, so as long as we're in notifier mode we shouldn't
|
||||
// disappear out from under ourselves woah
|
||||
self.reachabilityObject = self;
|
||||
return YES;
|
||||
} else {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"SCNetworkReachabilitySetDispatchQueue() failed: %s", SCErrorString(SCError()));
|
||||
#endif
|
||||
|
||||
// UH OH - FAILURE - stop any callbacks!
|
||||
SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL);
|
||||
}
|
||||
} else {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"SCNetworkReachabilitySetCallback() failed: %s", SCErrorString(SCError()));
|
||||
#endif
|
||||
}
|
||||
|
||||
// if we get here we fail at the internet
|
||||
self.reachabilityObject = nil;
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)stopNotifier {
|
||||
// First stop, any callbacks!
|
||||
SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL);
|
||||
|
||||
// Unregister target from the GCD serial dispatch queue.
|
||||
SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, NULL);
|
||||
|
||||
self.reachabilityObject = nil;
|
||||
}
|
||||
|
||||
#pragma mark - reachability tests
|
||||
|
||||
// This is for the case where you flick the airplane mode;
|
||||
// you end up getting something like this:
|
||||
// Reachability: WR ct-----
|
||||
// Reachability: -- -------
|
||||
// Reachability: WR ct-----
|
||||
// Reachability: -- -------
|
||||
// We treat this as 4 UNREACHABLE triggers - really apple should do better than this
|
||||
|
||||
#define testcase \
|
||||
(kSCNetworkReachabilityFlagsConnectionRequired | kSCNetworkReachabilityFlagsTransientConnection)
|
||||
|
||||
- (BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags {
|
||||
BOOL connectionUP = YES;
|
||||
|
||||
if (!(flags & kSCNetworkReachabilityFlagsReachable)) connectionUP = NO;
|
||||
|
||||
if ((flags & testcase) == testcase) connectionUP = NO;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
if (flags & kSCNetworkReachabilityFlagsIsWWAN) {
|
||||
// We're on 3G.
|
||||
if (!self.reachableOnWWAN) {
|
||||
// We don't want to connect when on 3G.
|
||||
connectionUP = NO;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return connectionUP;
|
||||
}
|
||||
|
||||
- (BOOL)isReachable {
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if (!SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) return NO;
|
||||
|
||||
return [self isReachableWithFlags:flags];
|
||||
}
|
||||
|
||||
- (BOOL)isReachableViaWWAN {
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
SCNetworkReachabilityFlags flags = 0;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) {
|
||||
// Check we're REACHABLE
|
||||
if (flags & kSCNetworkReachabilityFlagsReachable) {
|
||||
// Now, check we're on WWAN
|
||||
if (flags & kSCNetworkReachabilityFlagsIsWWAN) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isReachableViaWiFi {
|
||||
SCNetworkReachabilityFlags flags = 0;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) {
|
||||
// Check we're reachable
|
||||
if ((flags & kSCNetworkReachabilityFlagsReachable)) {
|
||||
#if TARGET_OS_IPHONE
|
||||
// Check we're NOT on WWAN
|
||||
if ((flags & kSCNetworkReachabilityFlagsIsWWAN)) {
|
||||
return NO;
|
||||
}
|
||||
#endif
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// WWAN may be available, but not active until a connection has been established.
|
||||
// WiFi may require a connection for VPN on Demand.
|
||||
- (BOOL)isConnectionRequired {
|
||||
return [self connectionRequired];
|
||||
}
|
||||
|
||||
- (BOOL)connectionRequired {
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) {
|
||||
return (flags & kSCNetworkReachabilityFlagsConnectionRequired);
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Dynamic, on demand connection?
|
||||
- (BOOL)isConnectionOnDemand {
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) {
|
||||
return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) &&
|
||||
(flags & (kSCNetworkReachabilityFlagsConnectionOnTraffic |
|
||||
kSCNetworkReachabilityFlagsConnectionOnDemand)));
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Is user intervention required?
|
||||
- (BOOL)isInterventionRequired {
|
||||
SCNetworkReachabilityFlags flags;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) {
|
||||
return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) &&
|
||||
(flags & kSCNetworkReachabilityFlagsInterventionRequired));
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - reachability status stuff
|
||||
|
||||
- (NetworkStatus)currentReachabilityStatus {
|
||||
if ([self isReachable]) {
|
||||
if ([self isReachableViaWiFi]) return ReachableViaWiFi;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
return ReachableViaWWAN;
|
||||
#endif
|
||||
}
|
||||
|
||||
return NotReachable;
|
||||
}
|
||||
|
||||
- (SCNetworkReachabilityFlags)reachabilityFlags {
|
||||
SCNetworkReachabilityFlags flags = 0;
|
||||
|
||||
if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) {
|
||||
return flags;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (NSString *)currentReachabilityString {
|
||||
NetworkStatus temp = [self currentReachabilityStatus];
|
||||
|
||||
if (temp == ReachableViaWWAN) {
|
||||
// Updated for the fact that we have CDMA phones now!
|
||||
return NSLocalizedString(@"Cellular", @"");
|
||||
}
|
||||
if (temp == ReachableViaWiFi) {
|
||||
return NSLocalizedString(@"WiFi", @"");
|
||||
}
|
||||
|
||||
return NSLocalizedString(@"No Connection", @"");
|
||||
}
|
||||
|
||||
- (NSString *)currentReachabilityFlags {
|
||||
return reachabilityFlags([self reachabilityFlags]);
|
||||
}
|
||||
|
||||
#pragma mark - Callback function calls this method
|
||||
|
||||
- (void)reachabilityChanged:(SCNetworkReachabilityFlags)flags {
|
||||
if ([self isReachableWithFlags:flags]) {
|
||||
if (self.reachableBlock) {
|
||||
self.reachableBlock(self);
|
||||
}
|
||||
} else {
|
||||
if (self.unreachableBlock) {
|
||||
self.unreachableBlock(self);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.reachabilityBlock) {
|
||||
self.reachabilityBlock(self, flags);
|
||||
}
|
||||
|
||||
// this makes sure the change notification happens on the MAIN THREAD
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kReachabilityChangedNotification
|
||||
object:self];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Debug Description
|
||||
|
||||
- (NSString *)description {
|
||||
NSString *description =
|
||||
[NSString stringWithFormat:@"<%@: %p (%@)>", NSStringFromClass([self class]), self,
|
||||
[self currentReachabilityFlags]];
|
||||
return description;
|
||||
}
|
||||
|
||||
@end
|
||||