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

This commit is contained in:
Ryan Ward
2026-05-07 11:29:34 -05:00
parent f4e2475c60
commit a41cbd8cc7
2433 changed files with 14208 additions and 9409 deletions
@@ -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
}
}