Skip to content

Commit 8325f99

Browse files
committed
feat: add optional fullscreen support for desktop platforms
Add fullscreen parameter to initWindow() so apps can launch in fullscreen mode. Implement toggleFullscreen() on Linux (EWMH) and Windows (borderless fullscreen), which were previously empty stubs. macOS already worked and now supports initial fullscreen at launch.
1 parent afb82f4 commit 8325f99

9 files changed

Lines changed: 141 additions & 12 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,25 @@ interface Sound { handle: number }
9393
interface Model { handle: number }
9494
```
9595

96+
## Fullscreen
97+
98+
Launch your game in fullscreen by passing `true` as the fourth argument to `initWindow`:
99+
100+
```typescript
101+
initWindow(800, 450, "My Game", true); // launches fullscreen
102+
initWindow(800, 450, "My Game"); // windowed (default)
103+
```
104+
105+
Toggle fullscreen at runtime:
106+
107+
```typescript
108+
if (isKeyPressed(Key.F11)) {
109+
toggleFullscreen();
110+
}
111+
```
112+
113+
Fullscreen is supported on macOS (native AppKit fullscreen), Windows (borderless fullscreen), and Linux (EWMH/X11). The width and height you pass are used as the windowed dimensions when exiting fullscreen.
114+
96115
## Skeletal Animation
97116

98117
Bloom supports GPU-accelerated skeletal animation via glTF/GLB models. The pipeline uses 4-bone linear blend skinning with a 128-joint uniform buffer, running entirely on the GPU.

native/android/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
8686
// ============================================================
8787

8888
#[no_mangle]
89-
pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8) {
89+
pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8, _fullscreen: f64) {
9090
let _title = str_from_header(title_ptr);
9191

9292
unsafe {

native/ios/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
509509
// ============================================================
510510

511511
#[no_mangle]
512-
pub extern "C" fn bloom_init_window(_width: f64, _height: f64, title_ptr: *const u8) {
512+
pub extern "C" fn bloom_init_window(_width: f64, _height: f64, title_ptr: *const u8, _fullscreen: f64) {
513513
let _title = str_from_header(title_ptr);
514514

515515
// Register ObjC classes for the scene delegate (window/view creation)

native/linux/src/lib.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,51 @@ mod x11_impl {
6565

6666
static mut DISPLAY: *mut x11::xlib::Display = std::ptr::null_mut();
6767
static mut X11_WINDOW: x11::xlib::Window = 0;
68+
static mut IS_FULLSCREEN: bool = false;
69+
70+
pub fn set_fullscreen(fullscreen: bool) {
71+
unsafe {
72+
if DISPLAY.is_null() || X11_WINDOW == 0 { return; }
73+
74+
let wm_state = x11::xlib::XInternAtom(
75+
DISPLAY,
76+
b"_NET_WM_STATE\0".as_ptr() as *const _,
77+
0,
78+
);
79+
let wm_fullscreen = x11::xlib::XInternAtom(
80+
DISPLAY,
81+
b"_NET_WM_STATE_FULLSCREEN\0".as_ptr() as *const _,
82+
0,
83+
);
84+
85+
let action = if fullscreen { 1 } else { 0 }; // _NET_WM_STATE_ADD / _REMOVE
86+
87+
let mut event: x11::xlib::XClientMessageEvent = std::mem::zeroed();
88+
event.type_ = x11::xlib::ClientMessage;
89+
event.window = X11_WINDOW;
90+
event.message_type = wm_state;
91+
event.format = 32;
92+
event.data.set_long(0, action);
93+
event.data.set_long(1, wm_fullscreen as i64);
94+
event.data.set_long(2, 0);
95+
event.data.set_long(3, 1); // source: normal application
96+
97+
let root = x11::xlib::XDefaultRootWindow(DISPLAY);
98+
x11::xlib::XSendEvent(
99+
DISPLAY,
100+
root,
101+
0,
102+
x11::xlib::SubstructureRedirectMask | x11::xlib::SubstructureNotifyMask,
103+
&mut event as *mut x11::xlib::XClientMessageEvent as *mut x11::xlib::XEvent,
104+
);
105+
x11::xlib::XFlush(DISPLAY);
106+
IS_FULLSCREEN = fullscreen;
107+
}
108+
}
109+
110+
pub fn toggle_fullscreen() {
111+
unsafe { set_fullscreen(!IS_FULLSCREEN); }
112+
}
68113

69114
pub fn create_window(width: f64, height: f64, title: &str) {
70115
unsafe {
@@ -171,7 +216,7 @@ mod x11_impl {
171216
}
172217

173218
#[no_mangle]
174-
pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8) {
219+
pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8, fullscreen: f64) {
175220
let title = str_from_header(title_ptr);
176221

177222
#[cfg(target_os = "linux")]
@@ -224,6 +269,10 @@ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u
224269

225270
let renderer = Renderer::new(device, queue, surface, surface_config);
226271
unsafe { let _ = ENGINE.set(EngineState::new(renderer)); }
272+
273+
if fullscreen != 0.0 {
274+
x11_impl::set_fullscreen(true);
275+
}
227276
}
228277

229278
#[cfg(not(target_os = "linux"))]
@@ -857,7 +906,10 @@ pub extern "C" fn bloom_set_directional_light(dx: f64, dy: f64, dz: f64, r: f64,
857906
// --- Utility FFI ---
858907

859908
#[no_mangle]
860-
pub extern "C" fn bloom_toggle_fullscreen() {}
909+
pub extern "C" fn bloom_toggle_fullscreen() {
910+
#[cfg(target_os = "linux")]
911+
x11_impl::toggle_fullscreen();
912+
}
861913
#[no_mangle]
862914
pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) { let _ = str_from_header(title_ptr); }
863915
#[no_mangle]

native/macos/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ unsafe extern "C" fn audio_render_callback(
235235
// ============================================================
236236

237237
#[no_mangle]
238-
pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8) {
238+
pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8, fullscreen: f64) {
239239
let title = str_from_header(title_ptr);
240240
let mtm = MainThreadMarker::from(unsafe { MainThreadMarker::new_unchecked() });
241241

@@ -331,6 +331,10 @@ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u
331331

332332
// Register Bloom's GPU screenshot capture with perry-geisterhand (if linked)
333333
bloom_register_geisterhand_screenshot();
334+
335+
if fullscreen != 0.0 {
336+
bloom_toggle_fullscreen();
337+
}
334338
}
335339

336340
#[no_mangle]

native/tvos/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1267,7 +1267,7 @@ fn setup_game_controllers() {
12671267
// ============================================================
12681268

12691269
#[no_mangle]
1270-
pub extern "C" fn bloom_init_window(_width: f64, _height: f64, title_ptr: *const u8) {
1270+
pub extern "C" fn bloom_init_window(_width: f64, _height: f64, title_ptr: *const u8, _fullscreen: f64) {
12711271
let _title = str_from_header(title_ptr);
12721272

12731273
// Register ObjC classes for the scene delegate (window/view creation)

native/windows/src/lib.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,53 @@ mod win32 {
5656
use raw_window_handle::{RawWindowHandle, Win32WindowHandle, RawDisplayHandle, WindowsDisplayHandle};
5757

5858
static mut HWND_GLOBAL: Option<HWND> = None;
59+
static mut IS_FULLSCREEN: bool = false;
60+
static mut WINDOWED_STYLE: u32 = 0;
61+
static mut WINDOWED_RECT: RECT = RECT { left: 0, top: 0, right: 0, bottom: 0 };
62+
63+
pub fn set_fullscreen(fullscreen: bool) {
64+
unsafe {
65+
let Some(hwnd) = HWND_GLOBAL else { return };
66+
67+
if fullscreen && !IS_FULLSCREEN {
68+
// Save current style and window rect for restore
69+
WINDOWED_STYLE = GetWindowLongW(hwnd, GWL_STYLE) as u32;
70+
let _ = GetWindowRect(hwnd, &mut WINDOWED_RECT);
71+
72+
// Get monitor dimensions
73+
let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
74+
let mut mi: MONITORINFO = std::mem::zeroed();
75+
mi.cbSize = std::mem::size_of::<MONITORINFO>() as u32;
76+
let _ = GetMonitorInfoW(monitor, &mut mi);
77+
78+
// Set borderless fullscreen
79+
SetWindowLongW(hwnd, GWL_STYLE, (WS_POPUP | WS_VISIBLE).0 as i32);
80+
let _ = SetWindowPos(
81+
hwnd, HWND_TOP,
82+
mi.rcMonitor.left, mi.rcMonitor.top,
83+
mi.rcMonitor.right - mi.rcMonitor.left,
84+
mi.rcMonitor.bottom - mi.rcMonitor.top,
85+
SWP_FRAMECHANGED | SWP_NOOWNERZORDER,
86+
);
87+
IS_FULLSCREEN = true;
88+
} else if !fullscreen && IS_FULLSCREEN {
89+
// Restore windowed mode
90+
SetWindowLongW(hwnd, GWL_STYLE, WINDOWED_STYLE as i32);
91+
let _ = SetWindowPos(
92+
hwnd, None,
93+
WINDOWED_RECT.left, WINDOWED_RECT.top,
94+
WINDOWED_RECT.right - WINDOWED_RECT.left,
95+
WINDOWED_RECT.bottom - WINDOWED_RECT.top,
96+
SWP_FRAMECHANGED | SWP_NOOWNERZORDER | SWP_NOZORDER,
97+
);
98+
IS_FULLSCREEN = false;
99+
}
100+
}
101+
}
102+
103+
pub fn toggle_fullscreen() {
104+
unsafe { set_fullscreen(!IS_FULLSCREEN); }
105+
}
59106

60107
unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
61108
match msg {
@@ -179,7 +226,7 @@ mod win32 {
179226
}
180227

181228
#[no_mangle]
182-
pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8) {
229+
pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8, fullscreen: f64) {
183230
let title = str_from_header(title_ptr);
184231

185232
#[cfg(windows)]
@@ -231,6 +278,10 @@ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u
231278

232279
let renderer = Renderer::new(device, queue, surface, surface_config);
233280
unsafe { let _ = ENGINE.set(EngineState::new(renderer)); }
281+
282+
if fullscreen != 0.0 {
283+
win32::set_fullscreen(true);
284+
}
234285
}
235286

236287
#[cfg(not(windows))]
@@ -959,7 +1010,10 @@ pub extern "C" fn bloom_set_directional_light(dx: f64, dy: f64, dz: f64, r: f64,
9591010
// --- Utility FFI ---
9601011

9611012
#[no_mangle]
962-
pub extern "C" fn bloom_toggle_fullscreen() {}
1013+
pub extern "C" fn bloom_toggle_fullscreen() {
1014+
#[cfg(windows)]
1015+
win32::toggle_fullscreen();
1016+
}
9631017
#[no_mangle]
9641018
pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) { let _ = str_from_header(title_ptr); }
9651019
#[no_mangle]

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"nativeLibrary": {
2222
"module": "bloom",
2323
"functions": [
24-
{ "name": "bloom_init_window", "params": ["f64", "f64", "i64"], "returns": "void" },
24+
{ "name": "bloom_init_window", "params": ["f64", "f64", "i64", "f64"], "returns": "void" },
2525
{ "name": "bloom_close_window", "params": [], "returns": "void" },
2626
{ "name": "bloom_window_should_close", "params": [], "returns": "f64" },
2727
{ "name": "bloom_begin_drawing", "params": [], "returns": "void" },

src/core/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export { ColorConstants as Color } from './colors';
66
export { Key, MouseButton } from './keys';
77

88
// FFI declarations
9-
declare function bloom_init_window(width: number, height: number, title: number): void;
9+
declare function bloom_init_window(width: number, height: number, title: number, fullscreen: number): void;
1010
declare function bloom_close_window(): void;
1111
declare function bloom_window_should_close(): number;
1212
declare function bloom_begin_drawing(): void;
@@ -69,8 +69,8 @@ declare function bloom_read_file(path: number): number;
6969

7070
// Window management
7171

72-
export function initWindow(width: number, height: number, title: string): void {
73-
bloom_init_window(width, height, title as any);
72+
export function initWindow(width: number, height: number, title: string, fullscreen: boolean = false): void {
73+
bloom_init_window(width, height, title as any, fullscreen ? 1.0 : 0.0);
7474
}
7575

7676
export function closeWindow(): void {

0 commit comments

Comments
 (0)