diff --git a/src/editor/editor.go b/src/editor/editor.go index 53267e822..bdbcb1a33 100644 --- a/src/editor/editor.go +++ b/src/editor/editor.go @@ -62,6 +62,7 @@ import ( "kaijuengine.com/matrix" "kaijuengine.com/platform/hid" "kaijuengine.com/platform/profiler/tracing" + "kaijuengine.com/platform/windowing" "kaijuengine.com/rendering" // Built-in workspace packages register themselves with @@ -109,6 +110,8 @@ type Editor struct { contentPreviewer content_previews.ContentPreviewer updateId engine.UpdateId blurred bool + cursorUI ui.Manager + cursor *ui.Cursor } type globalUI struct { @@ -160,6 +163,28 @@ func (ed *Editor) IsInputFocused() bool { return ed.currentWorkspace.IsFocusedOnInput() } +func (ed *Editor) ensureCursor() { + if ed.cursor != nil { + return + } + + ed.cursorUI.Init(ed.host) + + ed.cursor = ed.cursorUI.Add().ToCursor() + ed.cursor.Init(ui.CursorThemeByName(ed.settings.CursorTheme)) +} + +func cursorModeFromSetting(mode string) windowing.CursorMode { + switch mode { + case "virtual": + return windowing.CursorModeVirtual + case "auto": + return windowing.CursorModeAuto + default: + return windowing.CursorModeNative + } +} + func (ed *Editor) earlyLoadUI() { defer tracing.NewRegion("Editor.earlyLoadUI").End() ed.globalInterfaces.menuBar.Initialize(ed.host, ed) @@ -172,6 +197,14 @@ func (ed *Editor) UpdateSettings() { ed.settings.UIScrollSpeed = 1 } ui.UIScrollSpeed = ed.settings.UIScrollSpeed + + ed.ensureCursor() + ed.cursor.SetTheme(ui.CursorThemeByName(ed.settings.CursorTheme)) + + ed.host.Window.ResetCursor() + ed.host.Window.SetCursorMode(cursorModeFromSetting(ed.settings.CursorMode)) + ed.cursor.SyncWithWindow() + if err := ed.settings.Save(); err != nil { slog.Error("failed to save the editor settings", "error", err) return diff --git a/src/editor/editor_embedded_content/editor_content/editor/ui/workspace/settings_workspace.go.html b/src/editor/editor_embedded_content/editor_content/editor/ui/workspace/settings_workspace.go.html index f3fdc1a2c..3808f043e 100644 --- a/src/editor/editor_embedded_content/editor_content/editor/ui/workspace/settings_workspace.go.html +++ b/src/editor/editor_embedded_content/editor_content/editor/ui/workspace/settings_workspace.go.html @@ -188,6 +188,28 @@

{{.Name}}

.workspaceRequired { color: #cc6; margin-right: 1em; } .workspaceToggle { margin-right: 0.5em; } .workspaceMoveBtn { margin-left: 0.25em; } + .cursorAutoTestGrid { + width: 100%; + margin-top: 1em; + } + .cursorAutoTestBox { + width: 150px; + height: 34px; + margin: 4px; + padding: 8px; + background-color: #313232; + color: white; + border: 1px solid #575757; + } + .cursorAutoDefault { cursor: default; } + .cursorAutoPointer { cursor: pointer; } + .cursorAutoText { cursor: text; } + .cursorAutoWait { cursor: wait; } + .cursorAutoProgress { cursor: progress; } + .cursorAutoResizeNWSE { cursor: nwse-resize; } + .cursorAutoResizeNESW { cursor: nesw-resize; } + .cursorAutoZoomIn { cursor: zoom-in; } + .cursorAutoZoomOut { cursor: zoom-out; } .gitDownloadRow { @@ -226,6 +248,18 @@

Project Settings

Editor Settings

{{template "section" .Editor}} +

Cursor Auto Test

+
+
default
+
pointer
+
text
+
wait
+
progress
+
nwse-resize
+
nesw-resize
+
zoom-in
+
zoom-out
+

Workspaces

diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/LICENSE.txt b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/LICENSE.txt new file mode 100644 index 000000000..7c3ee641e --- /dev/null +++ b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/LICENSE.txt @@ -0,0 +1,22 @@ + + + Cursor Pack (1.1) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 05-06-2024 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + This content is free to use in personal, educational and commercial projects. + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) + + ------------------------------ + + Donate: http://support.kenney.nl + Patreon: http://patreon.com/kenney/ + + Follow on Twitter for updates: + http://twitter.com/KenneyNL \ No newline at end of file diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/alias.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/alias.png new file mode 100644 index 000000000..ffa8cb2a4 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/alias.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/cell.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/cell.png new file mode 100644 index 000000000..d92384a0e Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/cell.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/context_menu.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/context_menu.png new file mode 100644 index 000000000..a9877c498 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/context_menu.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/copy.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/copy.png new file mode 100644 index 000000000..4e634a0d5 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/copy.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/crosshair.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/crosshair.png new file mode 100644 index 000000000..72b014a65 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/crosshair.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/default.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/default.png new file mode 100644 index 000000000..827b8696b Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/default.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/grab.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/grab.png new file mode 100644 index 000000000..5d7a4b15f Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/grab.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/grabbing.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/grabbing.png new file mode 100644 index 000000000..c5d066ee6 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/grabbing.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/help.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/help.png new file mode 100644 index 000000000..85c84f011 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/help.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/move.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/move.png new file mode 100644 index 000000000..f1c694c79 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/move.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/no_drop.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/no_drop.png new file mode 100644 index 000000000..0ce55c6b0 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/no_drop.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/none.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/none.png new file mode 100644 index 000000000..dc2758c56 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/none.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/not_allowed.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/not_allowed.png new file mode 100644 index 000000000..0ce55c6b0 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/not_allowed.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/pointer.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/pointer.png new file mode 100644 index 000000000..1ce04eff6 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/pointer.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/progress.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/progress.png new file mode 100644 index 000000000..aadbd45c7 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/progress.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_all.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_all.png new file mode 100644 index 000000000..f1c694c79 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_all.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_col.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_col.png new file mode 100644 index 000000000..2e1cb9b31 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_col.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_e.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_e.png new file mode 100644 index 000000000..2e1cb9b31 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_e.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_ew.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_ew.png new file mode 100644 index 000000000..2e1cb9b31 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_ew.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_n.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_n.png new file mode 100644 index 000000000..d0ad7ae36 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_n.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_ne.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_ne.png new file mode 100644 index 000000000..dcd53f63b Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_ne.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_nesw.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_nesw.png new file mode 100644 index 000000000..dcd53f63b Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_nesw.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_ns.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_ns.png new file mode 100644 index 000000000..d0ad7ae36 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_ns.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_nw.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_nw.png new file mode 100644 index 000000000..471c9ce88 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_nw.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_nwse.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_nwse.png new file mode 100644 index 000000000..471c9ce88 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_nwse.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_row.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_row.png new file mode 100644 index 000000000..d0ad7ae36 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_row.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_s.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_s.png new file mode 100644 index 000000000..d0ad7ae36 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_s.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_se.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_se.png new file mode 100644 index 000000000..471c9ce88 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_se.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_sw.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_sw.png new file mode 100644 index 000000000..dcd53f63b Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_sw.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_w.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_w.png new file mode 100644 index 000000000..2e1cb9b31 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/resize_w.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/text.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/text.png new file mode 100644 index 000000000..88d13fc96 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/text.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/vertical_text.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/vertical_text.png new file mode 100644 index 000000000..f988f5c46 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/vertical_text.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/wait.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/wait.png new file mode 100644 index 000000000..bf2aa0283 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/wait.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/zoom_in.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/zoom_in.png new file mode 100644 index 000000000..a3c636e4b Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/zoom_in.png differ diff --git a/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/zoom_out.png b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/zoom_out.png new file mode 100644 index 000000000..f7deba027 Binary files /dev/null and b/src/editor/editor_embedded_content/editor_content/textures/cursors/kenney/zoom_out.png differ diff --git a/src/editor/editor_overlay/color_picker/color_picker_overlay.go b/src/editor/editor_overlay/color_picker/color_picker_overlay.go index 5dcd1139f..21c6e62f5 100644 --- a/src/editor/editor_overlay/color_picker/color_picker_overlay.go +++ b/src/editor/editor_overlay/color_picker/color_picker_overlay.go @@ -49,6 +49,7 @@ import ( "kaijuengine.com/engine/ui/markup/document" "kaijuengine.com/matrix" "kaijuengine.com/platform/profiler/tracing" + "kaijuengine.com/platform/windowing" ) type ColorPicker struct { @@ -123,7 +124,7 @@ func Show(host *engine.Host, config Config) (*ColorPicker, error) { func (p *ColorPicker) Close() { defer tracing.NewRegion("ColorPicker.Close").End() host := p.uiMan.Host - host.Window.CursorStandard() + host.Window.SetCursor(windowing.CursorKindDefault) p.doc.Destroy() } diff --git a/src/editor/editor_overlay/content_selector/content_selector_overlay.go b/src/editor/editor_overlay/content_selector/content_selector_overlay.go index a7374c124..bc9a2e78e 100644 --- a/src/editor/editor_overlay/content_selector/content_selector_overlay.go +++ b/src/editor/editor_overlay/content_selector/content_selector_overlay.go @@ -48,6 +48,7 @@ import ( "kaijuengine.com/engine/ui/markup/document" "kaijuengine.com/platform/hid" "kaijuengine.com/platform/profiler/tracing" + "kaijuengine.com/platform/windowing" ) type ContentSelector struct { @@ -126,7 +127,7 @@ func (o *ContentSelector) Close() { } func (o *ContentSelector) closeInternal() { - o.uiMan.Host.Window.CursorStandard() + o.uiMan.Host.Window.SetCursor(windowing.CursorKindDefault) o.doc.Destroy() o.uiMan.Host.Window.Keyboard.RemoveKeyCallback(o.keyKb) } diff --git a/src/editor/editor_settings/editor_settings.go b/src/editor/editor_settings/editor_settings.go index 87d72cd21..7e3edb896 100644 --- a/src/editor/editor_settings/editor_settings.go +++ b/src/editor/editor_settings/editor_settings.go @@ -61,6 +61,8 @@ type Settings struct { MeshEditor string AudioEditor string UIScrollSpeed float32 `default:"20" label:"UI Scroll Speed"` + CursorMode string `default:"virtual" label:"Cursor Mode"` + CursorTheme string `default:"kenney" label:"Cursor Theme"` ShowGrid bool `default:"true" label:"Show Viewport Grid"` EditorCamera EditorCameraSettings Snapping SnapSettings @@ -110,6 +112,8 @@ func (s *Settings) setDefaults() { s.RefreshRate = 60 s.CodeEditor = "code" s.UIScrollSpeed = 20 + s.CursorMode = "virtual" + s.CursorTheme = "kenney" s.ShowGrid = true s.EditorCamera.ZoomSpeed = 120 s.EditorCamera.FlySpeed = 10 diff --git a/src/editor/editor_workspace/settings_workspace/settings_workspace.go b/src/editor/editor_workspace/settings_workspace/settings_workspace.go index 42157db00..bbe46651b 100644 --- a/src/editor/editor_workspace/settings_workspace/settings_workspace.go +++ b/src/editor/editor_workspace/settings_workspace/settings_workspace.go @@ -82,18 +82,18 @@ type editorWorkspaceController interface { type SettingsWorkspace struct { common_workspace.CommonWorkspace - projectSettingsBox *document.Element - editorSettingsBox *document.Element - pluginSettingsBox *document.Element + projectSettingsBox *document.Element + editorSettingsBox *document.Element + pluginSettingsBox *document.Element workspaceSettingsBox *document.Element - editor editor_workspace.WorkspaceEditorInterface - editorSettings *editor_settings.Settings - projectSettings *project.Settings - plugins []editor_plugin.PluginInfo - pluginInitStates []bool - reloadRequested bool - recompiling bool - downloadingPlugin bool + editor editor_workspace.WorkspaceEditorInterface + editorSettings *editor_settings.Settings + projectSettings *project.Settings + plugins []editor_plugin.PluginInfo + pluginInitStates []bool + reloadRequested bool + recompiling bool + downloadingPlugin bool } // workspaceRowData is the per-row data the Workspaces panel template loops over. @@ -301,6 +301,7 @@ func (w *SettingsWorkspace) valueChanged(e *document.Element) { defer tracing.NewRegion("SettingsWorkspace.valueChanged").End() if w.editorSettingsBox.UI.Entity().IsActive() { common_workspace.SetObjectValueFromUI(w.editorSettings, e) + w.editor.UpdateSettings() } else if w.projectSettingsBox.UI.Entity().IsActive() { common_workspace.SetObjectValueFromUI(w.projectSettings, e) } @@ -501,7 +502,16 @@ func (w *SettingsWorkspace) uiData() settingsWorkspaceData { for i := range w.plugins { w.pluginInitStates[i] = w.plugins[i].Config.Enabled } - listings := map[string][]ui.SelectOption{} + listings := map[string][]ui.SelectOption{ + "CursorMode": { + {Name: "Native", Value: "native"}, + {Name: "Virtual", Value: "virtual"}, + {Name: "Auto", Value: "auto"}, + }, + "CursorTheme": { + {Name: "Kenney", Value: "kenney"}, + }, + } cache := w.editor.Project().CacheDatabase() return settingsWorkspaceData{ Editor: common_workspace.ReflectUIStructure(cache, diff --git a/src/engine/ui/cursor.go b/src/engine/ui/cursor.go new file mode 100644 index 000000000..4022a9a29 --- /dev/null +++ b/src/engine/ui/cursor.go @@ -0,0 +1,256 @@ +/******************************************************************************/ +/* cursor.go */ +/******************************************************************************/ +/* This file is part of */ +/* KAIJU ENGINE */ +/* https://kaijuengine.com/ */ +/******************************************************************************/ +/* MIT License */ +/* */ +/* Copyright (c) 2023-present Kaiju Engine authors (AUTHORS.md). */ +/* Copyright (c) 2015-present Brent Farris. */ +/* */ +/* May all those that this source may reach be blessed by the LORD and find */ +/* peace and joy in life. */ +/* Everyone who drinks of this water will be thirsty again; but whoever */ +/* drinks of the water that I will give him shall never thirst; John 4:13-14 */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining a */ +/* copy of this software and associated documentation files (the "Software"), */ +/* to deal in the Software without restriction, including without limitation */ +/* the rights to use, copy, modify, merge, publish, distribute, sublicense, */ +/* and/or sell copies of the Software, and to permit persons to whom the */ +/* Software is furnished to do so, subject to the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be included in */ +/* all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS */ +/* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT */ +/* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE */ +/* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/******************************************************************************/ + +package ui + +import ( + "log/slog" + "path" + + "kaijuengine.com/matrix" + "kaijuengine.com/platform/profiler/tracing" + "kaijuengine.com/platform/windowing" + "kaijuengine.com/rendering" +) + +type Cursor UI + +type CursorTheme struct { + Name string + BasePath string + Size matrix.Vec2 + Textures map[windowing.CursorKind]string + Hotspots map[windowing.CursorKind]matrix.Vec2 +} + +type cursorData struct { + panelData + theme CursorTheme + lastKind windowing.CursorKind +} + +var KenneyCursorTheme = CursorTheme{ + Name: "kenney", + BasePath: "cursors/kenney", + Size: matrix.NewVec2(32, 32), + Textures: map[windowing.CursorKind]string{ + windowing.CursorKindAuto: "default.png", + windowing.CursorKindDefault: "default.png", + windowing.CursorKindNone: "none.png", + windowing.CursorKindContextMenu: "context_menu.png", + windowing.CursorKindText: "text.png", + windowing.CursorKindVerticalText: "vertical_text.png", + windowing.CursorKindPointer: "pointer.png", + windowing.CursorKindHelp: "help.png", + windowing.CursorKindWait: "wait.png", + windowing.CursorKindProgress: "progress.png", + windowing.CursorKindCrosshair: "crosshair.png", + windowing.CursorKindCell: "cell.png", + windowing.CursorKindAlias: "alias.png", + windowing.CursorKindCopy: "copy.png", + windowing.CursorKindMove: "move.png", + windowing.CursorKindNoDrop: "no_drop.png", + windowing.CursorKindNotAllowed: "not_allowed.png", + windowing.CursorKindGrab: "grab.png", + windowing.CursorKindGrabbing: "grabbing.png", + windowing.CursorKindResizeN: "resize_n.png", + windowing.CursorKindResizeE: "resize_e.png", + windowing.CursorKindResizeS: "resize_s.png", + windowing.CursorKindResizeW: "resize_w.png", + windowing.CursorKindResizeNE: "resize_ne.png", + windowing.CursorKindResizeNW: "resize_nw.png", + windowing.CursorKindResizeSE: "resize_se.png", + windowing.CursorKindResizeSW: "resize_sw.png", + windowing.CursorKindResizeNS: "resize_ns.png", + windowing.CursorKindResizeEW: "resize_ew.png", + windowing.CursorKindResizeNWSE: "resize_nwse.png", + windowing.CursorKindResizeNESW: "resize_nesw.png", + windowing.CursorKindResizeCol: "resize_col.png", + windowing.CursorKindResizeRow: "resize_row.png", + windowing.CursorKindResizeAll: "resize_all.png", + windowing.CursorKindZoomIn: "zoom_in.png", + windowing.CursorKindZoomOut: "zoom_out.png", + }, + Hotspots: map[windowing.CursorKind]matrix.Vec2{ + windowing.CursorKindText: matrix.NewVec2(16, 16), + windowing.CursorKindVerticalText: matrix.NewVec2(16, 16), + windowing.CursorKindWait: matrix.NewVec2(16, 16), + windowing.CursorKindProgress: matrix.NewVec2(16, 16), + windowing.CursorKindCrosshair: matrix.NewVec2(16, 16), + windowing.CursorKindCell: matrix.NewVec2(16, 16), + windowing.CursorKindMove: matrix.NewVec2(16, 16), + windowing.CursorKindGrab: matrix.NewVec2(16, 16), + windowing.CursorKindGrabbing: matrix.NewVec2(16, 16), + windowing.CursorKindResizeN: matrix.NewVec2(16, 16), + windowing.CursorKindResizeE: matrix.NewVec2(16, 16), + windowing.CursorKindResizeS: matrix.NewVec2(16, 16), + windowing.CursorKindResizeW: matrix.NewVec2(16, 16), + windowing.CursorKindResizeNE: matrix.NewVec2(16, 16), + windowing.CursorKindResizeNW: matrix.NewVec2(16, 16), + windowing.CursorKindResizeSE: matrix.NewVec2(16, 16), + windowing.CursorKindResizeSW: matrix.NewVec2(16, 16), + windowing.CursorKindResizeNS: matrix.NewVec2(16, 16), + windowing.CursorKindResizeEW: matrix.NewVec2(16, 16), + windowing.CursorKindResizeNWSE: matrix.NewVec2(16, 16), + windowing.CursorKindResizeNESW: matrix.NewVec2(16, 16), + windowing.CursorKindResizeCol: matrix.NewVec2(16, 16), + windowing.CursorKindResizeRow: matrix.NewVec2(16, 16), + windowing.CursorKindResizeAll: matrix.NewVec2(16, 16), + windowing.CursorKindZoomIn: matrix.NewVec2(16, 16), + windowing.CursorKindZoomOut: matrix.NewVec2(16, 16), + }, +} + +func (u *UI) ToCursor() *Cursor { return (*Cursor)(u) } +func (c *Cursor) Base() *UI { return (*UI)(c) } +func (d *cursorData) innerPanelData() *panelData { return &d.panelData } + +func (c *Cursor) CursorData() *cursorData { + return c.Base().elmData.(*cursorData) +} + +func (c *Cursor) Init(theme CursorTheme) { + c.Base().elmData = &cursorData{ + theme: theme, + lastKind: windowing.CursorKind(-1), + } + + p := c.Base().ToPanel() + p.Init(nil, ElementTypeCursor) + p.AllowClickThrough() + + if p.shaderData != nil { + p.shaderData.BorderLen = matrix.Vec2Zero() + } + + layout := c.Base().Layout() + layout.SetPositioning(PositioningFixed) + layout.SetZ(100) + layout.Scale(theme.Size.X(), theme.Size.Y()) + c.setKind(windowing.CursorKindDefault) +} + +func (c *Cursor) SetTheme(theme CursorTheme) { + data := c.CursorData() + data.theme = theme + data.lastKind = windowing.CursorKind(-1) + c.Base().Layout().Scale(theme.Size.X(), theme.Size.Y()) + c.setKind(c.Base().Host().Window.CursorKind()) +} + +func (c *Cursor) SyncWithWindow() { + base := c.Base() + host := base.Host() + if host == nil { + return + } + if !host.Window.UsesVirtualCursor() || + !host.Window.CursorVisible() || + host.Window.CursorKind() == windowing.CursorKindNone { + base.Hide() + return + } + base.Show() +} + +func (theme CursorTheme) TexturePath(kind windowing.CursorKind) string { + if kind == windowing.CursorKindAuto { + kind = windowing.CursorKindDefault + } + if texture, ok := theme.Textures[kind]; ok { + return path.Join(theme.BasePath, texture) + } + return path.Join(theme.BasePath, theme.Textures[windowing.CursorKindDefault]) +} + +func (theme CursorTheme) Hotspot(kind windowing.CursorKind) matrix.Vec2 { + if hotspot, ok := theme.Hotspots[kind]; ok { + return hotspot + } + return matrix.Vec2Zero() +} + +func CursorThemeByName(name string) CursorTheme { + switch name { + case KenneyCursorTheme.Name: + return KenneyCursorTheme + default: + return KenneyCursorTheme + } +} + +func (c *Cursor) setKind(kind windowing.CursorKind) { + data := c.CursorData() + texturePath := data.theme.TexturePath(kind) + tex, err := c.Base().Host().TextureCache().Texture(texturePath, rendering.TextureFilterNearest) + if err != nil { + slog.Error("failed to load virtual cursor texture", + "theme", data.theme.Name, "kind", kind, "texture", texturePath, "error", err) + return + } + c.Base().ToPanel().SetBackground(tex) + data.lastKind = kind +} + +func (c *Cursor) update(deltaTime float64) { + defer tracing.NewRegion("Cursor.update").End() + + base := c.Base() + host := base.Host() + if host == nil { + return + } + + if !host.Window.UsesVirtualCursor() || + !host.Window.CursorVisible() || + host.Window.CursorKind() == windowing.CursorKindNone { + base.Hide() + return + } + base.Show() + + kind := host.Window.CursorKind() + data := c.CursorData() + if data.lastKind != kind { + c.setKind(kind) + } + + pos := host.Window.Cursor.ScreenPosition() + hotspot := data.theme.Hotspot(kind) + base.Layout().SetOffset(pos.X()-hotspot.X(), pos.Y()-hotspot.Y()) + + base.ToPanel().update(deltaTime) +} diff --git a/src/engine/ui/input.go b/src/engine/ui/input.go index e6ac5cc02..be3909f91 100644 --- a/src/engine/ui/input.go +++ b/src/engine/ui/input.go @@ -49,6 +49,7 @@ import ( "kaijuengine.com/matrix" "kaijuengine.com/platform/hid" "kaijuengine.com/platform/profiler/tracing" + "kaijuengine.com/platform/windowing" "kaijuengine.com/rendering" ) @@ -568,11 +569,11 @@ func (input *Input) onRebuild() { } func (input *Input) onEnter() { - input.man.Value().Host.Window.CursorIbeam() + input.man.Value().Host.Window.SetCursor(windowing.CursorKindText) } func (input *Input) onExit() { - input.man.Value().Host.Window.CursorStandard() + input.man.Value().Host.Window.SetCursor(windowing.CursorKindDefault) } func (input *Input) onDown() { @@ -619,7 +620,7 @@ func (input *Input) detectDoubleClick() bool { func (input *Input) onMiss() { input.RemoveFocus() - input.man.Value().Host.Window.CursorStandard() + input.man.Value().Host.Window.SetCursor(windowing.CursorKindDefault) } func (input *Input) deactivated() { @@ -762,7 +763,7 @@ func (input *Input) RemoveFocus() { } man := input.man.Value() if man != nil { - man.Host.Window.CursorStandard() + man.Host.Window.SetCursor(windowing.CursorKindDefault) man.Group.setFocus(nil) } input.blur() diff --git a/src/engine/ui/markup/css/properties/css_cursor.go b/src/engine/ui/markup/css/properties/css_cursor.go index 29b86111d..c167260e3 100644 --- a/src/engine/ui/markup/css/properties/css_cursor.go +++ b/src/engine/ui/markup/css/properties/css_cursor.go @@ -43,7 +43,7 @@ import ( "kaijuengine.com/engine/ui" "kaijuengine.com/engine/ui/markup/css/rules" "kaijuengine.com/engine/ui/markup/document" - "kaijuengine.com/klib" + "kaijuengine.com/platform/windowing" ) func (p Cursor) Process(panel *ui.Panel, elm *document.Element, values []rules.PropertyValue, host *engine.Host) error { @@ -53,81 +53,85 @@ func (p Cursor) Process(panel *ui.Panel, elm *document.Element, values []rules.P panel.Base().AddEvent(ui.EventTypeEnter, func() { switch values[0].Str { - case "text": - host.Window.CursorIbeam() + case "auto": + host.Window.SetCursor(windowing.CursorKindAuto) case "default": - host.Window.CursorStandard() + host.Window.SetCursor(windowing.CursorKindDefault) + case "none": + host.Window.SetCursor(windowing.CursorKindNone) case "context-menu": - fallthrough - case "help": - fallthrough + host.Window.SetCursor(windowing.CursorKindContextMenu) + case "text": + host.Window.SetCursor(windowing.CursorKindText) + case "vertical-text": + host.Window.SetCursor(windowing.CursorKindVerticalText) case "pointer": - fallthrough + host.Window.SetCursor(windowing.CursorKindPointer) + case "help": + host.Window.SetCursor(windowing.CursorKindHelp) case "progress": - fallthrough + host.Window.SetCursor(windowing.CursorKindProgress) case "wait": - fallthrough + host.Window.SetCursor(windowing.CursorKindWait) case "cell": - fallthrough + host.Window.SetCursor(windowing.CursorKindCell) case "crosshair": - fallthrough - case "vertical-text": - fallthrough + host.Window.SetCursor(windowing.CursorKindCrosshair) case "alias": - fallthrough + host.Window.SetCursor(windowing.CursorKindAlias) case "copy": - fallthrough + host.Window.SetCursor(windowing.CursorKindCopy) case "move": - fallthrough + host.Window.SetCursor(windowing.CursorKindMove) case "no-drop": - fallthrough + host.Window.SetCursor(windowing.CursorKindNoDrop) case "not-allowed": - fallthrough + host.Window.SetCursor(windowing.CursorKindNotAllowed) case "grab": - fallthrough + host.Window.SetCursor(windowing.CursorKindGrab) case "grabbing": - fallthrough + host.Window.SetCursor(windowing.CursorKindGrabbing) case "all-scroll": - fallthrough + host.Window.SetCursor(windowing.CursorKindResizeAll) case "col-resize": - fallthrough + host.Window.SetCursor(windowing.CursorKindResizeCol) + case "e-resize": + host.Window.SetCursor(windowing.CursorKindResizeE) + case "w-resize": + host.Window.SetCursor(windowing.CursorKindResizeW) + case "ew-resize": + host.Window.SetCursor(windowing.CursorKindResizeEW) case "row-resize": - fallthrough + host.Window.SetCursor(windowing.CursorKindResizeRow) case "n-resize": - fallthrough - case "e-resize": - fallthrough + host.Window.SetCursor(windowing.CursorKindResizeN) case "s-resize": - fallthrough - case "w-resize": - fallthrough + host.Window.SetCursor(windowing.CursorKindResizeS) + case "ns-resize": + host.Window.SetCursor(windowing.CursorKindResizeNS) case "ne-resize": - fallthrough - case "nw-resize": - fallthrough - case "se-resize": - fallthrough + host.Window.SetCursor(windowing.CursorKindResizeNE) case "sw-resize": - fallthrough - case "ew-resize": - fallthrough - case "ns-resize": - fallthrough + host.Window.SetCursor(windowing.CursorKindResizeSW) case "nesw-resize": - fallthrough + host.Window.SetCursor(windowing.CursorKindResizeNESW) + case "nw-resize": + host.Window.SetCursor(windowing.CursorKindResizeNW) + case "se-resize": + host.Window.SetCursor(windowing.CursorKindResizeSE) case "nwse-resize": - fallthrough + host.Window.SetCursor(windowing.CursorKindResizeNWSE) case "zoom-in": - fallthrough + host.Window.SetCursor(windowing.CursorKindZoomIn) case "zoom-out": - klib.NotYetImplemented(180) + host.Window.SetCursor(windowing.CursorKindZoomOut) default: - host.Window.CursorStandard() + host.Window.SetCursor(windowing.CursorKindDefault) } }) panel.Base().AddEvent(ui.EventTypeExit, func() { - host.Window.CursorStandard() + host.Window.SetCursor(windowing.CursorKindDefault) }) return nil diff --git a/src/engine/ui/ui.go b/src/engine/ui/ui.go index 01bd9e3ee..c46adb108 100644 --- a/src/engine/ui/ui.go +++ b/src/engine/ui/ui.go @@ -50,9 +50,11 @@ import ( "kaijuengine.com/rendering" ) -type DirtyType = int -type ElementType = uint8 -type uiBits uint8 +type ( + DirtyType = int + ElementType = uint8 + uiBits uint8 +) const ( DirtyTypeNone = iota @@ -80,6 +82,7 @@ const ( ElementTypeProgressBar ElementTypeSelect ElementTypeSlider + ElementTypeCursor ) const ( @@ -335,7 +338,7 @@ func (ui *UI) GenerateScissor() { for p.PanelData().overflow == OverflowVisible && !p.entity.IsRoot() { p = FirstPanelOnEntity(p.entity.Parent) } - //if !p.entity.IsRoot() { + // if !p.entity.IsRoot() { ps := p.Base().selfScissor() bounds.SetX(max(bounds.X(), ps.X())) bounds.SetY(max(bounds.Y(), ps.Y())) @@ -564,7 +567,7 @@ func (ui *UI) anyChildDirty() bool { func (ui *UI) updateFromManager(deltaTime float64) { defer tracing.NewRegion("UI.updateFromManager").End() - if !ui.IsActive() { + if !ui.IsActive() && ui.elmType != ElementTypeCursor { return } switch ui.elmType { @@ -584,6 +587,8 @@ func (ui *UI) updateFromManager(deltaTime float64) { ui.ToImage().update(deltaTime) case ElementTypeCheckbox: ui.ToPanel().update(deltaTime) + case ElementTypeCursor: + ui.ToCursor().update(deltaTime) } } diff --git a/src/platform/windowing/cocoa_window.h b/src/platform/windowing/cocoa_window.h index bb6cdf7b5..2055a650d 100644 --- a/src/platform/windowing/cocoa_window.h +++ b/src/platform/windowing/cocoa_window.h @@ -41,12 +41,7 @@ void cocoa_set_title(void* nsWindow, const char* title); void cocoa_copy_to_clipboard(const char* text); char* cocoa_clipboard_contents(void); -// Cursor variants -void cocoa_cursor_standard(void); -void cocoa_cursor_ibeam(void); -void cocoa_cursor_size_all(void); -void cocoa_cursor_size_ns(void); -void cocoa_cursor_size_we(void); +void cocoa_set_cursor(int kind); void cocoa_show_cursor(void); void cocoa_hide_cursor(void); diff --git a/src/platform/windowing/cocoa_window.m b/src/platform/windowing/cocoa_window.m index 33c13fb04..9a79f9f33 100644 --- a/src/platform/windowing/cocoa_window.m +++ b/src/platform/windowing/cocoa_window.m @@ -560,42 +560,88 @@ void cocoa_copy_to_clipboard(const char* text) { } } -void cocoa_cursor_standard(void) { - dispatch_async(dispatch_get_main_queue(), ^{ - @autoreleasepool { - [[NSCursor arrowCursor] set]; - } - }); -} - -void cocoa_cursor_ibeam(void) { - dispatch_async(dispatch_get_main_queue(), ^{ - @autoreleasepool { - [[NSCursor IBeamCursor] set]; - } - }); -} - -void cocoa_cursor_size_all(void) { - dispatch_async(dispatch_get_main_queue(), ^{ - @autoreleasepool { - [[NSCursor closedHandCursor] set]; - } - }); -} - -void cocoa_cursor_size_ns(void) { - dispatch_async(dispatch_get_main_queue(), ^{ - @autoreleasepool { - [[NSCursor resizeUpDownCursor] set]; - } - }); +static BOOL gCursorHiddenByKaiju = NO; + +static void cocoa_set_cursor_hidden(BOOL hidden) { + if (hidden && !gCursorHiddenByKaiju) { + [NSCursor hide]; + gCursorHiddenByKaiju = YES; + } else if (!hidden && gCursorHiddenByKaiju) { + [NSCursor unhide]; + gCursorHiddenByKaiju = NO; + } } -void cocoa_cursor_size_we(void) { +void cocoa_set_cursor(int kind) { dispatch_async(dispatch_get_main_queue(), ^{ @autoreleasepool { - [[NSCursor resizeLeftRightCursor] set]; + switch (kind) { + case 2: // CursorKindNone + cocoa_set_cursor_hidden(YES); + break; + case 4: // CursorKindText + cocoa_set_cursor_hidden(NO); + [[NSCursor IBeamCursor] set]; + break; + case 5: // CursorKindVerticalText + cocoa_set_cursor_hidden(NO); + [[NSCursor IBeamCursorForVerticalLayout] set]; + break; + case 3: // CursorKindContextMenu + cocoa_set_cursor_hidden(NO); + [[NSCursor contextualMenuCursor] set]; + break; + case 6: // CursorKindPointer + cocoa_set_cursor_hidden(NO); + [[NSCursor pointingHandCursor] set]; + break; + case 12: // CursorKindAlias + cocoa_set_cursor_hidden(NO); + [[NSCursor dragLinkCursor] set]; + break; + case 13: // CursorKindCopy + cocoa_set_cursor_hidden(NO); + [[NSCursor dragCopyCursor] set]; + break; + case 10: // CursorKindCrosshair + case 11: // CursorKindCell + cocoa_set_cursor_hidden(NO); + [[NSCursor crosshairCursor] set]; + break; + case 14: // CursorKindMove + case 18: // CursorKindGrabbing + case 33: // CursorKindResizeAll + cocoa_set_cursor_hidden(NO); + [[NSCursor closedHandCursor] set]; + break; + case 15: // CursorKindNoDrop + case 16: // CursorKindNotAllowed + cocoa_set_cursor_hidden(NO); + [[NSCursor operationNotAllowedCursor] set]; + break; + case 17: // CursorKindGrab + cocoa_set_cursor_hidden(NO); + [[NSCursor openHandCursor] set]; + break; + case 19: // CursorKindResizeN + case 21: // CursorKindResizeS + case 27: // CursorKindResizeNS + case 32: // CursorKindResizeRow + cocoa_set_cursor_hidden(NO); + [[NSCursor resizeUpDownCursor] set]; + break; + case 20: // CursorKindResizeE + case 22: // CursorKindResizeW + case 28: // CursorKindResizeEW + case 31: // CursorKindResizeCol + cocoa_set_cursor_hidden(NO); + [[NSCursor resizeLeftRightCursor] set]; + break; + default: + cocoa_set_cursor_hidden(NO); + [[NSCursor arrowCursor] set]; + break; + } } }); } @@ -603,7 +649,7 @@ void cocoa_cursor_size_we(void) { void cocoa_show_cursor(void) { dispatch_async(dispatch_get_main_queue(), ^{ @autoreleasepool { - [NSCursor unhide]; + cocoa_set_cursor_hidden(NO); } }); } @@ -611,7 +657,7 @@ void cocoa_show_cursor(void) { void cocoa_hide_cursor(void) { dispatch_async(dispatch_get_main_queue(), ^{ @autoreleasepool { - [NSCursor hide]; + cocoa_set_cursor_hidden(YES); } }); } diff --git a/src/platform/windowing/cursor.go b/src/platform/windowing/cursor.go new file mode 100644 index 000000000..357264d9b --- /dev/null +++ b/src/platform/windowing/cursor.go @@ -0,0 +1,96 @@ +/******************************************************************************/ +/* cursor.go */ +/******************************************************************************/ +/* This file is part of */ +/* KAIJU ENGINE */ +/* https://kaijuengine.com/ */ +/******************************************************************************/ +/* MIT License */ +/* */ +/* Copyright (c) 2023-present Kaiju Engine authors (AUTHORS.md). */ +/* Copyright (c) 2015-present Brent Farris. */ +/* */ +/* May all those that this source may reach be blessed by the LORD and find */ +/* peace and joy in life. */ +/* Everyone who drinks of this water will be thirsty again; but whoever */ +/* drinks of the water that I will give him shall never thirst; John 4:13-14 */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining a */ +/* copy of this software and associated documentation files (the "Software"), */ +/* to deal in the Software without restriction, including without limitation */ +/* the rights to use, copy, modify, merge, publish, distribute, sublicense, */ +/* and/or sell copies of the Software, and to permit persons to whom the */ +/* Software is furnished to do so, subject to the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be included in */ +/* all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS */ +/* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT */ +/* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE */ +/* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/******************************************************************************/ + +package windowing + +type CursorKind int + +const ( + CursorKindAuto CursorKind = iota + CursorKindDefault + CursorKindNone + CursorKindContextMenu + CursorKindText + CursorKindVerticalText + CursorKindPointer + CursorKindHelp + CursorKindWait + CursorKindProgress + CursorKindCrosshair + CursorKindCell + CursorKindAlias + CursorKindCopy + CursorKindMove + CursorKindNoDrop + CursorKindNotAllowed + CursorKindGrab + CursorKindGrabbing + CursorKindResizeN + CursorKindResizeE + CursorKindResizeS + CursorKindResizeW + CursorKindResizeNE + CursorKindResizeNW + CursorKindResizeSE + CursorKindResizeSW + CursorKindResizeNS + CursorKindResizeEW + CursorKindResizeNWSE + CursorKindResizeNESW + CursorKindResizeCol + CursorKindResizeRow + CursorKindResizeAll + CursorKindZoomIn + CursorKindZoomOut +) + +type CursorMode int + +const ( + CursorModeNative CursorMode = iota + CursorModeVirtual + CursorModeAuto +) + +func NativeCursorSupported(kind CursorKind) bool { + switch kind { + case CursorKindAuto: + kind = CursorKindDefault + case CursorKindDefault, CursorKindNone: + return true + } + return nativeCursorSupported(kind) +} diff --git a/src/platform/windowing/cursor_android.go b/src/platform/windowing/cursor_android.go new file mode 100644 index 000000000..64f030f11 --- /dev/null +++ b/src/platform/windowing/cursor_android.go @@ -0,0 +1,7 @@ +//go:build android + +package windowing + +func nativeCursorSupported(CursorKind) bool { + return false +} diff --git a/src/platform/windowing/cursor_darwin.go b/src/platform/windowing/cursor_darwin.go new file mode 100644 index 000000000..57d07d8e8 --- /dev/null +++ b/src/platform/windowing/cursor_darwin.go @@ -0,0 +1,16 @@ +//go:build darwin && !ios + +package windowing + +func nativeCursorSupported(kind CursorKind) bool { + switch kind { + case CursorKindHelp, CursorKindWait, CursorKindProgress, + CursorKindResizeNE, CursorKindResizeNW, + CursorKindResizeSE, CursorKindResizeSW, + CursorKindResizeNWSE, CursorKindResizeNESW, + CursorKindZoomIn, CursorKindZoomOut: + return false + default: + return true + } +} diff --git a/src/platform/windowing/cursor_windows.go b/src/platform/windowing/cursor_windows.go new file mode 100644 index 000000000..1f4a78906 --- /dev/null +++ b/src/platform/windowing/cursor_windows.go @@ -0,0 +1,12 @@ +//go:build windows + +package windowing + +func nativeCursorSupported(kind CursorKind) bool { + switch kind { + case CursorKindZoomIn, CursorKindZoomOut: + return false + default: + return true + } +} diff --git a/src/platform/windowing/cursor_x11.go b/src/platform/windowing/cursor_x11.go new file mode 100644 index 000000000..7e57f1dc1 --- /dev/null +++ b/src/platform/windowing/cursor_x11.go @@ -0,0 +1,7 @@ +//go:build linux && !android + +package windowing + +func nativeCursorSupported(CursorKind) bool { + return true +} diff --git a/src/platform/windowing/win32.c b/src/platform/windowing/win32.c index 6bbcdb0ac..2cb3d0154 100644 --- a/src/platform/windowing/win32.c +++ b/src/platform/windowing/win32.c @@ -696,7 +696,7 @@ void window_main(const wchar_t* windowTitle, shared_mem_flush_events(sm); return; } - window_cursor_standard(hwnd); + window_set_cursor(hwnd, 1); // CursorKindDefault sm->windowWidth = width; sm->windowHeight = height; shared_mem_add_event(sm, (WindowEvent) { @@ -896,24 +896,80 @@ void window_destroy(void* hwnd) { free(sm); } -void window_cursor_standard(void* hwnd) { - PostMessageA(hwnd, UWM_SET_CURSOR, CURSOR_ARROW, 0); -} - -void window_cursor_ibeam(void* hwnd) { - PostMessageA(hwnd, UWM_SET_CURSOR, CURSOR_IBEAM, 0); -} - -void window_cursor_size_all(void* hwnd) { - PostMessageA(hwnd, UWM_SET_CURSOR, CURSOR_SIZE_ALL, 0); -} - -void window_cursor_size_ns(void* hwnd) { - PostMessageA(hwnd, UWM_SET_CURSOR, CURSOR_SIZE_NS, 0); -} +void window_set_cursor(void* hwnd, int kind) { + SharedMem* sm = (SharedMem*)GetWindowLongPtrA(hwnd, GWLP_USERDATA); + if (kind == 2) { // CursorKindNone + if (sm != NULL) { + sm->cursorHidden = true; + } + SetCursor(NULL); + while (ShowCursor(FALSE) >= 0) {} + return; + } + if (sm != NULL && sm->cursorHidden) { + sm->cursorHidden = false; + while (ShowCursor(TRUE) < 0) {} + } -void window_cursor_size_we(void* hwnd) { - PostMessageA(hwnd, UWM_SET_CURSOR, CURSOR_SIZE_WE, 0); + int cursor = CURSOR_ARROW; + switch (kind) { + case 4: // CursorKindText + case 5: // CursorKindVerticalText + cursor = CURSOR_IBEAM; + break; + case 6: // CursorKindPointer + case 3: // CursorKindContextMenu + case 12: // CursorKindAlias + case 13: // CursorKindCopy + case 17: // CursorKindGrab + case 18: // CursorKindGrabbing + cursor = CURSOR_HAND; + break; + case 7: // CursorKindHelp + cursor = CURSOR_HELP; + break; + case 8: // CursorKindWait + cursor = CURSOR_WAIT; + break; + case 9: // CursorKindProgress + cursor = CURSOR_APP_STARTING; + break; + case 10: // CursorKindCrosshair + case 11: // CursorKindCell + cursor = CURSOR_CROSS; + break; + case 14: // CursorKindMove + case 33: // CursorKindResizeAll + cursor = CURSOR_SIZE_ALL; + break; + case 15: // CursorKindNoDrop + case 16: // CursorKindNotAllowed + cursor = CURSOR_NO; + break; + case 19: // CursorKindResizeN + case 21: // CursorKindResizeS + case 27: // CursorKindResizeNS + case 32: // CursorKindResizeRow + cursor = CURSOR_SIZE_NS; + break; + case 20: // CursorKindResizeE + case 22: // CursorKindResizeW + case 28: // CursorKindResizeEW + case 31: // CursorKindResizeCol + cursor = CURSOR_SIZE_WE; + break; + case 23: // CursorKindResizeNE + case 26: // CursorKindResizeSW + case 30: // CursorKindResizeNESW + cursor = CURSOR_SIZE_NESW; + break; + case 24: // CursorKindResizeNW + case 25: // CursorKindResizeSE + case 29: // CursorKindResizeNWSE + cursor = CURSOR_SIZE_NWSE; + break; + } + PostMessageA(hwnd, UWM_SET_CURSOR, cursor, 0); } float window_dpi(void* hwnd) { diff --git a/src/platform/windowing/win32.h b/src/platform/windowing/win32.h index d861f3d5a..4d15b0995 100644 --- a/src/platform/windowing/win32.h +++ b/src/platform/windowing/win32.h @@ -49,11 +49,7 @@ void window_show(void* hwnd); void window_poll_controller(void* hwnd); void window_poll(void* hwnd); void window_destroy(void* hwnd); -void window_cursor_standard(void* hwnd); -void window_cursor_ibeam(void* hwnd); -void window_cursor_size_all(void* hwnd); -void window_cursor_size_ns(void* hwnd); -void window_cursor_size_we(void* hwnd); +void window_set_cursor(void* hwnd, int kind); float window_dpi(void* hwnd); int screen_width_mm(void* hwnd); int screen_height_mm(void* hwnd); diff --git a/src/platform/windowing/window.darwin.go b/src/platform/windowing/window.darwin.go index 5c30e7616..5187aa3e4 100644 --- a/src/platform/windowing/window.darwin.go +++ b/src/platform/windowing/window.darwin.go @@ -101,12 +101,7 @@ func (w *Window) poll() { } } -// Cursor variants (private) -func (w *Window) cursorStandard() { C.cocoa_cursor_standard() } -func (w *Window) cursorIbeam() { C.cocoa_cursor_ibeam() } -func (w *Window) cursorSizeAll() { C.cocoa_cursor_size_all() } -func (w *Window) cursorSizeNS() { C.cocoa_cursor_size_ns() } -func (w *Window) cursorSizeWE() { C.cocoa_cursor_size_we() } +func (w *Window) setCursor(kind CursorKind) { C.cocoa_set_cursor(C.int(kind)) } // Clipboard (private) func (w *Window) copyToClipboard(text string) { diff --git a/src/platform/windowing/window.go b/src/platform/windowing/window.go index df8bc13a4..15b4fc750 100644 --- a/src/platform/windowing/window.go +++ b/src/platform/windowing/window.go @@ -86,7 +86,7 @@ type Window struct { width, height int left, top, right, bottom int // Full window including title and borders resetDragDataInFrames int - cursorChangeCount int + cursorVisible bool cachedScreenSizeWidthMM int cacheScreenSizeHeightMM int windowSync chan struct{} @@ -98,6 +98,8 @@ type Window struct { fatalFromNativeAPI bool resizedFromNativeAPI bool isFullScreen bool + cursorKind CursorKind + cursorMode CursorMode } type FileSearch struct { @@ -132,21 +134,24 @@ func New(windowName string, width, height, x, y int, adb assets.Database, platfo debug.Assert(height > 0, "window height must be greater than zero") debug.Assert(adb != nil, "asset database cannot be nil") w := &Window{ - Keyboard: hid.NewKeyboard(), - Mouse: hid.NewMouse(), - Touch: hid.NewTouch(), - Stylus: hid.NewStylus(), - Controller: hid.NewController(), - width: width, - height: height, - x: x, - y: y, - left: x, - top: y, - right: x + width, - bottom: y + height, - title: windowName, - windowSync: make(chan struct{}), + Keyboard: hid.NewKeyboard(), + Mouse: hid.NewMouse(), + Touch: hid.NewTouch(), + Stylus: hid.NewStylus(), + Controller: hid.NewController(), + width: width, + height: height, + x: x, + y: y, + left: x, + top: y, + right: x + width, + bottom: y + height, + title: windowName, + cursorKind: CursorKindDefault, + cursorMode: CursorModeNative, + cursorVisible: true, + windowSync: make(chan struct{}), } keys := w.checkToggleKeyState() for key, pressed := range keys { @@ -312,39 +317,81 @@ func DPI2PXF(pixels, mm, targetMM float64) float64 { return targetMM * (pixels / mm) } -func (w *Window) CursorStandard() { - w.cursorChangeCount = max(0, w.cursorChangeCount-1) - if w.cursorChangeCount == 0 { - w.cursorStandard() - } +func (w *Window) CursorKind() CursorKind { + return w.cursorKind +} + +func (w *Window) CursorMode() CursorMode { + return w.cursorMode } -func (w *Window) CursorIbeam() { - if w.canChangeCursor() { - w.cursorIbeam() +func (w *Window) cursorRenderMode() CursorMode { + if w.cursorMode != CursorModeAuto { + return w.cursorMode } - w.cursorChangeCount++ + if NativeCursorSupported(w.cursorKind) { + return CursorModeNative + } + return CursorModeVirtual +} + +func (w *Window) UsesVirtualCursor() bool { + return w.cursorRenderMode() == CursorModeVirtual +} + +func (w *Window) CursorVisible() bool { + return w.cursorVisible } -func (w *Window) CursorSizeAll() { - if w.canChangeCursor() { - w.cursorSizeAll() +// SetCursorMode selects the cursor renderer. Native mode delegates cursor +// drawing to the platform; virtual mode hides the platform cursor so UI can +// render it instead. +func (w *Window) SetCursorMode(mode CursorMode) { + if w.cursorMode == mode { + w.applyCursor() + return } - w.cursorChangeCount++ + w.cursorMode = mode + w.applyCursor() } -func (w *Window) CursorSizeNS() { - if w.canChangeCursor() { - w.cursorSizeNS() +func (w *Window) ResetCursor() { + w.SetCursor(CursorKindDefault) +} + +func (w *Window) SetCursor(kind CursorKind) { + if kind == CursorKindAuto { + kind = CursorKindDefault } - w.cursorChangeCount++ + w.cursorKind = kind + w.applyCursor() +} + +// HideCursor is a visibility override. They do not change the +// current cursor mode or semantic cursor kind. +func (w *Window) HideCursor() { + w.cursorVisible = false + w.hideCursor() +} + +// ShowCursor is a visibility override. They do not change the +// current cursor mode or semantic cursor kind. +func (w *Window) ShowCursor() { + w.cursorVisible = true + w.applyCursor() } -func (w *Window) CursorSizeWE() { - if w.canChangeCursor() { - w.cursorSizeWE() +func (w *Window) applyCursor() { + if !w.cursorVisible { + w.hideCursor() + return + } + if w.UsesVirtualCursor() { + w.hideCursor() + return } - w.cursorChangeCount++ + w.showCursor() + w.setCursor(w.cursorKind) } func (w *Window) CopyToClipboard(text string) { w.copyToClipboard(text) } @@ -371,7 +418,7 @@ func (w *Window) Destroy() { func (w *Window) Focus() { defer tracing.NewRegion("Window.Focus").End() w.focus() - w.cursorStandard() + w.applyCursor() } func (w *Window) Position() (x int, y int) { @@ -397,8 +444,6 @@ func (w *Window) SetSize(width, height int) { func (w *Window) RemoveBorder() { w.removeBorder() } func (w *Window) AddBorder() { w.addBorder() } -func (w *Window) ShowCursor() { w.showCursor() } -func (w *Window) HideCursor() { w.hideCursor() } func (w *Window) IsFullScreen() bool { return w.isFullScreen } func (w *Window) UnlockCursor() { w.unlockCursor() } @@ -490,8 +535,6 @@ func (w *Window) requestSync() { w.syncRequest = true } -func (w *Window) canChangeCursor() bool { return w.cursorChangeCount == 0 } - func (w *Window) processWindowResizeEvent(evt *WindowResizeEvent) { w.width = int(evt.width) w.height = int(evt.height) @@ -676,7 +719,7 @@ func (w *Window) becameInactive() { func (w *Window) becameActive() { defer tracing.NewRegion("Window.becameActive").End() - w.cursorStandard() + w.applyCursor() idx := -1 for i := range activeWindows { if activeWindows[i] == w { diff --git a/src/platform/windowing/window.x11.go b/src/platform/windowing/window.x11.go index 2427c5d19..782577377 100644 --- a/src/platform/windowing/window.x11.go +++ b/src/platform/windowing/window.x11.go @@ -53,11 +53,7 @@ package windowing #cgo noescape window_set_size #cgo noescape window_width_mm #cgo noescape window_height_mm -#cgo noescape window_cursor_standard -#cgo noescape window_cursor_ibeam -#cgo noescape window_cursor_size_all -#cgo noescape window_cursor_size_ns -#cgo noescape window_cursor_size_we +#cgo noescape window_set_cursor #cgo noescape window_show_cursor #cgo noescape window_hide_cursor #cgo noescape window_dpi @@ -127,24 +123,8 @@ func (w *Window) poll() { C.window_poll(w.handle) } -func (w *Window) cursorStandard() { - C.window_cursor_standard(w.handle) -} - -func (w *Window) cursorIbeam() { - C.window_cursor_ibeam(w.handle) -} - -func (w *Window) cursorSizeAll() { - C.window_cursor_size_all(w.handle) -} - -func (w *Window) cursorSizeNS() { - C.window_cursor_size_ns(w.handle) -} - -func (w *Window) cursorSizeWE() { - C.window_cursor_size_we(w.handle) +func (w *Window) setCursor(kind CursorKind) { + C.window_set_cursor(w.handle, C.int(kind)) } func (w *Window) copyToClipboard(text string) { diff --git a/src/platform/windowing/window_android.go b/src/platform/windowing/window_android.go index 2d748e455..b06ffe6ab 100644 --- a/src/platform/windowing/window_android.go +++ b/src/platform/windowing/window_android.go @@ -165,11 +165,7 @@ func (w *Window) cHandle() unsafe.Pointer { return w.handle } func (w *Window) cInstance() unsafe.Pointer { return w.instance } func (w *Window) showWindow() {} -func (w *Window) cursorStandard() {} -func (w *Window) cursorIbeam() {} -func (w *Window) cursorSizeAll() {} -func (w *Window) cursorSizeNS() {} -func (w *Window) cursorSizeWE() {} +func (w *Window) setCursor(kind CursorKind) {} func (w *Window) focus() {} func (w *Window) position() (x, y int) { return 0, 0 } func (w *Window) setPosition(x, y int) {} diff --git a/src/platform/windowing/window_windows.go b/src/platform/windowing/window_windows.go index a8052461f..8a5ecabcd 100644 --- a/src/platform/windowing/window_windows.go +++ b/src/platform/windowing/window_windows.go @@ -55,8 +55,7 @@ import ( #cgo noescape window_main #cgo noescape window_show #cgo noescape window_destroy -#cgo noescape window_cursor_standard -#cgo noescape window_cursor_ibeam +#cgo noescape window_set_cursor #cgo noescape window_dpi #cgo noescape screen_width_mm #cgo noescape screen_height_mm @@ -135,24 +134,8 @@ func (w *Window) poll() { w.pollEvents() } -func (w *Window) cursorStandard() { - C.window_cursor_standard(w.handle) -} - -func (w *Window) cursorIbeam() { - C.window_cursor_ibeam(w.handle) -} - -func (w *Window) cursorSizeAll() { - C.window_cursor_size_all(w.handle) -} - -func (w *Window) cursorSizeNS() { - C.window_cursor_size_ns(w.handle) -} - -func (w *Window) cursorSizeWE() { - C.window_cursor_size_we(w.handle) +func (w *Window) setCursor(kind CursorKind) { + C.window_set_cursor(w.handle, C.int(kind)) } func (w *Window) copyToClipboard(text string) { diff --git a/src/platform/windowing/x11.c b/src/platform/windowing/x11.c index b7ae9f47a..b40dd8c69 100644 --- a/src/platform/windowing/x11.c +++ b/src/platform/windowing/x11.c @@ -645,34 +645,113 @@ int screen_count(void* state) { return 1; // TODO } -void window_cursor_standard(void* state) { - X11State* s = state; - Cursor c = XcursorLibraryLoadCursor(s->d, "arrow"); - XDefineCursor(s->d, s->w, c); -} - -void window_cursor_ibeam(void* state) { - X11State* s = state; - Cursor c = XcursorLibraryLoadCursor(s->d, "xterm"); - XDefineCursor(s->d, s->w, c); +static Cursor load_cursor(Display* d, const char** names) { + for (int i = 0; names[i] != NULL; i++) { + Cursor c = XcursorLibraryLoadCursor(d, names[i]); + if (c != None) { + return c; + } + } + return XcursorLibraryLoadCursor(d, "arrow"); } -void window_cursor_size_all(void* state) { - X11State* s = state; - Cursor c = XcursorLibraryLoadCursor(s->d, "sizing"); - XDefineCursor(s->d, s->w, c); -} +void window_set_cursor(void* state, int kind) { + if (kind == 2) { // CursorKindNone + window_hide_cursor(state); + return; + } -void window_cursor_size_ns(void* state) { X11State* s = state; - Cursor c = XcursorLibraryLoadCursor(s->d, "sb_v_double_arrow"); - XDefineCursor(s->d, s->w, c); -} + const char* arrow[] = {"default", "arrow", "left_ptr", NULL}; + const char* text[] = {"text", "xterm", NULL}; + const char* hand[] = {"pointer", "hand2", "hand1", NULL}; + const char* help[] = {"help", "question_arrow", "left_ptr", NULL}; + const char* wait[] = {"wait", "watch", NULL}; + const char* progress[] = {"progress", "left_ptr_watch", "watch", NULL}; + const char* crosshair[] = {"crosshair", "cross", NULL}; + const char* move[] = {"move", "fleur", "sizing", NULL}; + const char* no[] = {"not-allowed", "no-drop", "crossed_circle", NULL}; + const char* grab[] = {"grab", "openhand", "hand2", NULL}; + const char* grabbing[] = {"grabbing", "closedhand", "fleur", NULL}; + const char* ns[] = {"ns-resize", "row-resize", "sb_v_double_arrow", NULL}; + const char* ew[] = {"ew-resize", "col-resize", "sb_h_double_arrow", NULL}; + const char* nesw[] = {"nesw-resize", "ne-resize", "sw-resize", "bottom_left_corner", "top_right_corner", NULL}; + const char* nwse[] = {"nwse-resize", "nw-resize", "se-resize", "bottom_right_corner", "top_left_corner", NULL}; + const char* zoom_in[] = {"zoom-in", "plus", NULL}; + const char* zoom_out[] = {"zoom-out", "minus", NULL}; + const char** names = arrow; + + switch (kind) { + case 3: // CursorKindContextMenu + case 6: // CursorKindPointer + case 12: // CursorKindAlias + case 13: // CursorKindCopy + names = hand; + break; + case 4: // CursorKindText + case 5: // CursorKindVerticalText + names = text; + break; + case 7: // CursorKindHelp + names = help; + break; + case 8: // CursorKindWait + names = wait; + break; + case 9: // CursorKindProgress + names = progress; + break; + case 10: // CursorKindCrosshair + case 11: // CursorKindCell + names = crosshair; + break; + case 14: // CursorKindMove + case 33: // CursorKindResizeAll + names = move; + break; + case 15: // CursorKindNoDrop + case 16: // CursorKindNotAllowed + names = no; + break; + case 17: // CursorKindGrab + names = grab; + break; + case 18: // CursorKindGrabbing + names = grabbing; + break; + case 19: // CursorKindResizeN + case 21: // CursorKindResizeS + case 27: // CursorKindResizeNS + case 32: // CursorKindResizeRow + names = ns; + break; + case 20: // CursorKindResizeE + case 22: // CursorKindResizeW + case 28: // CursorKindResizeEW + case 31: // CursorKindResizeCol + names = ew; + break; + case 23: // CursorKindResizeNE + case 26: // CursorKindResizeSW + case 30: // CursorKindResizeNESW + names = nesw; + break; + case 24: // CursorKindResizeNW + case 25: // CursorKindResizeSE + case 29: // CursorKindResizeNWSE + names = nwse; + break; + case 34: // CursorKindZoomIn + names = zoom_in; + break; + case 35: // CursorKindZoomOut + names = zoom_out; + break; + } -void window_cursor_size_we(void* state) { - X11State* s = state; - Cursor c = XcursorLibraryLoadCursor(s->d, "sb_h_double_arrow"); + Cursor c = load_cursor(s->d, names); XDefineCursor(s->d, s->w, c); + XFlush(s->d); } void window_show_cursor(void* state) { diff --git a/src/platform/windowing/x11.h b/src/platform/windowing/x11.h index 566a80072..a01a57605 100644 --- a/src/platform/windowing/x11.h +++ b/src/platform/windowing/x11.h @@ -109,11 +109,7 @@ int window_width_mm(void* state); int window_height_mm(void* state); int screen_count(void* state); void window_invalidate_monitor_cache(void* state); -void window_cursor_standard(void* state); -void window_cursor_ibeam(void* state); -void window_cursor_size_all(void* state); -void window_cursor_size_ns(void* state); -void window_cursor_size_we(void* state); +void window_set_cursor(void* state, int kind); void window_show_cursor(void* state); void window_hide_cursor(void* state); float window_dpi(void* state);