Browse Source

Borderless fullscreen support. Changed `init` to take an Init_Options struct that has a Window_Mode enum that says if you want Windowed, Windowed_Resizable or Borderless_Fullscreen

Karl Zylinski 1 month ago
parent
commit
233ebf7dd2
5 changed files with 168 additions and 90 deletions
  1. 18 11
      karl2d.doc.odin
  2. 20 13
      karl2d.odin
  3. 11 3
      window_interface.odin
  4. 20 21
      window_js.odin
  5. 99 42
      window_win32.odin

+ 18 - 11
karl2d.doc.odin

@@ -12,9 +12,14 @@ package karl2d
 //
 // `screen_width` and `screen_height` refer to the the resolution of the drawable area of the
 // window. The window might be slightly larger due borders and headers.
-init :: proc(screen_width: int, screen_height: int, window_title: string,
-            window_creation_flags := Window_Flags {},
-            allocator := context.allocator, loc := #caller_location) -> ^State
+init :: proc(
+	screen_width: int,
+	screen_height: int,
+	window_title: string,
+	options := Init_Options {},
+	allocator := context.allocator,
+	loc := #caller_location
+) -> ^State
 
 // Returns true the user has pressed the close button on the window, or used a key stroke such as
 // ALT+F4 on Windows. The application can decide if it wants to shut down or if it wants to show
@@ -74,7 +79,7 @@ get_screen_height :: proc() -> int
 
 // Moves the window.
 //
-// WebGL note: This moves the canvas within the window, which may not be what you want.
+// This does nothing for web builds.
 set_window_position :: proc(x: int, y: int)
 
 // Resize the window to a new size. If the window has the flag Resizable set, then the backbuffer
@@ -85,8 +90,8 @@ set_window_size :: proc(width: int, height: int)
 // 1 means 100% scale, 1.5 means 150% etc.
 get_window_scale :: proc() -> f32
 
-// These are the same kind of flags that you can send to `init`.
-set_window_flags :: proc(flags: Window_Flags)
+// Use to change between windowed mode, resizable windowed mode and fullscreen
+set_window_mode :: proc(window_mode: Window_Mode)
 
 // Flushes the current batch. This sends off everything to the GPU that has been queued in the
 // current batch. Normally, you do not need to do this manually. It is done automatically when these
@@ -575,13 +580,15 @@ Camera :: struct {
 	zoom: f32,
 }
 
-Window_Flag :: enum {
-	// Make the window possible to resize. This will make the backbuffer automatically resize as
-	// well.
-	Resizable,
+Window_Mode :: enum {
+	Windowed,
+	Windowed_Resizable,
+	Windowed_Borderless_Fullscreen,
 }
 
-Window_Flags :: bit_set[Window_Flag]
+Init_Options :: struct {
+	window_mode: Window_Mode,
+}
 
 Shader_Handle :: distinct Handle
 

+ 20 - 13
karl2d.odin

@@ -33,9 +33,14 @@ import hm "handle_map"
 //
 // `screen_width` and `screen_height` refer to the the resolution of the drawable area of the
 // window. The window might be slightly larger due borders and headers.
-init :: proc(screen_width: int, screen_height: int, window_title: string,
-            window_creation_flags := Window_Flags {},
-            allocator := context.allocator, loc := #caller_location) -> ^State {
+init :: proc(
+	screen_width: int,
+	screen_height: int,
+	window_title: string,
+	options := Init_Options {},
+	allocator := context.allocator,
+	loc := #caller_location
+) -> ^State {
 	assert(s == nil, "Don't call 'init' twice.")
 	context.allocator = allocator
 
@@ -57,7 +62,7 @@ init :: proc(screen_width: int, screen_height: int, window_title: string,
 	s.window_state, window_state_alloc_error = mem.alloc(win.state_size(), allocator = allocator)
 	log.assertf(window_state_alloc_error == nil, "Failed allocating memory for window state: %v", window_state_alloc_error)
 
-	win.init(s.window_state, screen_width, screen_height, window_title, window_creation_flags, allocator)
+	win.init(s.window_state, screen_width, screen_height, window_title, options, allocator)
 
 	// This is a OS-independent handle that we can pass to any rendering backend.
 	s.window = win.window_handle()
@@ -294,7 +299,7 @@ get_screen_height :: proc() -> int  {
 
 // Moves the window.
 //
-// WebGL note: This moves the canvas within the window, which may not be what you want.
+// This does nothing for web builds.
 set_window_position :: proc(x: int, y: int) {
 	win.set_position(x, y)
 }
@@ -313,9 +318,9 @@ get_window_scale :: proc() -> f32 {
 	return win.get_window_scale()
 }
 
-// These are the same kind of flags that you can send to `init`.
-set_window_flags :: proc(flags: Window_Flags) {
-	win.set_flags(flags)
+// Use to change between windowed mode, resizable windowed mode and fullscreen
+set_window_mode :: proc(window_mode: Window_Mode) {
+	win.set_window_mode(window_mode)
 }
 
 // Flushes the current batch. This sends off everything to the GPU that has been queued in the
@@ -1619,13 +1624,15 @@ Camera :: struct {
 	zoom: f32,
 }
 
-Window_Flag :: enum {
-	// Make the window possible to resize. This will make the backbuffer automatically resize as
-	// well.
-	Resizable,
+Window_Mode :: enum {
+	Windowed,
+	Windowed_Resizable,
+	Windowed_Borderless_Fullscreen,
 }
 
-Window_Flags :: bit_set[Window_Flag]
+Init_Options :: struct {
+	window_mode: Window_Mode,
+}
 
 Shader_Handle :: distinct Handle
 

+ 11 - 3
window_interface.odin

@@ -4,8 +4,16 @@ import "base:runtime"
 
 Window_Interface :: struct #all_or_none {
 	state_size: proc() -> int,
-	init: proc(window_state: rawptr, window_width: int, window_height: int, window_title: string, 
-	           flags: Window_Flags, allocator: runtime.Allocator),
+
+	init: proc(
+		window_state: rawptr,
+		window_width: int,
+		window_height: int,
+		window_title: string,
+		init_options: Init_Options,
+		allocator: runtime.Allocator,
+	),
+
 	shutdown: proc(),
 	window_handle: proc() -> Window_Handle,
 	process_events: proc(),
@@ -16,7 +24,7 @@ Window_Interface :: struct #all_or_none {
 	get_width: proc() -> int,
 	get_height: proc() -> int,
 	get_window_scale: proc() -> f32,
-	set_flags: proc(flags: Window_Flags),
+	set_window_mode: proc(window_mode: Window_Mode),
 
 	is_gamepad_active: proc(gamepad: int) -> bool,
 	get_gamepad_axis: proc(gamepad: int, axis: Gamepad_Axis) -> f32,

+ 20 - 21
window_js.odin

@@ -17,7 +17,7 @@ WINDOW_INTERFACE_JS :: Window_Interface {
 	set_position = js_set_position,
 	set_size = js_set_size,
 	get_window_scale = js_get_window_scale,
-	set_flags = js_set_flags,
+	set_window_mode = js_set_window_mode,
 	is_gamepad_active = js_is_gamepad_active,
 	get_gamepad_axis = js_get_gamepad_axis,
 	set_gamepad_vibration = js_set_gamepad_vibration,
@@ -28,7 +28,6 @@ WINDOW_INTERFACE_JS :: Window_Interface {
 import "core:sys/wasm/js"
 import "base:runtime"
 import "core:log"
-import "core:fmt"
 
 js_state_size :: proc() -> int {
 	return size_of(JS_State)
@@ -39,24 +38,28 @@ js_init :: proc(
 	window_width: int,
 	window_height: int,
 	window_title: string,
-	flags: Window_Flags,
+	init_options: Init_Options,
 	allocator: runtime.Allocator,
 ) {
 	s = (^JS_State)(window_state)
 	s.allocator = allocator
 	s.canvas_id = "webgl-canvas"
-	s.flags = flags
 
 	js.set_document_title(window_title)
 
 	// The browser window probably has some other size than what was sent in.
-	if .Resizable in flags {
+	switch init_options.window_mode {
+	case .Windowed:
+		js_set_size(window_width, window_height)
+	case .Windowed_Resizable:
 		add_window_event_listener(.Resize, js_event_window_resize)
 		update_canvas_size(s.canvas_id)
-	} else {
-		js_set_size(window_width, window_height)
+	case .Windowed_Borderless_Fullscreen:
+		log.error("Windowed_Borderless_Fullscreen not implemented on web, but you can make it happen by using Window_Mode.Windowed_Resizable and putting the game in a fullscreen iframe.")
 	}
 
+	s.window_mode = init_options.window_mode
+
 	add_canvas_event_listener(.Mouse_Move, js_event_mouse_move)
 	add_canvas_event_listener(.Mouse_Down, js_event_mouse_down)
 	add_canvas_event_listener(.Mouse_Up, js_event_mouse_up)
@@ -260,9 +263,7 @@ js_clear_events :: proc() {
 }
 
 js_set_position :: proc(x: int, y: int) {
-	buf: [256]u8
-	js.set_element_style(s.canvas_id, "margin-top", fmt.bprintf(buf[:], "%vpx", x))
-	js.set_element_style(s.canvas_id, "margin-left", fmt.bprintf(buf[:], "%vpx", y))
+	log.warn("set_window_position not implemented on web")
 }
 
 js_set_size :: proc(w, h: int) {
@@ -276,18 +277,16 @@ js_get_window_scale :: proc() -> f32 {
 	return f32(js.device_pixel_ratio())
 }
 
-js_set_flags :: proc(flags: Window_Flags) {
-	if .Resizable in (flags ~ s.flags) {
-		if .Resizable in flags {
-			add_window_event_listener(.Resize, js_event_window_resize)
-			update_canvas_size(s.canvas_id)
-		} else {
-			remove_window_event_listener(.Resize, js_event_window_resize)
-			js_set_size(s.width, s.height)
-		}
+js_set_window_mode :: proc(window_mode: Window_Mode) {
+	if window_mode == .Windowed_Resizable && s.window_mode == .Windowed {
+		add_window_event_listener(.Resize, js_event_window_resize)
+		update_canvas_size(s.canvas_id)
+	} else if window_mode == .Windowed && s.window_mode == .Windowed_Resizable {
+		remove_window_event_listener(.Resize, js_event_window_resize)
+		js_set_size(s.width, s.height)
 	}
 
-	s.flags = flags
+	s.window_mode = window_mode
 }
 
 js_is_gamepad_active :: proc(gamepad: int) -> bool {
@@ -335,7 +334,7 @@ JS_State :: struct {
 	height: int,
 	events: [dynamic]Window_Event,
 	gamepad_state: [MAX_GAMEPADS]js.Gamepad_State,
-	flags: Window_Flags,
+	window_mode: Window_Mode,
 }
 
 s: ^JS_State

+ 99 - 42
window_win32.odin

@@ -17,7 +17,8 @@ WINDOW_INTERFACE_WIN32 :: Window_Interface {
 	set_position = win32_set_position,
 	set_size = win32_set_size,
 	get_window_scale = win32_get_window_scale,
-	set_flags = win32_set_flags,
+	set_window_mode = win32_set_window_mode,
+
 	is_gamepad_active = win32_is_gamepad_active,
 	get_gamepad_axis = win32_get_gamepad_axis,
 	set_gamepad_vibration = win32_set_gamepad_vibration,
@@ -32,14 +33,20 @@ win32_state_size :: proc() -> int {
 	return size_of(Win32_State)
 }
 
-win32_init :: proc(window_state: rawptr, screen_width: int, screen_height: int, window_title: string,
-	               flags: Window_Flags, allocator: runtime.Allocator) {
+win32_init :: proc(
+	window_state: rawptr,
+	screen_width: int,
+	screen_height: int,
+	window_title: string,
+	init_options: Init_Options,
+	allocator: runtime.Allocator,
+) {
 	assert(window_state != nil)
 	s = (^Win32_State)(window_state)
 	s.allocator = allocator
 	s.events = make([dynamic]Window_Event, allocator)
-	s.width = screen_width
-	s.height = screen_height
+	s.wanted_width = screen_width
+	s.wanted_height = screen_height
 	s.custom_context = context
 	
 	win32.SetProcessDpiAwarenessContext(win32.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
@@ -57,29 +64,21 @@ win32_init :: proc(window_state: rawptr, screen_width: int, screen_height: int,
 
 	win32.RegisterClassW(&cls)
 
-	r: win32.RECT
-	r.right = i32(screen_width)
-	r.bottom = i32(screen_height)
-
-	s.flags = flags
-
-	style := style_from_flags(flags)
-
-	win32.AdjustWindowRect(&r, style, false)
-
+	// We create a window with default position and size. We set the correct size in
+	// `win32_set_window_mode`.
 	hwnd := win32.CreateWindowW(CLASS_NAME,
 		win32.utf8_to_wstring(window_title),
-		style,
+		win32.WS_VISIBLE,
+		win32.CW_USEDEFAULT, win32.CW_USEDEFAULT,
 		win32.CW_USEDEFAULT, win32.CW_USEDEFAULT,
-		r.right - r.left, r.bottom - r.top,
 		nil, nil, instance, nil,
 	)
-
-	win32.XInputEnable(true)
-
 	assert(hwnd != nil, "Failed creating window")
-
 	s.hwnd = hwnd
+	
+	win32_set_window_mode(init_options.window_mode)
+	
+	win32.XInputEnable(true)
 }
 
 win32_shutdown :: proc() {
@@ -161,11 +160,11 @@ win32_get_events :: proc() -> []Window_Event {
 }
 
 win32_get_width :: proc() -> int {
-	return s.width
+	return s.current_width
 }
 
 win32_get_height :: proc() -> int {
-	return s.height
+	return s.current_height
 }
 
 win32_clear_events :: proc() {
@@ -202,12 +201,6 @@ win32_get_window_scale :: proc() -> f32 {
 	return f32(win32.GetDpiForWindow(s.hwnd))/96.0
 }
 
-win32_set_flags :: proc(flags: Window_Flags) {
-	s.flags = flags
-	style := style_from_flags(flags)
-	win32.SetWindowLongW(s.hwnd, win32.GWL_STYLE, i32(style))
-}
-
 win32_is_gamepad_active :: proc(gamepad: int) -> bool {
 	if gamepad < 0 || gamepad >= MAX_GAMEPADS {
 		return false
@@ -265,22 +258,77 @@ Win32_State :: struct {
 	allocator: runtime.Allocator,
 	custom_context: runtime.Context,
 	hwnd: win32.HWND,
-	flags: Window_Flags,
-	width: int,
-	height: int,
+	window_mode: Window_Mode,
+	current_width: int,
+	current_height: int,
+
+	wanted_width: int,
+	wanted_height: int,
+
+	// old (x,y) pos, good when returning to windowed mode
+	windowed_pos_x: int,
+	windowed_pos_y: int,
+
 	events: [dynamic]Window_Event,
 }
 
-style_from_flags :: proc(flags: Window_Flags) -> win32.DWORD {
-	style := win32.WS_OVERLAPPED | win32.WS_CAPTION | win32.WS_SYSMENU |
-	         win32.WS_MINIMIZEBOX | win32.WS_VISIBLE |
-	         win32.CS_OWNDC
-
-	if .Resizable in flags {
-		style |= win32.WS_THICKFRAME & win32.WS_MAXIMIZEBOX
+win32_set_window_mode :: proc(window_mode: Window_Mode) {
+	s.window_mode = window_mode
+
+	style: win32.DWORD
+	switch window_mode {
+	case .Windowed:
+		style = win32.WS_OVERLAPPED |
+	            win32.WS_CAPTION |
+	            win32.WS_SYSMENU |
+	            win32.WS_MINIMIZEBOX |
+	            win32.WS_VISIBLE
+
+	case .Windowed_Resizable:
+		style = win32.WS_OVERLAPPED |
+	            win32.WS_CAPTION |
+	            win32.WS_SYSMENU |
+	            win32.WS_MINIMIZEBOX |
+	            win32.WS_VISIBLE |
+	            win32.WS_THICKFRAME |
+	            win32.WS_MAXIMIZEBOX
+
+	case .Windowed_Borderless_Fullscreen:
+		style = win32.WS_VISIBLE
 	}
 
-	return style
+	win32.SetWindowLongW(s.hwnd, win32.GWL_STYLE, i32(style))
+
+	#partial switch window_mode {
+	case .Windowed, .Windowed_Resizable:
+		r: win32.RECT
+		r.left = i32(s.windowed_pos_x)
+		r.top = i32(s.windowed_pos_y)
+		r.right = r.left + i32(s.wanted_width)
+		r.bottom = r.top + i32(s.wanted_height)
+		win32.AdjustWindowRect(&r, style, false)
+
+		win32.SetWindowPos(
+			s.hwnd,
+			{},
+			i32(r.left),
+			i32(r.top),
+			i32(r.right - r.left),
+			i32(r.bottom - r.top),
+			win32.SWP_NOACTIVATE | win32.SWP_NOZORDER,
+		)
+
+	case .Windowed_Borderless_Fullscreen:
+		mi := win32.MONITORINFO { cbSize = size_of (win32.MONITORINFO)}
+		mon := win32.MonitorFromWindow(s.hwnd, .MONITOR_DEFAULTTONEAREST)
+		if (win32.GetMonitorInfoW(mon, &mi)) {
+			win32.SetWindowPos(s.hwnd, win32.HWND_TOP,
+			mi.rcMonitor.left, mi.rcMonitor.top,
+			mi.rcMonitor.right - mi.rcMonitor.left,
+			mi.rcMonitor.bottom - mi.rcMonitor.top,
+			win32.SWP_NOOWNERZORDER | win32.SWP_FRAMECHANGED)
+		}
+	}
 }
 
 s: ^Win32_State
@@ -356,12 +404,21 @@ window_proc :: proc "stdcall" (hwnd: win32.HWND, msg: win32.UINT, wparam: win32.
 			button = .Right,
 		})
 
+	case win32.WM_MOVE:
+		if s.window_mode == .Windowed || s.window_mode == .Windowed_Resizable {
+			x := win32.LOWORD(lparam)
+			y := win32.HIWORD(lparam)
+
+			s.windowed_pos_x = int(x)
+			s.windowed_pos_y = int(y)
+		}
+
 	case win32.WM_SIZE:
 		width := win32.LOWORD(lparam)
 		height := win32.HIWORD(lparam)
 
-		s.width = int(width)
-		s.height = int(height)
+		s.current_width = int(width)
+		s.current_height = int(height)
 
 		append(&s.events, Window_Event_Resize {
 			width = int(width),