diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e003ee..c194b5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,18 +9,90 @@ All notable changes to **blenderui** (BUI). The format follows ### Added - **Rust:** `` (and ``): the field reports every keystroke to `on_input` / `on_change`, not only on Enter or blur, so a view can follow the typing (a search box, a date field that lights up a calendar). +- **Linux window requests** that Win32 and Cocoa already had, through `BK_window_wm` (Python `Window.wm`): + `"state"` 2 and 0 hide and show a window on X11 and Wayland; `"override"` removes the frame (`_MOTIF_WM_HINTS` + on X11, client-side decorations on Wayland) and `"topmost"` keeps an X11 window above others + (`_NET_WM_STATE_ABOVE`), both ported from `GHOST_WindowX11`. +- **Transparent windows on Linux:** `wm "transparent"` before `show` gives per-pixel alpha, with no region + background, through a 32-bit ARGB visual on X11 and ARGB8888 buffers on Wayland. The software raster can keep + premultiplied alpha (`BUI_raster_set_alpha`, `BUI_raster_alpha`, `BUI_raster_clear`); opaque frames are + unchanged byte for byte and draw as fast as before. The OpenGL renderer draws transparent windows too: a GLX config for the + 32-bit visual, frames cleared to alpha 0 and `BUI_raster_clear` as a GPU clear of the redrawn rect (`ROP`: premultiplied + `ONE, ONE_MINUS_SRC_ALPHA` for colour and alpha, as in Blender's `GPU_BLEND_ALPHA_PREMULT`). +- **Transparent windows on Windows and macOS:** `wm "transparent"` gives per-pixel alpha with the software + raster. Windows shows the premultiplied frame through a layered window (`UpdateLayeredWindow`, `AC_SRC_ALPHA`; the + window has no frame and `"alpha"` scales the whole window); macOS draws it in a non-opaque window with a clear + background. Rust `WindowBackgroundAppearance::Transparent` and JavaScript `transparent: true` use it there too. + With the Direct3D 11 renderer the window uses DWM blur-behind (`DwmEnableBlurBehindWindow` over the client area, the + surface cleared to zero alpha) and a bitblt-model swap chain, as SDL does (no copy to the CPU, `ClearView` for the + redrawn rect); without the device it falls back to `UpdateLayeredWindow`. A window that is not transparent keeps the + flip-model swap chain with `DXGI_ALPHA_MODE_IGNORE`. +- **Layer-shell windows on Wayland:** `wm "layer"` (layer 0 background to 3 overlay, keyboard interactivity) and + `"anchor"` (edges, exclusive zone) make a wlr-layer-shell surface, on KDE Plasma, wlroots compositors and + COSMIC (not GNOME). `wm_get("layer")` says whether the compositor has it. The protocol file (MIT) is in + `kit/extern/wayland_protocols`. +- **Focus callback:** `BKWindowCallbacks.on_focus` reports when a window gets or loses the keyboard focus, on + Win32, Cocoa, X11 and Wayland (Python `Window.on_focus`). Losing it cancels an open pie, as Blender's + `WINDEACTIVATE` does. In Python the callback is queued behind a running callback, like the timers. +- **Hit test:** `BK_hit_at(ctx, x, y)` gives the interactive node under a point (Python `ctx.hit`, Lua `ctx:hit`). +- **Pie menus as in Blender** (`pie_handler`, `pie_menu_begin`): opened while a key is held, releasing the key + picks the item in the pointer's direction, and a quick tap keeps the pie open for a click; an item with a + submenu opens a sub-pie at the pointer; past 8 items, the 8th is "More" with the rest + (`pie_menu_level_create`); items get underlined accelerator letters (`menu_block_set_keyaccels`); and the + items grow out of the center over 60 ms (`pie_animation_timeout`, off with reduced motion). +- **Rust:** `App::on_window_activation(|id, active, cx| ...)` reports keyboard focus, and `WindowOptions.window_background = + WindowBackgroundAppearance::Transparent` opens a transparent window on Linux, Windows and macOS. +- **JavaScript** (Node, Bun and `bui build` apps): `app.window({ transparent: true })` and the window events `focus` and + `blur`. +- **Window kinds in Rust and JavaScript:** Rust `WindowOptions.kind` (`WindowKind::Floating`, `PopUp` and + `LayerShell(LayerShellOptions)`) and `window_decorations` (`WindowDecorations::Client`), and JavaScript + `app.window({ alwaysOnTop, frame: false, layerShell: { layer, anchor, exclusiveZone, keyboard } })`, send the + `topmost`, `override`, `layer` and `anchor` window requests that Python reaches through `Window.wm`. Topmost works on + Windows, macOS and X11; a Wayland toplevel cannot ask for it, so use the layer shell there. +- **Dear ImGui:** `ImGui_ImplBUI_AbandonFrame()` (C: `cImGui_ImplBUI_AbandonFrame`) ends a frame that a binding's + callback left early on an error, with Dear ImGui's error recovery. ### Changed - **Text fields:** in the HTML look (browser-style inputs and text areas) the caret blinks like a browser's (about every 530 ms, solid right after it moves or the text changes); Blender's own fields keep Blender's solid caret. The window wakes only for the next toggle, so an idle window still sleeps. +- `BK_open_pie` takes the pie's center (`at_x`, `at_y`; `-100000` for the pointer, as in `BK_open_menu`). + Python `PieMenu.show(..., at=(x, y))` and `ctx.open_pie(..., at=)`, Lua `ctx:open_pie(items, title, x, y)`. +- Pie number keys are directions on the numpad (8 north, 6 east, 2 south, 4 west, the diagonals on 7, 9, 1 and 3), + as in Blender. They were item positions. +- **Python text has no shadow:** Blender's UI text shadow made small text look soft. The Python binding turns it off at import; + `buipy.kit_set_text_shadow(True)` brings it back. Other bindings keep it. The switch is `BK_set_text_shadow(on)` in C. +- **Wayland cursors** go through the cursor-shape protocol when the compositor has it (KDE, wlroots, GNOME 46+), as in + Blender's GHOST: the compositor draws the cursor and no cursor theme is loaded. Elsewhere the theme loads on the first + cursor change, not at start-up. Builds with wayland-protocols older than 1.32 keep the theme. +- `BKWindowCallbacks` has the new `on_focus` member, and so has the Rust `blenderui-sys` struct. +- **GPU renderer header:** `BuiGpuX11Window` has the new `visual` member (the window's VisualID, 0 for the default one), + and `BUI_raster_clear` clears on a GPU raster too. Call `BUI_raster_set_alpha` before attaching a GPU renderer: it + reads the setting when it starts. ### Fixed +- **Closing a pie during its open animation** freed it while the animation still pointed at it, so the next render read freed + memory (`bui_lua_context` crashed in CI). Closing a popup now clears everything that refers into it through `bk_forget`. +- **Wayland frames copied the whole window every frame** after the first full one: the double-buffer copy kept + its own full copy as the next frame's damage. It copies and reports only what changed now. +- **Setting a node's geometry or a flag property** (`visible`, `enabled` ...) **to the value it already has** relaid out + and redrew the whole window. Unchanged values do nothing now. The Rust binding sets every widget's place each frame, + so each Rust frame was a full redraw: 2.7 ms of CPU per frame at 941x982 on Wayland, 0.43 ms now. +- **`BK_canvas_put_item` / `BK_canvas_truncate`** (display lists rebuilt each frame) redrew the whole canvas. Only items + that changed, moved or went away are redrawn now. +- **Python Tk host on X11:** a held key no longer sends a release on each auto-repeat, so holding a pie key keeps the pie open + until the key is up (the native X11 host already did this). - **Lua bindings** set_menus on menubar fix; ctx:free() no-ops fix; widget tests - **Browser-look text fields** take the CSS `color` and `font-size` the field itself is given (one that differs from its parent's); without them they keep Chrome's black 13.33px, as before. - **Browser-look form controls** drew their text with Blender's UI drop shadow, which made it look heavy next to the page's text. They draw none now, as in a browser. - **macOS:** the npm `bui-runtime` (and the release build of the addon) failed to link with `Undefined symbols: ___isPlatformVersionAtLeast`: the Cocoa host asked for ProMotion with `@available`, which needs a symbol from clang's runtime that a Rust binary linked with `-nodefaultlibs` does not have. It asks by selector now. +- **Wayland:** a new window drew nothing until a later turn of the event loop, because the host did not wait for + the compositor's first configure. It waits now, as `GHOST_WindowWayland` does (`buipy_render` failed on + Wayland sessions). +- **Dear ImGui from Python:** an exception after `Begin()` in a frame callback aborted the process in builds with + asserts ("Missing End()"). The frame is unwound now and the exception reaches Python. +- Pies with more than 8 items dropped the rest; they now get Blender's "More" item. ## [0.5.0] - 2026-11-08 diff --git a/README.md b/README.md index c1ebe4d..0d959c1 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ If you're starting fresh in Python, the Blender components are the friendliest. - **Math in number fields**, with Blender's own expression evaluator: `2*pi`, `min(4, 3)`, `round(1.7)`, `x if c else y`. - Ctrl+Wheel steps values, Ctrl+C / Ctrl+V copy values, Backspace resets. - **Drag across checkboxes** to set them all. -- Press-drag-release menus, pie menus, and **Blender's fuzzy menu search**: "ad cu" finds "Add Cube", typos are forgiven, recent picks rank first. +- Press-drag-release menus, pie menus that pick on key release as in Blender (sub-pies, a "More" level, numpad directions, underlined letters), and **Blender's fuzzy menu search**: "ad cu" finds "Add Cube", typos are forgiven, recent picks rank first. - **Ctrl+Z / Ctrl+Shift+Z** on every UI edit, and inside a text field while you type; Tab between fields. - Blender's float display precision, multi-field vertical drags, and panels that collapse with Blender's animation. - The HSV color wheel, color ramps, curves, trees, lists, text areas, canvases, a node editor and Blender's File Browser. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 20dcb14..6353041 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -19,6 +19,7 @@ and their licenses. It is generated by `tools/gen_notices.py`: do not edit it by | NanoSVG, as edited by Blender | Rasterizing Blender's SVG icons (kit/extern/nanosvg) | zlib | https://github.com/memononen/nanosvg | | Slug reference shaders | GPU glyph coverage (gpu/intern/gpu_gl.c, gpu_d3d11.c) | MIT | https://github.com/EricLengyel/Slug | | Tailwind CSS v4 theme values | The default theme behind the Tailwind support (blenderui-tailwind) | MIT | https://tailwindcss.com | +| wlr-layer-shell-unstable-v1 (version 5) | Layer-shell windows on Wayland (kit/extern/wayland_protocols) | MIT | https://gitlab.freedesktop.org/wlroots/wlr-protocols | | Lua 5.4.7 | The Lua runtime embedded in bui-lua (bindings/lua, fetched from lua.org) | MIT | https://www.lua.org | | linenoise 1.0 | REPL line editing (bindings/lua/extern/linenoise) | BSD-2-Clause | https://github.com/antirez/linenoise | @@ -722,6 +723,37 @@ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ```` +### wlr-layer-shell-unstable-v1 (version 5) + +License: MIT + +````text +SPDX-License-Identifier: MIT + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. +```` + ### Lua 5.4.7 License: MIT diff --git a/bindings/imgui/c/cimgui_impl_bui.cpp b/bindings/imgui/c/cimgui_impl_bui.cpp index c0955e6..7ab2e1f 100644 --- a/bindings/imgui/c/cimgui_impl_bui.cpp +++ b/bindings/imgui/c/cimgui_impl_bui.cpp @@ -72,6 +72,7 @@ CIMGUI_API bool cImGui_ImplBUI_Init(BKNode* node, cImGui_ImplBUI_FrameFn frame, CIMGUI_API void cImGui_ImplBUI_Shutdown(void) { ImGui_ImplBUI_Shutdown(); } CIMGUI_API BKNode* cImGui_ImplBUI_GetNode(void) { return reinterpret_cast(ImGui_ImplBUI_GetNode()); } CIMGUI_API void cImGui_ImplBUI_RequestFrames(int n) { ImGui_ImplBUI_RequestFrames(n); } +CIMGUI_API void cImGui_ImplBUI_AbandonFrame(void) { ImGui_ImplBUI_AbandonFrame(); } CIMGUI_API void cImGui_ImplBUI_RequestFrameIn(int ms) { ImGui_ImplBUI_RequestFrameIn(ms); } CIMGUI_API int cImGui_ImplBUI_FrameCount(void) { return ImGui_ImplBUI_FrameCount(); } diff --git a/bindings/imgui/c/cimgui_impl_bui.h b/bindings/imgui/c/cimgui_impl_bui.h index 40ad977..5d5ae71 100644 --- a/bindings/imgui/c/cimgui_impl_bui.h +++ b/bindings/imgui/c/cimgui_impl_bui.h @@ -48,6 +48,7 @@ CIMGUI_API bool cImGui_ImplBUI_Init(BKNode* node, cImGui_ImplBUI_FrameFn fram CIMGUI_API void cImGui_ImplBUI_Shutdown(void); CIMGUI_API BKNode* cImGui_ImplBUI_GetNode(void); CIMGUI_API void cImGui_ImplBUI_RequestFrames(int n); +CIMGUI_API void cImGui_ImplBUI_AbandonFrame(void); /* a frame callback stopped on an error */ CIMGUI_API void cImGui_ImplBUI_RequestFrameIn(int ms); CIMGUI_API int cImGui_ImplBUI_FrameCount(void); diff --git a/bindings/imgui/imgui_impl_bui.cpp b/bindings/imgui/imgui_impl_bui.cpp index d36ee69..e3ae3e7 100644 --- a/bindings/imgui/imgui_impl_bui.cpp +++ b/bindings/imgui/imgui_impl_bui.cpp @@ -49,6 +49,7 @@ struct ImGui_ImplBUI_Data bool SuperDown = false; char* ClipboardText = nullptr; bool InFrame = false; + ImGuiErrorRecoveryState FrameState; // before the user's frame, for AbandonFrame }; static double now_seconds() @@ -520,6 +521,7 @@ static void Bui_Frame(void* data, BKNode* node) bd->InFrame = true; ImGui::NewFrame(); + ImGui::ErrorRecoveryStoreState(&bd->FrameState); if (bd->Frame) bd->Frame(bd->User); ImGui::Render(); @@ -676,6 +678,20 @@ bool ImGui_ImplBUI_Init(BKNode* node, ImGui_ImplBUI_FrameFn frame, void* user, c return true; } +void ImGui_ImplBUI_AbandonFrame() +{ + // Dear ImGui's recovery for scripting languages: unwind to the state before the user's frame, + // without the assert (the caller reports the error) + ImGui_ImplBUI_Data* bd = GetBackendData(); + if (bd == nullptr || !bd->InFrame) + return; + ImGuiIO& io = ImGui::GetIO(); + const bool assert_on = io.ConfigErrorRecoveryEnableAssert; + io.ConfigErrorRecoveryEnableAssert = false; + ImGui::ErrorRecoveryTryToRecoverState(&bd->FrameState); + io.ConfigErrorRecoveryEnableAssert = assert_on; +} + void ImGui_ImplBUI_Shutdown() { ImGui_ImplBUI_Data* bd = GetBackendData(); diff --git a/bindings/imgui/imgui_impl_bui.h b/bindings/imgui/imgui_impl_bui.h index ace6eda..9ef2b4c 100644 --- a/bindings/imgui/imgui_impl_bui.h +++ b/bindings/imgui/imgui_impl_bui.h @@ -79,6 +79,9 @@ IMGUI_IMPL_API BKNode* ImGui_ImplBUI_GetNode(); // Ask for frames: e.g. after the application changed state shown by the UI. n frames // back to back (paced to the display in a window), or one frame after `ms` milliseconds. IMGUI_IMPL_API void ImGui_ImplBUI_RequestFrames(int n = 1); +// Call from a frame callback that stops early on an error (an exception in a scripting +// language): closes what the frame opened (Begin() without End() ...) so the frame can end. +IMGUI_IMPL_API void ImGui_ImplBUI_AbandonFrame(); IMGUI_IMPL_API void ImGui_ImplBUI_RequestFrameIn(int ms); // Frames run since Init (tests, profiling) and whether the last frame changed the output. IMGUI_IMPL_API int ImGui_ImplBUI_FrameCount(); diff --git a/bindings/lua/src/bui_context.c b/bindings/lua/src/bui_context.c index d30b706..30da6ec 100644 --- a/bindings/lua/src/bui_context.c +++ b/bindings/lua/src/bui_context.c @@ -380,10 +380,8 @@ static int l_ctx_close_popups(lua_State *L) { return 0; } -static int l_ctx_focus(lua_State *L) { - luabui_Context *cud = CHECK_CTX(L, 1); - if (!cud->ctx) return 0; - BKNode *n = BK_focus(cud->ctx); +/* Push the node's userdata (made on first use), or nothing for NULL. */ +static int push_node(lua_State *L, BKNode *n) { if (!n) return 0; if (luabui_lookup_node(L, n)) return 1; luabui_Node *ud = (luabui_Node *)lua_newuserdata(L, sizeof(luabui_Node)); @@ -396,20 +394,23 @@ static int l_ctx_focus(lua_State *L) { return 1; } +static int l_ctx_focus(lua_State *L) { + luabui_Context *cud = CHECK_CTX(L, 1); + if (!cud->ctx) return 0; + return push_node(L, BK_focus(cud->ctx)); +} + static int l_ctx_hover(lua_State *L) { luabui_Context *cud = CHECK_CTX(L, 1); if (!cud->ctx) return 0; - BKNode *n = BK_hover(cud->ctx); - if (!n) return 0; - if (luabui_lookup_node(L, n)) return 1; - luabui_Node *ud = (luabui_Node *)lua_newuserdata(L, sizeof(luabui_Node)); - ud->node = n; - luaL_getmetatable(L, LUABUI_NODE_MT); - lua_setmetatable(L, -2); - lua_newtable(L); - lua_setuservalue(L, -2); - luabui_map_node(L, n, -1); - return 1; + return push_node(L, BK_hover(cud->ctx)); +} + +static int l_ctx_hit(lua_State *L) { + luabui_Context *cud = CHECK_CTX(L, 1); + if (!cud->ctx) return 0; + return push_node(L, BK_hit_at(cud->ctx, (int)luaL_checkinteger(L, 2), + (int)luaL_checkinteger(L, 3))); } static int l_ctx_cursor(lua_State *L) { @@ -591,7 +592,9 @@ static int l_ctx_open_pie(lua_State *L) { BKMenuSpec *specs; int n = luabui_build_menuspec(L, 2, &specs); const char *title = luaL_optstring(L, 3, NULL); - if (specs) BK_open_pie(cud->ctx, specs, n, title); + int ax = (int)luaL_optinteger(L, 4, -100000); + int ay = (int)luaL_optinteger(L, 5, -100000); + if (specs) BK_open_pie(cud->ctx, specs, n, title, ax, ay); return 0; } @@ -665,6 +668,7 @@ static const luaL_Reg ctx_methods[] = { {"focus", l_ctx_focus}, {"focus_node", l_ctx_focus_node}, {"hover", l_ctx_hover}, + {"hit", l_ctx_hit}, {"cursor", l_ctx_cursor}, {"set_pointer", l_ctx_set_pointer}, {"modal", l_ctx_modal}, diff --git a/bindings/lua/tests/test_context.lua b/bindings/lua/tests/test_context.lua index da7917a..36100dc 100644 --- a/bindings/lua/tests/test_context.lua +++ b/bindings/lua/tests/test_context.lua @@ -221,7 +221,7 @@ ctx:open_pie({ {text="South", icon=1}, {text="East", icon=2}, {text="West", icon=3}, -}, "Direction") +}, "Direction", 160, 100) -- Close popups after open_menu/open_pie ctx:close_popups() @@ -235,6 +235,12 @@ for i = 1, 3 do check(rh == 200, "render " .. i .. " height") end +-- ── Hit ─────────────────────────────────────────────────────────────── + +local bx, by, bw, bh = btn:rect() +assert(ctx:hit(bx + bw // 2, by + bh // 2) == btn, "hit over the button") +assert(ctx:hit(-5, -5) == nil, "hit outside the window") + -- ── Context tostring ───────────────────────────────────────────────── local s = tostring(ctx) diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index aa46202..0f61457 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -18,6 +18,26 @@ export interface WindowOptions { height?: number resizable?: boolean gpu?: boolean + /** Per-pixel alpha: only the elements show (no frame on Windows). */ + transparent?: boolean + /** Stay above other windows: Windows, macOS and X11. A Wayland toplevel cannot; use `layerShell`. */ + alwaysOnTop?: boolean + /** `false`: no frame (Electron's `frame`). */ + frame?: boolean + /** A wlr-layer-shell surface (Wayland compositors with layer shell: KDE Plasma, wlroots); + * a normal window elsewhere. */ + layerShell?: LayerShellOptions +} + +export interface LayerShellOptions { + /** Default `'overlay'`. */ + layer?: 'background' | 'bottom' | 'top' | 'overlay' + /** Edges the surface sticks to; both edges of an axis fill it. */ + anchor?: Array<'top' | 'bottom' | 'left' | 'right'> + /** Space the compositor keeps free for the surface (a panel), in pixels. */ + exclusiveZone?: number + /** Default `'none'`. */ + keyboard?: 'none' | 'exclusive' | 'on-demand' } export type EventType = 'click' | 'mousedown' | 'mouseup' | 'mousemove' | 'mouseenter' | 'mouseleave' | @@ -199,7 +219,8 @@ export class Window { screenshot (): Promise<{ width: number, height: number, data: Buffer }> click (x: number, y: number): void close (): void - on (event: 'close', listener: () => void): this + /** `focus` / `blur`: the window got or lost the keyboard focus. */ + on (event: 'close' | 'focus' | 'blur', listener: () => void): this } export class App { diff --git a/bindings/node/index.js b/bindings/node/index.js index 04327bd..f04aed1 100644 --- a/bindings/node/index.js +++ b/bindings/node/index.js @@ -41,6 +41,30 @@ const signals = require('./signals') const dom = require('./dom') const { OP, HTMLElement, TextNode, Comment, DocumentFragment, createElement } = dom +const LAYERS = ['background', 'bottom', 'top', 'overlay'] +const EDGES = ['top', 'bottom', 'left', 'right'] +const KEYBOARD = ['none', 'exclusive', 'on-demand'] + +function oneOf (what, value, names) { + if (value != null && !names.includes(value)) { + throw new TypeError(`${what}: '${value}' is not one of ${names.map((n) => `'${n}'`).join(', ')}`) + } +} + +/** The window options as the addon takes them; unknown layer-shell names throw here. */ +function windowConfig (o) { + const l = o.layerShell + if (l) { + oneOf('layerShell.layer', l.layer, LAYERS) + oneOf('layerShell.keyboard', l.keyboard, KEYBOARD) + for (const e of l.anchor ?? []) oneOf('layerShell.anchor', e, EDGES) + } + return { + title: o.title, width: o.width, height: o.height, resizable: o.resizable, gpu: o.gpu, + transparent: o.transparent, alwaysOnTop: o.alwaysOnTop, frame: o.frame, layerShell: l, + } +} + class Window { constructor (app, id, options = {}) { this.app = app @@ -52,7 +76,7 @@ class Window { this._strings = [] this._stringIds = new Map() this._scheduled = false - this._onClose = new Set() + this._on = { close: new Set(), focus: new Set(), blur: new Set() } this._frameCallbacks = new Map() this._nextFrameId = 1 this._framePending = false @@ -63,10 +87,7 @@ class Window { this.body.ownerWindow = this this.body.id = 0 this._nodes.set(0, this.body) - app.native.openWindow(id, { - title: options.title, width: options.width, height: options.height, - resizable: options.resizable, gpu: options.gpu, - }) + app.native.openWindow(id, windowConfig(options)) } /** An element. Capitalized tags are Blender's widgets (`Button`, `NumberSlider` ...). */ @@ -154,17 +175,22 @@ class Window { } on (event, fn) { - if (event !== 'close') throw new Error(`blenderui: unsupported window event "${event}"`) - this._onClose.add(fn) + const set = this._on[event] + if (!set) throw new Error(`blenderui: unsupported window event "${event}"`) + set.add(fn) return this } + _emit (event) { + for (const fn of this._on[event]) { + try { fn() } catch (err) { queueMicrotask(() => { throw err }) } + } + } + _closed () { if (this.closed) return this.closed = true - for (const fn of this._onClose) { - try { fn() } catch (err) { queueMicrotask(() => { throw err }) } - } + this._emit('close') // the native window is gone: free what follows signals or holds listeners for (const n of this._nodes.values()) { n._unbindAll() @@ -268,6 +294,10 @@ class App { win._frame() return } + if (raw.kind === 'focus' || raw.kind === 'blur') { + win._emit(raw.kind) + return + } win._nodes.get(raw.target)?._dispatch(raw) } diff --git a/bindings/node/test/native.test.js b/bindings/node/test/native.test.js index 09ea232..89e473a 100644 --- a/bindings/node/test/native.test.js +++ b/bindings/node/test/native.test.js @@ -178,3 +178,12 @@ test('document: detached nodes join a window when inserted; fragments, clones, c assert.strictEqual(win._nodes.size, size - 1, 'the dropped text node is freed; the moved button is not') assert.ok(win._nodes.has(btn.id)) }) + +test('window kinds: layer-shell names are checked', (t) => { + const app = new App({ headless: true }) + t.after(() => app.quit()) + assert.throws(() => app.window({ layerShell: { layer: 'middle' } }), /layerShell.layer: 'middle'/) + assert.throws(() => app.window({ layerShell: { anchor: ['up'] } }), /layerShell.anchor/) + assert.throws(() => app.window({ layerShell: { keyboard: 'all' } }), /layerShell.keyboard/) + app.window({ alwaysOnTop: true, frame: false, layerShell: { layer: 'top', anchor: ['top', 'left'], exclusiveZone: 24 } }) +}) diff --git a/bindings/python/bui/_surface.py b/bindings/python/bui/_surface.py index 8be5e08..c2d5cc3 100644 --- a/bindings/python/bui/_surface.py +++ b/bindings/python/bui/_surface.py @@ -154,6 +154,7 @@ def __init__(self, master, scene, width=None, height=None): self._overlay = None if buipy.has_native_text() else _TkTextOverlay(self.widget) self.widget.pack(fill="both", expand=True) + self._release = None # (after id, event) of a held-back key release w = self.widget w.bind("", self.on_press) w.bind("", self.on_release) @@ -304,6 +305,12 @@ def _raw_button(self, e, num, release): self.request() def _on_key_release(self, e): + # x11 autorepeat sends a release and a press at the same time: hold the release + # until idle, and drop it if that press comes (as kit_window_x11.c does). + self._release = (self.widget.after_idle(self._send_release, e), e) + + def _send_release(self, e): + self._release = None self.scene.key_release(e.keysym, Modifiers(e.state)) self.request() @@ -312,6 +319,10 @@ def on_motion(self, event) -> None: self.request() def on_key(self, event) -> None: + if self._release and self._release[1].keysym == event.keysym \ + and self._release[1].time == event.time: + self.widget.after_cancel(self._release[0]) + self._release = None self.scene.key(event.char, event.keysym, getattr(event, "keycode", 0), self._mods(event)) self.request() diff --git a/bindings/python/bui/components/menus.py b/bindings/python/bui/components/menus.py index 6eb7fb9..cc5aa8b 100644 --- a/bindings/python/bui/components/menus.py +++ b/bindings/python/bui/components/menus.py @@ -93,10 +93,13 @@ def show(ctx, owner, items, title: str = None, at=None, on_pick=None) -> None: class PieMenu: - """Radial menu around the pointer: ``PieMenu.show(ctx, items, "Mode")``. - Up to 8 items in Blender's order W, E, S, N, NW, NE, SW, SE; move toward - an item and click, or press 1-8.""" + """Radial menu around the pointer or ``at=(x, y)``: ``PieMenu.show(ctx, items, "Mode")``. + Items in Blender's order W, E, S, N, NW, NE, SW, SE; move toward + an item and click, press a number key in its numpad direction (8 north, 6 east), + or press its underlined letter. Opened by a held key, releasing the key picks + the item; a quick tap keeps the pie open, as in Blender. A ``submenu`` item + opens a sub-pie at the pointer; past 8 items, the 8th slot is "More".""" @staticmethod - def show(ctx, items, title: str = "") -> None: - ctx.open_pie(menu_items(items), title or None) + def show(ctx, items, title: str = "", at=None) -> None: + ctx.open_pie(menu_items(items), title or None, at) diff --git a/bindings/python/bui/host.py b/bindings/python/bui/host.py index 7a06a2e..0bf0eea 100644 --- a/bindings/python/bui/host.py +++ b/bindings/python/bui/host.py @@ -9,7 +9,7 @@ :class:`bui._surface.TkSurface` blitting the raster. Both give the same API: ``after``, ``after_cancel``, ``show``, ``close``, -``title``, ``size``, ``on_close``, ``run`` (one loop for all windows), +``title``, ``size``, ``on_close``, ``on_focus``, ``run`` (one loop for all windows), ``update`` (pump pending events). SPDX-License-Identifier: GPL-2.0-or-later @@ -111,6 +111,8 @@ def __init__(self, ctx, title: str = "BUI", size=(640, 480), native: bool = None self._title, self._size = title, tuple(size) self.native = native_available() if native is None else native self.on_close: Optional[Callable[[], object]] = None + # called with True when the window gets the keyboard focus, False when it loses it + self.on_focus: Optional[Callable[[bool], None]] = None self._closed = False self._win = None self._top = self._surface = None @@ -119,6 +121,7 @@ def __init__(self, ctx, title: str = "BUI", size=(640, 480), native: bool = None _tk_owns_nsapp() self._win = buipy.KitWindow(ctx, title, *self._size) self._win.on_close = self._close_requested + self._win.on_focus = self._focus if gpu: # None: buipy.set_gpu() / BUI_GPU decide self._win.gpu = True else: @@ -136,6 +139,8 @@ def _make_tk(self): top.configure(bg="#%02x%02x%02x" % tuple( int(round(v * 255)) for v in buipy.kit_theme_color("region_bg")[:3])) top.protocol("WM_DELETE_WINDOW", self._tk_close) + top.bind("", lambda e: e.widget is top and self._focus(True)) + top.bind("", lambda e: e.widget is top and self._focus(False)) top.withdraw() surface = TkSurface(top, _Scene(self.ctx), width=self._size[0], height=self._size[1]) surface.size_hint = self._size @@ -172,6 +177,10 @@ def _tk_close(self): if self._close_requested(): self.close() + def _focus(self, active: bool) -> None: + if self.on_focus is not None: + self.on_focus(active) + def _close_requested(self): if self.on_close is not None: return bool(self.on_close()) @@ -268,7 +277,9 @@ def pixel(self, x: int, y: int): def wm(self, key: str, a: float = 0.0, b: float = 0.0) -> None: """Window-manager request: pos, minsize, maxsize, resizable, topmost, alpha, override, state (0 normal, 1 iconic, 2 withdrawn, 3 zoomed), - lift, lower, focus.""" + lift, lower, focus, transparent (before show on Linux), layer + (layer 0-3 or -1, keyboard 0-2) and anchor (edges, exclusive zone) on Wayland + compositors with layer shell.""" if self.native: self._win.wm(key, float(a), float(b)) return diff --git a/bindings/python/src/buiimgui_module.cpp b/bindings/python/src/buiimgui_module.cpp index cb74013..0039d45 100644 --- a/bindings/python/src/buiimgui_module.cpp +++ b/bindings/python/src/buiimgui_module.cpp @@ -41,6 +41,7 @@ static void bi_app_frame(void* user) PyObject* r = PyObject_CallNoArgs(a->frame); if (r == nullptr) { + cimgui::cImGui_ImplBUI_AbandonFrame(); // the exception may leave Begin() without End() PyErr_Fetch(&a->et, &a->ev, &a->etb); cimgui::cImGui_ImplBUI_CloseApp(a->app); return; diff --git a/bindings/python/src/buipy_kit.c b/bindings/python/src/buipy_kit.c index 94bffd0..5f2d82b 100644 --- a/bindings/python/src/buipy_kit.c +++ b/bindings/python/src/buipy_kit.c @@ -1843,6 +1843,15 @@ static PyObject *kitctx_picker_part(KitContextObject *self, PyObject *args) return node_wrap(p ? BK_picker_part(p, name) : NULL); } +static PyObject *kitctx_hit(KitContextObject *self, PyObject *args) +{ + int x, y; + if (!PyArg_ParseTuple(args, "ii:hit", &x, &y)) { + return NULL; + } + return node_wrap(BK_hit_at(self->ctx, x, y)); +} + static PyObject *kitctx_open_menu(KitContextObject *self, PyObject *args, PyObject *kw) { static char *kwlist[] = {"owner", "items", "title", "at", NULL}; @@ -1872,11 +1881,17 @@ static PyObject *kitctx_open_menu(KitContextObject *self, PyObject *args, PyObje Py_RETURN_NONE; } -static PyObject *kitctx_open_pie(KitContextObject *self, PyObject *args) +static PyObject *kitctx_open_pie(KitContextObject *self, PyObject *args, PyObject *kw) { + static char *kwlist[] = {"items", "title", "at", NULL}; PyObject *items; const char *title = NULL; - if (!PyArg_ParseTuple(args, "O|z:open_pie", &items, &title)) { + PyObject *at = Py_None; + if (!PyArg_ParseTupleAndKeywords(args, kw, "O|zO:open_pie", kwlist, &items, &title, &at)) { + return NULL; + } + int ax = -100000, ay = -100000; + if (at != Py_None && !PyArg_ParseTuple(at, "ii", &ax, &ay)) { return NULL; } BKMenuSpec *specs; @@ -1884,7 +1899,7 @@ static PyObject *kitctx_open_pie(KitContextObject *self, PyObject *args) if (!build_specs(items, &specs, &n)) { return NULL; } - BK_open_pie(self->ctx, specs, n, title); + BK_open_pie(self->ctx, specs, n, title, ax, ay); free_specs(specs, n); Py_RETURN_NONE; } @@ -1974,9 +1989,11 @@ static PyMethodDef kitctx_methods[] = { {"popup_owner", (PyCFunction)kitctx_popup_owner, METH_O, ""}, {"menu_rows", (PyCFunction)kitctx_menu_rows, METH_O, "[(text, rect)] of menu popup i"}, {"picker_part", (PyCFunction)kitctx_picker_part, METH_VARARGS, "Color picker part."}, + {"hit", (PyCFunction)kitctx_hit, METH_VARARGS, "hit(x, y): the node under the point, or None"}, {"open_menu", (PyCFunction)(void (*)(void))kitctx_open_menu, METH_VARARGS | METH_KEYWORDS, "open_menu(owner, items, title=None, at=None)"}, - {"open_pie", (PyCFunction)kitctx_open_pie, METH_VARARGS, "open_pie(items, title=None)"}, + {"open_pie", (PyCFunction)(void (*)(void))kitctx_open_pie, METH_VARARGS | METH_KEYWORDS, + "open_pie(items, title=None, at=None)"}, {NULL, NULL, 0, NULL}, }; @@ -2015,6 +2032,16 @@ static PyObject *kit_set_scale(PyObject *m, PyObject *arg) Py_RETURN_NONE; } +static PyObject *kit_set_text_shadow(PyObject *m, PyObject *arg) +{ + const int on = PyObject_IsTrue(arg); + if (on < 0) { + return NULL; + } + BK_set_text_shadow(on); + Py_RETURN_NONE; +} + static PyObject *kit_scale(PyObject *m, PyObject *unused) { return PyFloat_FromDouble(BK_scale()); @@ -2338,6 +2365,8 @@ static PyMethodDef kit_functions[] = { {"image_set_budget", kit_image_set_budget, METH_O, "Image cache budget in bytes."}, {"kit_set_scale", kit_set_scale, METH_O, "UI scale (1.0 = 20 px widgets)."}, {"kit_scale", kit_scale, METH_NOARGS, ""}, + {"kit_set_text_shadow", kit_set_text_shadow, METH_O, + "Blender's UI text shadows (off in Python by default)."}, {"kit_unit", kit_unit, METH_NOARGS, "U.widget_unit in pixels."}, {"kit_set_font", kit_set_font, METH_O, "UI font family."}, {"kit_set_theme", kit_set_theme, METH_O, "Theme: 'dark' (default) or 'light' (Blender's presets)."}, @@ -2362,6 +2391,7 @@ typedef struct KitWindowObject { PyObject *ctx; /* KitContext (kept alive) */ PyObject *timers; /* id -> callable */ PyObject *on_close; /* callable -> bool, or None */ + PyObject *on_focus; /* callable(active: bool), or None */ } KitWindowObject; static int stop_check(void) @@ -2402,6 +2432,39 @@ static int win_on_close(void *data, BKWindow *w) return allow; } +static void win_on_focus(void *data, BKWindow *w, int active) +{ + KitWindowObject *self = (KitWindowObject *)data; + if (self->on_focus && self->on_focus != Py_None) { + PyObject *r = PyObject_CallOneArg(self->on_focus, active ? Py_True : Py_False); + if (!r) { + stash_error(); + } + Py_XDECREF(r); + } +} + +static void win_on_focus_gil(void *data, BKWindow *w, int active) +{ + KitWindowObject *self = (KitWindowObject *)data; + GIL_ENTER; + if (g_py_depth > 0) { /* queued behind the running callback, like the timers */ + PyObject *args = PyTuple_Pack(1, active ? Py_True : Py_False); + if (args) { + defer_call(self->on_focus, args); + } + Py_XDECREF(args); + PyErr_Clear(); + } + else { + g_py_depth++; + win_on_focus(data, w, active); + g_py_depth--; + run_deferred(); + } + GIL_LEAVE; +} + /* The timer's callable, taken off the table (one-shot) and queued behind the running callback. */ static void win_defer_timer(void *data, int id) { @@ -2433,16 +2496,39 @@ static void win_on_timer_gil(void *data, BKWindow *w, int id) GIL_LEAVE; } +/* The close that was queued behind a running callback: ask the handler now, close if it agrees. */ +static PyObject *win_deferred_close(PyObject *self, PyObject *unused) +{ + KitWindowObject *win = (KitWindowObject *)self; + if (win->win && BK_window_is_open(win->win) && win_on_close(win, win->win)) { + BK_window_close(win->win); + } + Py_RETURN_NONE; +} + +static PyMethodDef win_deferred_close_def = {"close", win_deferred_close, METH_NOARGS, NULL}; + +static void win_defer_close(void *data) +{ + PyObject *fn = PyCFunction_New(&win_deferred_close_def, (PyObject *)data); + defer_call(fn, NULL); + Py_XDECREF(fn); + PyErr_Clear(); +} + static int win_on_close_gil(void *data, BKWindow *w) { GIL_ENTER; - int allow = 0; /* the handler answers now: a close that arrives while a callback runs is refused */ + int allow = 0; /* a close that arrives while a callback runs is answered after it */ if (g_py_depth == 0) { g_py_depth++; allow = win_on_close(data, w); g_py_depth--; run_deferred(); } + else { + win_defer_close(data); + } GIL_LEAVE; return allow; } @@ -2471,7 +2557,9 @@ static int kitwin_init(KitWindowObject *self, PyObject *args, PyObject *kw) } Py_XSETREF(self->ctx, Py_NewRef(ctx)); Py_XSETREF(self->timers, PyDict_New()); - BKWindowCallbacks cb = {self, win_on_timer_gil, win_on_close_gil}; + BKWindowCallbacks cb = { + .data = self, .on_timer = win_on_timer_gil, .on_close = win_on_close_gil, + .on_focus = win_on_focus_gil}; BK_window_set_callbacks(self->win, &cb); return 0; } @@ -2486,6 +2574,7 @@ static void kitwin_dealloc(KitWindowObject *self) Py_CLEAR(self->ctx); Py_CLEAR(self->timers); Py_CLEAR(self->on_close); + Py_CLEAR(self->on_focus); Py_TYPE(self)->tp_free((PyObject *)self); } @@ -2494,6 +2583,7 @@ static int kitwin_traverse(KitWindowObject *self, visitproc visit, void *arg) Py_VISIT(self->ctx); Py_VISIT(self->timers); Py_VISIT(self->on_close); + Py_VISIT(self->on_focus); return 0; } @@ -2501,6 +2591,7 @@ static int kitwin_clear(KitWindowObject *self) { Py_CLEAR(self->timers); Py_CLEAR(self->on_close); + Py_CLEAR(self->on_focus); return 0; } @@ -2653,14 +2744,16 @@ static PyObject *kitwin_after_cancel(KitWindowObject *self, PyObject *arg) Py_RETURN_NONE; } -static PyObject *kitwin_get_on_close(KitWindowObject *self, void *c) +/* A callback attribute; `offset` is the field's offsetof in KitWindowObject. */ +static PyObject *kitwin_get_cb(KitWindowObject *self, void *offset) { - return Py_NewRef(self->on_close ? self->on_close : Py_None); + PyObject *v = *(PyObject **)((char *)self + (size_t)offset); + return Py_NewRef(v ? v : Py_None); } -static int kitwin_set_on_close(KitWindowObject *self, PyObject *v, void *c) +static int kitwin_set_cb(KitWindowObject *self, PyObject *v, void *offset) { - Py_XSETREF(self->on_close, Py_NewRef(v ? v : Py_None)); + Py_XSETREF(*(PyObject **)((char *)self + (size_t)offset), Py_NewRef(v ? v : Py_None)); return 0; } @@ -2672,9 +2765,9 @@ static PyMethodDef kitwin_methods[] = { {"set_size", (PyCFunction)kitwin_set_size, METH_VARARGS, "Logical size (DPI scaled)."}, {"wm", (PyCFunction)kitwin_wm, METH_VARARGS, "wm(key, a=0, b=0): pos, minsize, maxsize, resizable, topmost, alpha, override, state, " - "lift, lower, focus."}, + "lift, lower, focus, transparent (Linux, before show), layer, anchor (Wayland layer shell)."}, {"wm_get", (PyCFunction)kitwin_wm_get, METH_VARARGS, - "wm_get(key) -> (a, b) or None: pos, state, screen, pointer, scale."}, + "wm_get(key) -> (a, b) or None: pos, state, screen, pointer, scale, layer."}, {"render_now", (PyCFunction)kitwin_render_now, METH_NOARGS, "Render + present now."}, {"pixel", (PyCFunction)kitwin_pixel, METH_VARARGS, "Frame buffer pixel (r, g, b)."}, {"after", (PyCFunction)kitwin_after, METH_VARARGS, "after(ms, fn) -> id (one-shot)."}, @@ -2688,8 +2781,12 @@ static PyGetSetDef kitwin_getset[] = { {"gpu", (getter)kitwin_get_gpu, (setter)kitwin_set_gpu, "True once frames are drawn on the GPU; set True to switch this window to the GPU", NULL}, {"hwnd", (getter)kitwin_hwnd, NULL, "native handle", NULL}, - {"on_close", (getter)kitwin_get_on_close, (setter)kitwin_set_on_close, - "called on the close button; return True to close", NULL}, + {"on_close", (getter)kitwin_get_cb, (setter)kitwin_set_cb, + "called on the close button; return True to close", + (void *)offsetof(KitWindowObject, on_close)}, + {"on_focus", (getter)kitwin_get_cb, (setter)kitwin_set_cb, + "called with True when the window gets the keyboard focus, False when it loses it", + (void *)offsetof(KitWindowObject, on_focus)}, {NULL, NULL, NULL, NULL, NULL}, }; @@ -2755,6 +2852,7 @@ static PyMethodDef window_functions[] = { int buipy_kit_init(PyObject *module) { + BK_set_text_shadow(0); /* Python: crisp text without Blender's UI shadow */ if (PyType_Ready(&KitNode_Type) < 0 || PyType_Ready(&KitTreeItem_Type) < 0 || PyType_Ready(&KitContext_Type) < 0 || PyType_Ready(&KitWindow_Type) < 0) { diff --git a/bindings/python/tests/test_components.py b/bindings/python/tests/test_components.py index 4b31504..bc75370 100644 --- a/bindings/python/tests/test_components.py +++ b/bindings/python/tests/test_components.py @@ -487,6 +487,151 @@ def test_pie_menu(): assert got == ["N"] and not d.popups() +def _pie_on_q(got): + from bui.components import MenuItem, PieMenu + d = Driver(Label("x"), size=(600, 500)) + d.ctx.set_pointer(300, 250) + d.app.bind("q", lambda: PieMenu.show(d.ctx, [ + MenuItem(n, (lambda n=n: got.append(n))) for n in "WESN"])) + d.app.key("q", "q", 0, mods()) + d.frame() + return d + + +def test_pie_hold_release(): + import time + got = [] + d = _pie_on_q(got) + d.app.key("q", "q", 0, mods()) # auto-repeat is ignored + d.app.motion(301, 249, mods()) # still inside the click threshold + time.sleep(0.25) # past pie_tap_timeout + d.app.motion(300, 120, mods()) + d.ctx.key_release("q") + assert got == ["N"] and not d.popups() + + +def test_pie_drag_release(): + got = [] + d = _pie_on_q(got) + d.app.motion(430, 250, mods()) # a drag: release picks at once + d.ctx.key_release("q") + assert got == ["E"] and not d.popups() + + +def test_pie_tap_stays_open(): + got = [] + d = _pie_on_q(got) + d.ctx.key_release("q") # a quick tap: click style + assert d.popups() == 1 and not got + d.app.motion(170, 250, mods()) + d.app.press(170, 250, mods()) + assert got == ["W"] and not d.popups() + + +def test_pie_sub_menu(): + from bui.components import MenuItem, PieMenu + got = [] + d = Driver(Label("x"), size=(600, 500)) + d.ctx.set_pointer(300, 250) + leaf = [MenuItem(n, (lambda n=n: got.append(n))) for n in "wesn"] + PieMenu.show(d.ctx, [MenuItem("W"), MenuItem("Sub", submenu=leaf)]) + d.frame() + d.app.motion(430, 250, mods()) # east: "Sub" + d.app.press(430, 250, mods()) + assert d.popups() == 1 and not got # the sub-pie, centred at the pointer + d.frame() + d.app.motion(430, 120, mods()) # north of the new centre + d.app.press(430, 120, mods()) + assert got == ["n"] and not d.popups() + + +def test_pie_more_level(): + from bui.components import MenuItem, PieMenu + got = [] + d = Driver(Label("x"), size=(600, 500)) + d.ctx.set_pointer(300, 250) + PieMenu.show(d.ctx, [MenuItem(str(i), (lambda i=i: got.append(i))) for i in range(10)]) + d.frame() + d.app.key("3", "3", 0, mods()) # numpad SE: the 8th slot, "More" + d.frame() + assert d.popups() == 1 and not got + d.app.key("2", "2", 0, mods()) # items 7, 8, 9 at W, E, S: S is 9 + assert got == [9] and not d.popups() + + +def test_pie_number_keys(): + from bui.components import MenuItem, PieMenu + got = [] + d = Driver(Label("x"), size=(600, 500)) + PieMenu.show(d.ctx, [MenuItem(n, (lambda n=n: got.append(n))) for n in "WESN"], at=(300, 250)) + d.frame() + d.app.key("5", "5", 0, mods()) # the centre: no item + d.app.key("1", "1", 0, mods()) # SW: no item there + assert d.popups() == 1 + d.app.key("8", "8", 0, mods()) # numpad 8 is north + assert got == ["N"] and not d.popups() + + +def test_pie_accelerator_keys(): + from bui.components import MenuItem, PieMenu + got = [] + d = Driver(Label("x"), size=(600, 500)) + names = ["Object Mode", "Edit Mode", "Sculpt", "Open Shading"] + PieMenu.show(d.ctx, [MenuItem(n, (lambda n=n: got.append(n))) for n in names], at=(300, 250)) + d.frame() + # first letters of words: o, e, s; both of "Open Shading"'s are taken, so the second pass + # gives it its first free letter, p + d.app.key("m", "m", 0, mods()) # no item has m + assert d.popups() == 1 + d.app.key("p", "p", 0, mods()) + assert got == ["Open Shading"] and not d.popups() + + +def test_pie_open_animation(): + import time + from bui.components import MenuItem, PieMenu + + def east_x(d): + d.frame() + x, y, w, h = dict(d.ctx.menu_rows(-1))["E"] + return x + w / 2 + + d = Driver(Label("x"), size=(600, 500)) + buipy.kit_set_animations(True) + try: + PieMenu.show(d.ctx, [MenuItem(n) for n in "WESN"], at=(300, 250)) + start = east_x(d) # items start at the center + time.sleep(0.1) # past pie_animation_timeout + end = east_x(d) + assert start < 330 < 380 < end, (start, end) + finally: + buipy.kit_set_animations(False) + d.ctx.close_popups() + PieMenu.show(d.ctx, [MenuItem(n) for n in "WESN"], at=(300, 250)) + assert east_x(d) == end # reduced motion: no animation + + +def test_pie_menu_at(): + from bui.components import MenuItem, PieMenu + got = [] + d = Driver(Label("x"), size=(600, 500)) + d.ctx.set_pointer(10, 10) + PieMenu.show(d.ctx, [MenuItem(n, (lambda n=n: got.append(n))) for n in "WESN"], at=(300, 250)) + d.frame() + d.app.motion(430, 250, mods()) # east of the given centre + d.app.press(430, 250, mods()) + assert got == ["E"] and not d.popups() + + +def test_hit(): + from bui.components import Button + btn = Button("x") + d = Driver(btn, size=(300, 200)) + x, y, w, h = btn.rect + assert d.ctx.hit(x + w // 2, y + h // 2) is btn + assert d.ctx.hit(-5, -5) is None + + # -- color ------------------------------------------------------------------------------ diff --git a/bindings/python/tests/test_tk_input.py b/bindings/python/tests/test_tk_input.py index 33b6c24..43371c3 100644 --- a/bindings/python/tests/test_tk_input.py +++ b/bindings/python/tests/test_tk_input.py @@ -219,6 +219,31 @@ def test_panedwindow_sash_drag(): app.close() +def test_surface_drops_autorepeat_release(): + # x11 autorepeat: a release and a press at one time must not reach the scene + import tkinter + from types import SimpleNamespace as Ev + from bui._surface import TkSurface + + class Scene: + released = [] + def render(self, w, h): return 0 + def key(self, *a): pass + def key_release(self, keysym, mods): self.released.append(keysym) + + root = tkinter.Tk() + surf = TkSurface(root, Scene()) + scene = surf.scene + surf._on_key_release(Ev(keysym="q", time=5, state=0)) + surf.on_key(Ev(char="q", keysym="q", keycode=0, time=5, state=0)) + root.update() + assert scene.released == [], scene.released + surf._on_key_release(Ev(keysym="q", time=9, state=0)) + root.update() + assert scene.released == ["q"], scene.released + root.destroy() + + def main() -> int: for name, fn in sorted(globals().items()): if name.startswith("test_"): diff --git a/bindings/rust/blenderui-jsview/src/lib.rs b/bindings/rust/blenderui-jsview/src/lib.rs index 123a818..2f15924 100644 --- a/bindings/rust/blenderui-jsview/src/lib.rs +++ b/bindings/rust/blenderui-jsview/src/lib.rs @@ -51,6 +51,58 @@ pub struct WindowConfig { pub height: Option, pub resizable: Option, pub gpu: Option, + /// Per-pixel alpha, only the elements show. + pub transparent: Option, + /// Above other windows. + pub always_on_top: Option, + /// `false`: no frame. + pub frame: Option, + pub layer_shell: Option, +} + +/// A layer-shell surface as JavaScript asks for it: the names of `index.d.ts`, checked there. +#[derive(Clone, Debug, Default)] +pub struct LayerShellConfig { + pub layer: Option, + pub anchor: Option>, + pub exclusive_zone: Option, + pub keyboard: Option, +} + +impl From<&LayerShellConfig> for LayerShellOptions { + fn from(c: &LayerShellConfig) -> Self { + let edge = |name: &str| match name { + "top" => Anchor::TOP, + "bottom" => Anchor::BOTTOM, + "left" => Anchor::LEFT, + "right" => Anchor::RIGHT, + _ => Anchor::default(), + }; + LayerShellOptions { + layer: match c.layer.as_deref() { + Some("background") => Layer::Background, + Some("bottom") => Layer::Bottom, + Some("top") => Layer::Top, + _ => Layer::Overlay, + }, + anchor: c.anchor.iter().flatten().fold(Anchor::default(), |a, e| a | edge(e)), + exclusive_zone: c.exclusive_zone.map(|z| px(z as f32)), + keyboard_interactivity: match c.keyboard.as_deref() { + Some("exclusive") => KeyboardInteractivity::Exclusive, + Some("on-demand") => KeyboardInteractivity::OnDemand, + _ => KeyboardInteractivity::None, + }, + } + } +} + +/// The window kind: a layer surface wins over `alwaysOnTop`. +fn window_kind(config: &WindowConfig) -> WindowKind { + match (&config.layer_shell, config.always_on_top) { + (Some(l), _) => WindowKind::LayerShell(l.into()), + (None, Some(true)) => WindowKind::Floating, + (None, _) => WindowKind::Normal, + } } /// A layout box, logical pixels. @@ -415,24 +467,30 @@ impl JsView { // ========================================================================== // Window operations (UI thread) -/// Set up the UI-thread state; `sink` also gets `close` events. +/// Set up the UI-thread state; `sink` also gets `close`, `focus` and `blur` events. pub fn init(cx: &mut App, sink: Sink) { cx.set_global(UiState::default()); - cx.set_global(CloseSink(sink)); + cx.set_global(WindowSink(sink)); + cx.on_window_activation(|id, active, cx| { + let Some(w) = cx.global::().by_native.get(&id.0).copied() else { return }; + let kind = if active { "focus" } else { "blur" }; + cx.global::().0(Event { kind: kind.into(), window: w, ..Default::default() }); + }); cx.on_window_closed(|id, cx| { let Some(w) = cx.global::().by_native.get(&id.0).copied() else { return }; let st = cx.global_mut::(); st.windows.remove(&w); st.by_native.remove(&id.0); - let sink = cx.global::().0.clone(); + let sink = cx.global::().0.clone(); sink(Event { kind: "close".into(), window: w, ..Default::default() }); }); } -struct CloseSink(Sink); +struct WindowSink(Sink); /// Open window `id` (chosen by JavaScript); its events go to `sink`. pub fn open_window(cx: &mut App, id: u32, config: WindowConfig, sink: Sink) { + let kind = window_kind(&config); let opts = blenderui::WindowOptions { window_bounds: Some(WindowBounds::Windowed(Bounds { origin: point(px(0.0), px(0.0)), @@ -441,6 +499,17 @@ pub fn open_window(cx: &mut App, id: u32, config: WindowConfig, sink: Sink) { titlebar: config.title.map(|t| TitlebarOptions { title: Some(t.into()) }), is_resizable: config.resizable, gpu: config.gpu, + window_background: if config.transparent == Some(true) { + WindowBackgroundAppearance::Transparent + } else { + WindowBackgroundAppearance::Opaque + }, + kind, + window_decorations: if config.frame == Some(false) { + WindowDecorations::Client + } else { + WindowDecorations::Server + }, ..Default::default() }; let handle = cx.open_window(opts, |_, cx| cx.new(|_| JsView::new(id, sink.clone()))); diff --git a/bindings/rust/blenderui-node/src/lib.rs b/bindings/rust/blenderui-node/src/lib.rs index a06c08b..ee93c78 100644 --- a/bindings/rust/blenderui-node/src/lib.rs +++ b/bindings/rust/blenderui-node/src/lib.rs @@ -83,6 +83,19 @@ pub struct WindowConfig { pub height: Option, pub resizable: Option, pub gpu: Option, + pub transparent: Option, + pub always_on_top: Option, + pub frame: Option, + pub layer_shell: Option, +} + +#[napi(object)] +#[derive(Clone)] +pub struct LayerShellConfig { + pub layer: Option, + pub anchor: Option>, + pub exclusive_zone: Option, + pub keyboard: Option, } #[napi(object)] @@ -222,6 +235,10 @@ impl NativeApp { pub fn open_window(&self, id: u32, config: Option) { let c = config.map(|c| view::WindowConfig { title: c.title, width: c.width, height: c.height, resizable: c.resizable, gpu: c.gpu, + transparent: c.transparent, always_on_top: c.always_on_top, frame: c.frame, + layer_shell: c.layer_shell.map(|l| view::LayerShellConfig { + layer: l.layer, anchor: l.anchor, exclusive_zone: l.exclusive_zone, keyboard: l.keyboard, + }), }).unwrap_or_default(); self.link.send(move |cx| { let s = sink(cx.global::()); diff --git a/bindings/rust/blenderui-qjs/src/main.rs b/bindings/rust/blenderui-qjs/src/main.rs index cd494da..2a1ed5c 100644 --- a/bindings/rust/blenderui-qjs/src/main.rs +++ b/bindings/rust/blenderui-qjs/src/main.rs @@ -356,6 +356,32 @@ fn describe(v: &Value<'_>) -> String { // ========================================================================== // Host functions (`host` in prelude.js) +/// The window options object of `index.d.ts`. +fn window_config(c: &Object<'_>) -> rquickjs::Result { + let layer_shell = c + .get::<_, Option>("layerShell")? + .map(|l| -> rquickjs::Result { + Ok(view::LayerShellConfig { + layer: l.get("layer")?, + anchor: l.get("anchor")?, + exclusive_zone: l.get("exclusiveZone")?, + keyboard: l.get("keyboard")?, + }) + }) + .transpose()?; + Ok(view::WindowConfig { + title: c.get("title")?, + width: c.get("width")?, + height: c.get("height")?, + resizable: c.get("resizable")?, + gpu: c.get("gpu")?, + transparent: c.get("transparent")?, + always_on_top: c.get("alwaysOnTop")?, + frame: c.get("frame")?, + layer_shell, + }) +} + fn host_object<'js>(ctx: &Ctx<'js>, argv: Vec) -> rquickjs::Result> { let h = Object::new(ctx.clone())?; macro_rules! f { @@ -397,8 +423,9 @@ fn host_object<'js>(ctx: &Ctx<'js>, argv: Vec) -> rquickjs::Result, width: f64, height: f64, resizable: Option, gpu: Option| { - command(Command::Open(id, view::WindowConfig { title, width: Some(width), height: Some(height), resizable, gpu })); + f!("openWindow", |id: u32, c: Object<'js>| -> rquickjs::Result<()> { + command(Command::Open(id, window_config(&c)?)); + Ok(()) }); f!("commit", |w: u32, ops: TypedArray<'js, i32>, strings: Vec, animated: bool| { // SAFETY: copied right away, before JavaScript can detach or resize the buffer diff --git a/bindings/rust/blenderui-qjs/src/prelude.js b/bindings/rust/blenderui-qjs/src/prelude.js index 83be785..94a1e27 100644 --- a/bindings/rust/blenderui-qjs/src/prelude.js +++ b/bindings/rust/blenderui-qjs/src/prelude.js @@ -111,8 +111,8 @@ get headless () { return host.headless() } openWindow (id, c) { c = c || {} - host.openWindow(id, c.title ?? null, c.width ?? 800, c.height ?? 600, c.resizable ?? null, - c.gpu ?? this._options.gpu ?? null) + host.openWindow(id, { ...c, width: c.width ?? 800, height: c.height ?? 600, + gpu: c.gpu ?? this._options.gpu }) } commit (window, ops, strings, animated) { host.commit(window, ops, strings, !!animated) } diff --git a/bindings/rust/blenderui-sys/src/lib.rs b/bindings/rust/blenderui-sys/src/lib.rs index 4a4c43e..2c74104 100644 --- a/bindings/rust/blenderui-sys/src/lib.rs +++ b/bindings/rust/blenderui-sys/src/lib.rs @@ -215,6 +215,7 @@ pub struct BKWindowCallbacks { pub on_timer: Option, pub on_close: Option c_int>, pub on_frame: Option, + pub on_focus: Option, } unsafe extern "C" { diff --git a/bindings/rust/blenderui/src/lib.rs b/bindings/rust/blenderui/src/lib.rs index 6312c63..b060446 100644 --- a/bindings/rust/blenderui/src/lib.rs +++ b/bindings/rust/blenderui/src/lib.rs @@ -124,6 +124,7 @@ pub mod styled; pub(crate) mod text; pub(crate) mod widgets; pub mod window; +pub mod window_kind; pub use blenderui_sys as sys; @@ -136,6 +137,7 @@ pub use paint::{image_stats, set_image_budget, Animation, Image, Painter}; pub use video::{Video, VideoState}; pub use styled::{FontWeight, StyleBlock, Styled}; pub use window::*; +pub use window_kind::{Anchor, KeyboardInteractivity, Layer, LayerShellOptions, WindowDecorations, WindowKind}; /// Whether `class` names also work as Tailwind CSS v4 utilities (`p-4`, /// `md:hover:bg-sky-500/50` ...), compiled on first use. On by default; @@ -243,4 +245,5 @@ pub mod prelude { pub use crate::paint::{Image, Painter}; pub use crate::styled::{FontWeight, StyleBlock, Styled}; pub use crate::window::*; + pub use crate::window_kind::{Anchor, KeyboardInteractivity, Layer, LayerShellOptions, WindowDecorations, WindowKind}; } diff --git a/bindings/rust/blenderui/src/window.rs b/bindings/rust/blenderui/src/window.rs index a59814b..0795289 100644 --- a/bindings/rust/blenderui/src/window.rs +++ b/bindings/rust/blenderui/src/window.rs @@ -18,6 +18,7 @@ use crate::events::*; use crate::geometry::{point, px, size, Bounds, Pixels, Size}; use crate::layout::{id_key, layout, Content, Frame, Key, LayoutCaches, UiState}; use crate::prof::{self, span, Phase}; +use crate::window_kind::{WindowDecorations, WindowKind}; use crate::paint::{paint_frame, Painter}; use crate::widgets::{WKind, Widgets}; use blenderui_sys as sys; @@ -203,6 +204,15 @@ pub enum WindowBounds { Fullscreen(Bounds), } +/// How the window's background shows (gpui's `WindowBackgroundAppearance`). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum WindowBackgroundAppearance { + #[default] + Opaque, + /// Per-pixel alpha: only the elements show. On Windows the window has no frame. + Transparent, +} + #[derive(Clone, Debug, Default)] pub struct TitlebarOptions { pub title: Option, @@ -218,6 +228,9 @@ pub struct WindowOptions { /// Draw this window on the GPU (Direct3D 11 / OpenGL 3.3) instead of the software /// raster; `None` follows [`Application::gpu`] / the `BUI_GPU` variable. pub gpu: Option, + pub window_background: WindowBackgroundAppearance, + pub kind: WindowKind, + pub window_decorations: WindowDecorations, } /// The application entry point (gpui's `Application`). @@ -364,6 +377,8 @@ pub struct App { remote: Option<(std::sync::mpsc::Sender, std::sync::mpsc::Receiver)>, /// Called when the user closes a window. close_hooks: Vec>, + /// Called when a window gets or loses the keyboard focus. + activation_hooks: Vec>, /// Counts `notify_all` calls (see `WindowHandle::update`). notified: u64, } @@ -401,7 +416,8 @@ unsafe extern "C" fn on_wake() { impl App { fn create(headless: bool) -> App { App { windows: Vec::new(), entities: Vec::new(), headless, globals: HashMap::new(), propagate: true, - remote: None, close_hooks: Vec::new(), notified: 0 } + remote: None, close_hooks: Vec::new(), + activation_hooks: Vec::new(), notified: 0 } } /// Create an entity (gpui's `cx.new`). @@ -449,6 +465,12 @@ impl App { sys::BK_window_wm(*n, c"minsize".as_ptr(), (m.width.0 * w.scale) as f64, (m.height.0 * w.scale) as f64); } + if options.window_background == WindowBackgroundAppearance::Transparent { + sys::BK_window_wm(*n, c"transparent".as_ptr(), 1.0, 0.0); + } + for (key, a, b) in options.kind.requests(options.window_decorations) { + sys::BK_window_wm(*n, key.as_ptr(), a, b); + } if options.gpu == Some(true) { sys::BK_window_set_gpu(*n, 1); } @@ -502,6 +524,12 @@ impl App { self.close_hooks.push(Box::new(f)); } + /// Call `f` with `true` when a window gets the keyboard focus and `false` when it + /// loses it (gpui's `observe_window_activation`). + pub fn on_window_activation(&mut self, f: impl FnMut(WindowId, bool, &mut App) + 'static) { + self.activation_hooks.push(Box::new(f)); + } + /// Close a window (as if the user had). pub fn close_window(&mut self, id: WindowId) { if let Some(w) = self.windows.get_mut(id.0).and_then(|w| w.as_deref_mut()) { @@ -815,16 +843,27 @@ unsafe extern "C" fn on_close(data: *mut c_void, _win: *mut sys::BKWindow) -> c_ if let Some(w) = app.windows.get_mut(id).and_then(|w| w.as_deref_mut()) { w.closed = true; } - let mut hooks = std::mem::take(&mut app.close_hooks); - for h in hooks.iter_mut() { - h(WindowId(id), app); - } - hooks.append(&mut app.close_hooks); - app.close_hooks = hooks; + run_hooks(app, |a| &mut a.close_hooks, |h, app| h(WindowId(id), app)); }); 1 } +unsafe extern "C" fn on_focus(data: *mut c_void, _win: *mut sys::BKWindow, active: c_int) { + let id = data as usize; + with_running_app(|app| { + run_hooks(app, |a| &mut a.activation_hooks, |h, app| h(WindowId(id), active != 0, app)); + }); +} + +/// Call each hook; hooks a hook adds run from the next call. +fn run_hooks(app: &mut App, list: fn(&mut App) -> &mut Vec>, + mut call: impl FnMut(&mut H, &mut App)) { + let mut hooks = std::mem::take(list(app)); + hooks.iter_mut().for_each(|h| call(h, app)); + hooks.append(list(app)); + *list(app) = hooks; +} + /// Run `f` with the app, unless there is none (before the loop, headless) /// or a callback further up the stack already holds it. fn with_running_app(f: impl FnOnce(&mut App)) -> bool { @@ -947,7 +986,8 @@ impl Window { return Err("could not create a native window".into()); } let cb = sys::BKWindowCallbacks { data: id as *mut c_void, on_timer: Some(on_timer), - on_close: Some(on_close), on_frame: Some(on_frame) }; + on_close: Some(on_close), on_frame: Some(on_frame), + on_focus: Some(on_focus) }; sys::BK_window_set_callbacks(n, &cb); win.scale = sys::BK_scale().max(0.5); win.phys = ((w * win.scale).round() as i32, (h * win.scale).round() as i32); diff --git a/bindings/rust/blenderui/src/window_kind.rs b/bindings/rust/blenderui/src/window_kind.rs new file mode 100644 index 0000000..3e8d425 --- /dev/null +++ b/bindings/rust/blenderui/src/window_kind.rs @@ -0,0 +1,162 @@ +//! Window kinds, decorations and layer-shell options (gpui's `WindowKind`, +//! `WindowDecorations` and `layer_shell`), sent to the host as window requests. + +use std::ffi::CStr; +use std::ops::BitOr; + +use crate::geometry::Pixels; + +/// What a window is for (gpui's `WindowKind`). +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum WindowKind { + #[default] + Normal, + /// No frame, above other windows. + PopUp, + /// Above other windows ("topmost"). A Wayland toplevel cannot ask for this: + /// use [`WindowKind::LayerShell`] there. + Floating, + /// A wlr-layer-shell surface (Wayland compositors with layer shell: KDE Plasma, + /// wlroots). Elsewhere it is a normal window. + LayerShell(LayerShellOptions), +} + +/// Who draws the window frame (gpui's `WindowDecorations`). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum WindowDecorations { + /// The system's frame. + #[default] + Server, + /// No frame: the app draws everything. + Client, +} + +/// A layer-shell layer, bottom-most first. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Layer { + Background = 0, + Bottom = 1, + Top = 2, + #[default] + Overlay = 3, +} + +/// Whether a layer surface takes the keyboard. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum KeyboardInteractivity { + #[default] + None = 0, + Exclusive = 1, + OnDemand = 2, +} + +/// Screen edges a layer surface sticks to: `Anchor::TOP | Anchor::LEFT`. Anchored to both +/// edges of an axis, the surface fills that axis. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Anchor(u8); + +impl Anchor { + pub const TOP: Anchor = Anchor(1); + pub const BOTTOM: Anchor = Anchor(2); + pub const LEFT: Anchor = Anchor(4); + pub const RIGHT: Anchor = Anchor(8); +} + +impl Anchor { + /// The edges as the host's bit mask. + pub(crate) const fn bits(self) -> u8 { + self.0 + } +} + +impl BitOr for Anchor { + type Output = Anchor; + + fn bitor(self, rhs: Anchor) -> Anchor { + Anchor(self.0 | rhs.0) + } +} + +/// gpui's `LayerShellOptions`. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct LayerShellOptions { + pub layer: Layer, + pub anchor: Anchor, + /// Space the compositor keeps free for the surface (a panel); `None`: none. + pub exclusive_zone: Option, + pub keyboard_interactivity: KeyboardInteractivity, +} + +/// A window request: key, then the two numbers `BK_window_wm` takes. +pub(crate) type Request = (&'static CStr, f64, f64); + +impl WindowKind { + /// The window requests that make a new window of this kind and decoration. + pub(crate) fn requests(self, decorations: WindowDecorations) -> impl Iterator { + let (first, second) = match self { + WindowKind::Normal => (None, None), + WindowKind::PopUp | WindowKind::Floating => (Some((c"topmost", 1.0, 0.0)), None), + WindowKind::LayerShell(o) => ( + Some(( + c"layer", + f64::from(o.layer as u8), + f64::from(o.keyboard_interactivity as u8), + )), + Some(( + c"anchor", + f64::from(o.anchor.bits()), + f64::from(o.exclusive_zone.map_or(0.0, |z| z.0)), + )), + ), + }; + let frameless = decorations == WindowDecorations::Client || self == WindowKind::PopUp; + [first, second, frameless.then_some((c"override", 1.0, 0.0))] + .into_iter() + .flatten() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::geometry::px; + + fn requests(kind: WindowKind, decorations: WindowDecorations) -> Vec { + kind.requests(decorations).collect() + } + + #[test] + fn normal_window_sends_nothing() { + assert!(requests(WindowKind::Normal, WindowDecorations::Server).is_empty()); + } + + #[test] + fn floating_is_topmost_and_pop_up_has_no_frame() { + assert_eq!( + requests(WindowKind::Floating, WindowDecorations::Server), + [(c"topmost", 1.0, 0.0)] + ); + assert_eq!( + requests(WindowKind::PopUp, WindowDecorations::Server), + [(c"topmost", 1.0, 0.0), (c"override", 1.0, 0.0)] + ); + assert_eq!( + requests(WindowKind::Normal, WindowDecorations::Client), + [(c"override", 1.0, 0.0)] + ); + } + + #[test] + fn layer_shell_sends_layer_then_anchor() { + let kind = WindowKind::LayerShell(LayerShellOptions { + layer: Layer::Top, + anchor: Anchor::TOP | Anchor::LEFT, + exclusive_zone: Some(px(24.0)), + keyboard_interactivity: KeyboardInteractivity::OnDemand, + }); + assert_eq!( + requests(kind, WindowDecorations::Server), + [(c"layer", 2.0, 2.0), (c"anchor", 5.0, 24.0)] + ); + } +} diff --git a/docs/c-api.md b/docs/c-api.md index 64e5011..f99234c 100644 --- a/docs/c-api.md +++ b/docs/c-api.md @@ -80,11 +80,12 @@ int main(void) | **Context** | `BK_context_new(root)`: input state, popups, focus, undo. Input goes in with `BK_press`, `BK_release`, `BK_motion`, `BK_wheel`, `BK_key`, `BK_key_release`, `BK_button`, `BK_context_menu` (input methods: `BK_ime_event`, `BK_ime_state`, see below); frames come out of `BK_render(ctx, raster, w, h, full)`, which returns the number of redrawn regions (0 means nothing changed). | | **Host** | `BKHost`: `request` (schedule a render), `set_cursor`, `warp`, clipboard, `schedule` (timers), `emit` (events). A `BKWindow` provides all of them. | | **Events** | `emit(data, node, kind, ref, a, b)`. Kinds: `BK_EV_CHANGE`, `CLICK`, `CALL` (menu item or hotkey), `RENAME`, `ACTIVATE`, `TOGGLE`, `BROWSE`, `SUBMIT`. With the `"events"` property you also get raw `PRESS` / `RELEASE` / `DOUBLE` / `MOTION` / `ENTER` / `LEAVE` / `KEY` / `KEYRELEASE` / `WHEEL` / `CONFIGURE`, plus `VIEW` and `SCROLL` for scrolling. | -| **Windows** | `BK_window_new`, `show`, `close`, `set_title`, `set_size`, `wm(key, a, b)` (position, topmost, alpha, resizable, minsize, state ...), `timer`, `run`, `pump`, `quit` | +| **Windows** | `BK_window_new`, `show`, `close`, `set_title`, `set_size`, `wm(key, a, b)` (position, topmost, alpha, override, transparent, resizable, minsize, state; on Wayland also layer and anchor for wlr-layer-shell), callbacks `on_timer`, `on_close`, `on_frame`, `on_focus`, `timer`, `run`, `pump`, `quit` | | **GPU rendering** | `BUI_gpu_set_default(1)` (all new windows), `BK_window_set_gpu(win, 1)` (one window), `BK_window_gpu(win)` (1 once frames draw on the GPU). `BUI_GPU=1/0` overrides. For your own rasters: `BUI_raster_gpu_attach(r, hwnd_or_NULL, wait)`, `BUI_raster_gpu_poll`, `BUI_raster_is_gpu`. | -| **Theme** | `BK_set_theme("dark" / "light")` (Blender's Dark or Light preset; every context relayouts and redraws), `BK_theme_name()`, `BK_theme_color(key, rgba)` (`"region_bg"`, `"text"`, `"regular.inner"` ...). Without a call, `BUI_THEME=dark|light` picks the theme. `BK_set_scale` sets the UI scale. | +| **Theme** | `BK_set_theme("dark" / "light")` (Blender's Dark or Light preset; every context relayouts and redraws), `BK_theme_name()`, `BK_theme_color(key, rgba)` (`"region_bg"`, `"text"`, `"regular.inner"` ...). Without a call, `BUI_THEME=dark|light` picks the theme. `BK_set_scale` sets the UI scale; `BK_set_text_shadow(0)` turns off Blender's UI text shadows (on by default; Python turns them off). | | **Text** | `BUI_text_measure`, `BUI_raster_text`, `BUI_text_add_font(family, flags, data, len)` (register a font; needed on wasm) | -| **Specialised** | Canvas display list (`BK_canvas_set_item`), text area (`BK_textarea_*`), trees (`BK_tree_item_*`), menus (`BK_open_menu`, `BK_open_pie`), color ramps and curves, `BK_undo` / `BK_redo` | +| **Raster alpha** | `BUI_raster_set_alpha(r, 1)` keeps premultiplied alpha in a CPU raster, `BUI_raster_clear` makes it transparent, `BUI_raster_alpha` reads the setting. An opaque raster draws the same bytes as before. | +| **Specialised** | Canvas display list (`BK_canvas_set_item`), text area (`BK_textarea_*`), trees (`BK_tree_item_*`), menus (`BK_open_menu`, `BK_open_pie`), the hit test (`BK_hit_at`), color ramps and curves, `BK_undo` / `BK_redo` | | **Custom nodes** | A `custom` node draws and handles input itself: `BK_custom_set(node, &cls, data)` with `event` (pointer with all three buttons and a grab, precise wheel via `BK_scroll`, keys and text while focused, key releases, focus), `frame` (called by `BK_render` when requested with `BK_custom_request_frame(node, ms)`: immediate-mode UIs build their frame there) and `draw` (raster + rect). `BK_custom_set_cursor`, `BK_node_context`. Dear ImGui runs on it: [../bindings/imgui](../bindings/imgui/README.md). | | **File browser** | `BK_node_new("filebrowser")`: Blender's File Browser in any window. Strings `dir`, `file`, `button`, `search`; numbers `mode` (0 open, 1 save, 2 directory), `multiple`, `hidden`, `sort`, `invert`; list `filters` (`"Label\tpattern ..."`); read `state` and `result`. `BK_file_dialog(title, mode, dir, file, filters, n, multiple, &paths)` runs one in its own window and returns the number of chosen paths (free them with `BK_file_dialog_free`). | | **Meshes and textures** | `BUI_texture_create/update/free` (RGBA8, shared by every raster and GPU device), `BUI_raster_mesh(r, vertices, n, indices, n_idx, flags, tex, clip, ox, oy)`: indexed triangles with position, UV and color, straight-alpha blending, per-call clip; the same pixels on the software raster, Direct3D 11 and OpenGL. | diff --git a/docs/components.md b/docs/components.md index 157369f..b6341e2 100644 --- a/docs/components.md +++ b/docs/components.md @@ -36,8 +36,8 @@ App(title="BUI", size=(420, 640), *children, padding=None, spacing=None, header= | `app.add(*components)` | Append to the root | | `app.bind("ctrl+s", fn)` | Window hotkey (`"q"`, `"shift+f3"`, `"ctrl+alt+z"` ...) | | `app.run(native=None)` | Open the window and run the loop. `native=False` forces the Tk host. | -| `app.open(native=None)` | Open the window without running the loop. Returns the `bui.host.Window`. | -| `app.ctx` | The C kit context (popups, focus, undo: `ctx.undo()` / `ctx.redo()`) | +| `app.open(native=None)` | Open the window without running the loop. Returns the `bui.host.Window`: `wm(key, a, b)` sends window requests (`"transparent"`, `"override"`, `"topmost"`, and on Wayland `"layer"` and `"anchor"`), `on_focus(active)` reports keyboard focus. | +| `app.ctx` | The C kit context (popups, focus, undo: `ctx.undo()` / `ctx.redo()`; `ctx.hit(x, y)` gives the interactive node under a point) | | `app.render_raster(w, h)` | Render headlessly into a `buipy.Raster` (tests, screenshots) | | `app.press / release / motion / key / wheel / context_menu` | Drive input headlessly (tests) | diff --git a/docs/imgui.md b/docs/imgui.md index 7708814..fb87b0f 100644 --- a/docs/imgui.md +++ b/docs/imgui.md @@ -95,7 +95,7 @@ int main(void) Link `bui::cimgui` (CMake). `cimgui_impl_bui.h` is the backend for C: apps (window or headless), `cImGui_ImplBUI_Init(node, frame, user, config)` for a node in an existing BUI -UI, `cImGui_ImplBUI_StyleBlender`, the UI scale, and headless input helpers. Example: +UI, `cImGui_ImplBUI_StyleBlender`, `cImGui_ImplBUI_AbandonFrame` (ends a frame that a callback left early on an error), the UI scale, and headless input helpers. Example: `examples/imgui_demo_c.c` (`bui_imgui_demo_c`, `--screenshot out.png` for headless). ## Python diff --git a/docs/rust.md b/docs/rust.md index 2087173..a2f02ea 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -66,7 +66,7 @@ It follows gpui closely: | `impl Render for View` | the same: `fn render(&mut self, window, cx) -> impl IntoElement` | | `Entity`, `WeakEntity`, `cx.new` | `Copy` handles into the app's entity slots: `read` (returns `&T`), `update`, `downgrade`; `cx.release(e)` drops a value | | `Context`: `notify`, `listener`, `entity` | the same; it derefs to `App` | -| `WindowOptions { window_bounds, titlebar, .. }`, `WindowBounds::Windowed(..)` | the same, plus `is_resizable`, `window_min_size` | +| `WindowOptions { window_bounds, titlebar, .. }`, `WindowBounds::Windowed(..)` | the same, plus `is_resizable`, `window_min_size`, `gpu`, `window_background` (`WindowBackgroundAppearance::Transparent`), `kind` (`WindowKind::Floating` stays on top, `PopUp` is on top without a frame, `LayerShell(LayerShellOptions { layer, anchor, exclusive_zone, keyboard_interactivity })` is a Wayland layer-shell surface) and `window_decorations` (`WindowDecorations::Client`: no frame); `App::on_window_activation` reports focus. A Wayland toplevel cannot stay on top: use `LayerShell` | | `cx.set_global`, `cx.global` | the same | | `cx.stop_propagation()` | the same | diff --git a/gpu/include/GPU_raster.h b/gpu/include/GPU_raster.h index 034a6f0..393c520 100644 --- a/gpu/include/GPU_raster.h +++ b/gpu/include/GPU_raster.h @@ -53,6 +53,14 @@ const uint32_t *BUI_raster_pixels(const BuiRaster *r); int BUI_raster_damage(const BuiRaster *r, const int (**r_rects)[4]); /* Force the next end_frame to report the full frame as damaged. */ void BUI_raster_invalidate(BuiRaster *r); +/* Keep alpha: the frame becomes premultiplied 0xAARRGGBB, starting transparent where + * BUI_raster_clear clears it (a transparent window). Off (the default), the alpha byte stays 0 + * and the frame is opaque 0x00RRGGBB. A GPU renderer takes the setting when it is attached (the + * window needs an alpha surface: a 32-bit visual, a composition swap chain), so set it first. */ +void BUI_raster_set_alpha(BuiRaster *r, int on); +int BUI_raster_alpha(const BuiRaster *r); +/* Set the pixels of a rect to transparent (0), within the clip. */ +void BUI_raster_clear(BuiRaster *r, int x, int y, int w, int h); /* Clip subsequent drawing; w/h <= 0 removes the clip. */ void BUI_raster_set_clip(BuiRaster *r, int x, int y, int w, int h); @@ -309,6 +317,7 @@ const void *BUI_text_default_font(int *r_len); typedef struct BuiGpuX11Window { void *display; /* Display * the window lives on (NULL: open one) */ unsigned long window; /* Window */ + unsigned long visual; /* its VisualID (0: the default visual) */ } BuiGpuX11Window; int BUI_raster_gpu_attach(BuiRaster *r, void *window_handle, int wait); int BUI_raster_gpu_poll(BuiRaster *r); diff --git a/gpu/intern/gpu_d3d11.c b/gpu/intern/gpu_d3d11.c index 8e3321d..c4c47ef 100644 --- a/gpu/intern/gpu_d3d11.c +++ b/gpu/intern/gpu_d3d11.c @@ -23,6 +23,7 @@ # define COBJMACROS # include # include +# include # include # include @@ -39,6 +40,8 @@ static const GUID k_IID_IDXGIFactory2 = { 0x50c83a1c, 0xe072, 0x4c48, {0x87, 0xb0, 0x36, 0x30, 0xfa, 0x36, 0xa6, 0xd0}}; static const GUID k_IID_ID3D11Texture2D = { 0x6f15aaf2, 0xd208, 0x4e89, {0x9a, 0xb4, 0x48, 0x95, 0x35, 0xd3, 0x4f, 0x9c}}; +static const GUID k_IID_ID3D11DeviceContext1 = { + 0xbb2c6faa, 0xb5fb, 0x4082, {0x8e, 0x6b, 0x38, 0x8b, 0x8c, 0xfa, 0x90, 0xe1}}; # define REL(p) \ do { \ @@ -137,6 +140,10 @@ struct BuiGpu { IDXGIFactory2 *factory; IDXGISwapChain1 *swap; int sw, sh; /* swap chain size */ + /* alpha: frames keep premultiplied alpha. The swap chain is a bitblt one (DWM blur-behind + * shows its alpha, as in SDL) and ClearView clears the rects of a redrawn region. */ + int alpha; + ID3D11DeviceContext1 *ctx1; int swap_fresh; ID3D11VertexShader *vs_quad, *vs_tri, *vs_mesh; @@ -304,6 +311,11 @@ static int device_init(BuiGpu *g) if (FAILED(hr)) { return fail("D3D11CreateDevice", hr); } + if (g->alpha && FAILED(hr = ID3D11DeviceContext_QueryInterface(g->ctx, &k_IID_ID3D11DeviceContext1, + (void **)&g->ctx1))) + { + return fail("ID3D11DeviceContext1", hr); /* Windows 8: ClearView */ + } g->max_tex = got >= D3D_FEATURE_LEVEL_11_0 ? D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION : 8192; const char *max_env = getenv("BUI_GPU_MAX_TEXTURE"); /* tests: force tiling */ if (max_env && atoi(max_env) >= 64) { @@ -431,13 +443,14 @@ static DWORD WINAPI device_thread(void *arg) return 0; } -BuiGpu *bui_gpu_create(void *hwnd) +BuiGpu *bui_gpu_create(void *hwnd, int alpha) { BuiGpu *g = (BuiGpu *)calloc(1, sizeof(BuiGpu)); if (!g) { return NULL; } g->hwnd = (HWND)hwnd; + g->alpha = alpha; g->mask_map = (Slot *)calloc(MASK_MAP, sizeof(Slot)); g->img_map = (Slot *)calloc(IMG_MAP, sizeof(Slot)); g->thread = CreateThread(NULL, 0, device_thread, g, 0, NULL); @@ -523,6 +536,7 @@ void bui_gpu_free(BuiGpu *g) REL(g->ps_tri); REL(g->swap); REL(g->factory); + REL(g->ctx1); REL(g->ctx); REL(g->dev); free(g->inst); @@ -1393,8 +1407,8 @@ static int ensure_target(BuiGpu *g) } g->tw = g->w; g->th = g->h; - const float black[4] = {0, 0, 0, 1}; - ID3D11DeviceContext_ClearRenderTargetView(g->ctx, g->rtv, black); + const float clear[4] = {0, 0, 0, g->alpha ? 0.0f : 1.0f}; + ID3D11DeviceContext_ClearRenderTargetView(g->ctx, g->rtv, clear); g->dmg_full = 1; return 1; } @@ -1566,9 +1580,17 @@ static int ensure_swap(BuiGpu *g) sd.SampleDesc.Count = 1; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.BufferCount = 2; - sd.Scaling = DXGI_SCALING_NONE; - sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; - sd.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + if (g->alpha) { + /* bitblt model, as SDL's transparent D3D11 windows: Present copies the alpha channel into + * the window's surface, which DWM blends (the host enables blur-behind) */ + sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; + sd.Scaling = DXGI_SCALING_STRETCH; + } + else { + sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; + sd.Scaling = DXGI_SCALING_NONE; + sd.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + } if (FAILED(IDXGIFactory2_CreateSwapChainForHwnd(g->factory, (IUnknown *)g->dev, g->hwnd, &sd, NULL, NULL, &g->swap))) { @@ -1582,18 +1604,29 @@ static int ensure_swap(BuiGpu *g) return 1; } +void bui_gpu_clear(BuiGpu *g, int x, int y, int w, int h) +{ + flush(g); /* what was recorded for earlier regions comes first */ + if (!g->ctx1 || !g->target || g->tw != g->w || g->th != g->h) { + return; /* a new target starts transparent */ + } + const float clear[4] = {0, 0, 0, 0}; + const RECT rect = {x, y, x + w, y + h}; + ID3D11DeviceContext1_ClearView(g->ctx1, (ID3D11View *)g->rtv, clear, &rect, 1); +} + int bui_gpu_present(BuiGpu *g, int full) { flush(g); if (!g->target || !ensure_swap(g)) { g->n_dmg = g->dmg_full = 0; - return g->target != NULL; + return g->target != NULL && !g->alpha; /* no alpha surface: the renderer is given up */ } ID3D11Texture2D *back = NULL; if (FAILED(IDXGISwapChain1_GetBuffer(g->swap, 0, &k_IID_ID3D11Texture2D, (void **)&back))) { return 0; } - /* the flip model hands back an older buffer: copy the whole frame */ + /* a flip-model buffer is older than the last frame: copy the whole frame */ ID3D11DeviceContext_CopyResource(g->ctx, (ID3D11Resource *)back, (ID3D11Resource *)g->target); REL(back); DXGI_PRESENT_PARAMETERS pp = {0}; @@ -1640,10 +1673,12 @@ int bui_gpu_read(BuiGpu *g, uint32_t *dst, int w, int h) D3D11_MAPPED_SUBRESOURCE m; int ok = 0; if (SUCCEEDED(ID3D11DeviceContext_Map(g->ctx, (ID3D11Resource *)st, 0, D3D11_MAP_READ, 0, &m))) { + /* like the raster: 0x00RRGGBB, or premultiplied 0xAARRGGBB with alpha */ + const uint32_t mask = g->alpha ? 0xFFFFFFFFu : 0xFFFFFFu; for (int y = 0; y < h; y++) { const uint32_t *src = (const uint32_t *)((const uint8_t *)m.pData + (size_t)y * m.RowPitch); for (int x = 0; x < w; x++) { - dst[(size_t)y * w + x] = src[x] & 0xFFFFFFu; /* 0x00RRGGBB like the raster */ + dst[(size_t)y * w + x] = src[x] & mask; } } ID3D11DeviceContext_Unmap(g->ctx, (ID3D11Resource *)st, 0); @@ -1682,9 +1717,10 @@ int bui_gpu_slug_glyph(BuiGpu *g, int face, int glyph, unsigned gen, float r_bbo # include "gpu_d3d11.h" -BuiGpu *bui_gpu_create(void *hwnd) +BuiGpu *bui_gpu_create(void *hwnd, int alpha) { (void)hwnd; + (void)alpha; return NULL; } void bui_gpu_free(BuiGpu *g) @@ -1799,6 +1835,14 @@ void bui_gpu_mesh(BuiGpu *g, (void)nearest; (void)clip; } +void bui_gpu_clear(BuiGpu *g, int x, int y, int w, int h) +{ + (void)g; + (void)x; + (void)y; + (void)w; + (void)h; +} void bui_gpu_damage(BuiGpu *g, int x, int y, int w, int h) { (void)g; diff --git a/gpu/intern/gpu_d3d11.h b/gpu/intern/gpu_d3d11.h index f088759..b7b9300 100644 --- a/gpu/intern/gpu_d3d11.h +++ b/gpu/intern/gpu_d3d11.h @@ -43,7 +43,9 @@ typedef struct BuiGpu BuiGpu; /* Starts creating the device on a worker thread (the first frames keep * using the software raster). NULL when D3D11 isn't available. */ -BuiGpu *bui_gpu_create(void *hwnd); +/* `alpha`: frames keep premultiplied alpha (the target starts transparent, and the window + * shows it with per-pixel alpha). */ +BuiGpu *bui_gpu_create(void *hwnd, int alpha); void bui_gpu_free(BuiGpu *g); /* 1 once the device is ready, -1 when creation failed, 0 while pending * (never 0 with `wait`). */ @@ -128,6 +130,8 @@ unsigned bui_texture_free_gen(void); * when more went by than the log keeps (free whatever isn't in use). */ int bui_image_forgotten(unsigned *seen, uint64_t *r_keys, int max); +/* Sets a rect of the frame to transparent (BUI_raster_clear of an alpha frame). */ +void bui_gpu_clear(BuiGpu *g, int x, int y, int w, int h); /* Records damage for present (x, y, w, h); w <= 0 means the whole frame. */ void bui_gpu_damage(BuiGpu *g, int x, int y, int w, int h); /* Draws everything recorded and presents. Returns 0 on device loss. */ diff --git a/gpu/intern/gpu_gl.c b/gpu/intern/gpu_gl.c index d234c67..09cb821 100644 --- a/gpu/intern/gpu_gl.c +++ b/gpu/intern/gpu_gl.c @@ -191,6 +191,7 @@ typedef void (*ProcFn)(void); # define GLX_RED_SIZE 8 # define GLX_GREEN_SIZE 9 # define GLX_BLUE_SIZE 10 +# define GLX_ALPHA_SIZE 11 # define GLX_X_VISUAL_TYPE 0x22 # define GLX_TRUE_COLOR 0x8002 # define GLX_VISUAL_ID 0x800B @@ -922,6 +923,8 @@ struct BuiGpu { int has_thread; XDisplay *host_dpy; XID host_win; + unsigned long host_visual; /* the window's VisualID (0: the default visual) */ + int alpha; /* frames keep premultiplied alpha */ /* context */ int api; @@ -1077,6 +1080,8 @@ static int glx_create(BuiGpu *g, XDisplay *dpy, XID window) 8, GLX_BLUE_SIZE, 8, + GLX_ALPHA_SIZE, + g->alpha ? 8 : 0, GLX_DOUBLEBUFFER, window ? 1 : 0, 0}; @@ -1088,10 +1093,12 @@ static int glx_create(BuiGpu *g, XDisplay *dpy, XID window) } return fail("glXChooseFBConfig"); } - /* the kit's windows use the default visual: take the config made for it */ + /* the window's visual (the default one, or the 32-bit ARGB one of a transparent window): + * take the config made for it */ GLXFBConfig cfg = cfgs[0]; if (window) { - const unsigned long want = p_XVisualIDFromVisual(p_XDefaultVisual(dpy, scr)); + const unsigned long want = g->host_visual ? g->host_visual : + p_XVisualIDFromVisual(p_XDefaultVisual(dpy, scr)); for (int i = 0; i < n; i++) { int vid = 0; if (p_glXGetFBConfigAttrib(dpy, cfgs[i], GLX_VISUAL_ID, &vid) == 0 && @@ -1490,7 +1497,7 @@ static void *device_thread(void *arg) return NULL; } -BuiGpu *bui_gpu_create(void *handle) +BuiGpu *bui_gpu_create(void *handle, int alpha) { BuiGpu *g = (BuiGpu *)calloc(1, sizeof(BuiGpu)); if (!g) { @@ -1500,7 +1507,9 @@ BuiGpu *bui_gpu_create(void *handle) const BuiGpuX11Window *xw = (const BuiGpuX11Window *)handle; g->host_dpy = (XDisplay *)xw->display; g->host_win = (XID)xw->window; + g->host_visual = xw->visual; } + g->alpha = alpha; g->mask_map = (Slot *)calloc(MASK_MAP, sizeof(Slot)); g->img_map = (Slot *)calloc(IMG_MAP, sizeof(Slot)); g->has_thread = (!handle || g->host_win) && g->mask_map && g->img_map && @@ -2440,7 +2449,7 @@ static int ensure_target(BuiGpu *g) g->tw = g->w; g->th = g->h; GL(glViewport)(0, 0, g->tw, g->th); - GL(glClearColor)(0, 0, 0, 1); + GL(glClearColor)(0, 0, 0, g->alpha ? 0 : 1); GL(glClear)(GL_COLOR_BUFFER_BIT); g->dmg_full = 1; return 1; @@ -2549,6 +2558,20 @@ static void flush(BuiGpu *g) g->n_b = g->n_inst = g->n_tv = g->n_mv = g->n_mi = 0; } +void bui_gpu_clear(BuiGpu *g, int x, int y, int w, int h) +{ + flush(g); /* what was recorded for earlier regions comes first */ + if (!g->target || g->tw != g->w || g->th != g->h || !make_current(g)) { + return; /* a new target starts transparent */ + } + GL(glBindFramebuffer)(GL_FRAMEBUFFER, g->fbo); + GL(glEnable)(GL_SCISSOR_TEST); + GL(glScissor)(x, y, w, h); /* the frame is stored top row first: GL y == BUI y */ + GL(glClearColor)(0, 0, 0, 0); + GL(glClear)(GL_COLOR_BUFFER_BIT); + GL(glDisable)(GL_SCISSOR_TEST); +} + int bui_gpu_present(BuiGpu *g, int full) { (void)full; @@ -2591,7 +2614,8 @@ int bui_gpu_read(BuiGpu *g, uint32_t *dst, int w, int h) * 0xAARRGGBB words */ GL(glReadPixels)(0, 0, w, h, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, dst); const size_t n = (size_t)w * h; - for (size_t i = 0; i < n; i++) { + const size_t keep = g->alpha ? 0 : n; /* a frame with alpha keeps it */ + for (size_t i = 0; i < keep; i++) { dst[i] &= 0xFFFFFFu; /* 0x00RRGGBB like the raster */ } return 1; diff --git a/gpu/intern/gpu_raster.c b/gpu/intern/gpu_raster.c index e2e3eb7..3234e7b 100644 --- a/gpu/intern/gpu_raster.c +++ b/gpu/intern/gpu_raster.c @@ -32,6 +32,9 @@ struct BuiRaster { BuiGpu *gpu; BuiGpu *gpu_pending; int gpu_switched; /* the renderer changed: the next frame must be full */ + /* 0xFF000000 when the frame keeps alpha (premultiplied 0xAARRGGBB, BUI_raster_set_alpha), + * else 0: the alpha byte stays 0 and the frame is opaque. */ + uint32_t alpha; struct BuiRaster *scratch; /* CPU coverage for polygons in GPU mode */ }; @@ -40,7 +43,7 @@ struct BuiRaster { * \{ */ typedef struct Paint { - uint32_t rgb; /* 0x00RRGGBB */ + uint32_t rgb; /* 0x00RRGGBB, or 0xFFRRGGBB in a raster that keeps alpha */ int a; /* 0..256 */ } Paint; @@ -54,24 +57,27 @@ static inline float clampf(float v, float lo, float hi) return v < lo ? lo : (v > hi ? hi : v); } -static inline Paint paint_from(const float c[4]) +static inline Paint paint_from(uint32_t alpha, const float c[4]) { Paint p; int r = (int)(clampf(c[0], 0.0f, 1.0f) * 255.0f + 0.5f); int g = (int)(clampf(c[1], 0.0f, 1.0f) * 255.0f + 0.5f); int b = (int)(clampf(c[2], 0.0f, 1.0f) * 255.0f + 0.5f); - p.rgb = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b; + p.rgb = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b | alpha; p.a = (int)(clampf(c[3], 0.0f, 1.0f) * 256.0f + 0.5f); return p; } -/* Blend s over d with factor f in 0..256. */ +/* Blend s over d with factor f in 0..256. The alpha byte only takes part when s has one + * (a raster that keeps alpha): an opaque frame costs what it did without alpha. */ static inline uint32_t blend(uint32_t d, uint32_t s, uint32_t f) { const uint32_t nf = 256 - f; const uint32_t rb = (((s & 0xFF00FFu) * f + (d & 0xFF00FFu) * nf) >> 8) & 0xFF00FFu; - const uint32_t g = (((s & 0x00FF00u) * f + (d & 0x00FF00u) * nf) >> 8) & 0x00FF00u; - return rb | g; + if (!(s >> 24)) { + return rb | ((((s & 0x00FF00u) * f + (d & 0x00FF00u) * nf) >> 8) & 0x00FF00u); + } + return rb | ((((s >> 8) & 0xFF00FFu) * f + ((d >> 8) & 0xFF00FFu) * nf) & 0xFF00FF00u); } /* Write with coverage cov (0..1) scaled by paint alpha. */ @@ -269,7 +275,7 @@ int BUI_raster_gpu_attach(BuiRaster *r, void *window_handle, int wait) if (r->gpu || r->gpu_pending) { return 1; } - r->gpu_pending = bui_gpu_create(window_handle); + r->gpu_pending = bui_gpu_create(window_handle, r->alpha != 0); if (!r->gpu_pending) { return 0; } @@ -469,6 +475,32 @@ void BUI_raster_invalidate(BuiRaster *r) r->full_damage = 1; } +void BUI_raster_set_alpha(BuiRaster *r, int on) +{ + r->alpha = on ? 0xFF000000u : 0u; + r->full_damage = 1; +} + +int BUI_raster_alpha(const BuiRaster *r) +{ + return r->alpha != 0; +} + +void BUI_raster_clear(BuiRaster *r, int x, int y, int w, int h) +{ + int x0, y0, x1, y1; + if (!clip_box(r, (float)x, (float)y, (float)(x + w), (float)(y + h), &x0, &y0, &x1, &y1)) { + return; + } + if (r->gpu) { + bui_gpu_clear(r->gpu, x0, y0, x1 - x0, y1 - y0); + return; + } + for (int py = y0; py < y1; py++) { + memset(r->pix + (size_t)py * r->w + x0, 0, sizeof(uint32_t) * (size_t)(x1 - x0)); + } +} + size_t BUI_raster_view_size(void) { return sizeof(BuiRaster); @@ -515,7 +547,7 @@ void BUI_raster_fill_rect(BuiRaster *r, float x, float y, float w, float h, cons } return; } - const Paint p = paint_from(rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -604,7 +636,7 @@ void BUI_raster_round_rect( } return; } - const Paint p = paint_from(rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -694,7 +726,7 @@ void BUI_raster_circle( } return; } - const Paint p = paint_from(rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -753,7 +785,7 @@ void BUI_raster_line( } return; } - const Paint p = paint_from(rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -805,7 +837,7 @@ void BUI_raster_ellipse(BuiRaster *r, } return; } - const Paint p = paint_from(rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -902,9 +934,9 @@ void BUI_raster_widget(BuiRaster *r, return; } const int gradient = top && bot && memcmp(top, bot, sizeof(float[4])) != 0; - Paint p_inner = top ? paint_from(top) : (Paint){0, 0}; - const Paint p_out = outline ? paint_from(outline) : (Paint){0, 0}; - const Paint p_emb = has_emboss ? paint_from(emboss) : (Paint){0, 0}; + Paint p_inner = top ? paint_from(r->alpha, top) : (Paint){0, 0}; + const Paint p_out = outline ? paint_from(r->alpha, outline) : (Paint){0, 0}; + const Paint p_emb = has_emboss ? paint_from(r->alpha, emboss) : (Paint){0, 0}; /* Fully covered interior margin: no SDF needed farther than this from * every edge (skips sqrt for the bulk of each widget). */ const float safe = rad + ow + 1.0f; @@ -918,7 +950,7 @@ void BUI_raster_widget(BuiRaster *r, for (int k = 0; k < 4; k++) { c[k] = top[k] + (bot[k] - top[k]) * t; } - p_inner = paint_from(c); + p_inner = paint_from(r->alpha, c); } const int row_inside = (sy - y) >= safe && (y + h - sy) >= safe; /* Interior span of this row: filled directly, no per-pixel SDF. */ @@ -983,6 +1015,7 @@ void BUI_raster_widget(BuiRaster *r, * only) is the emboss. */ void BUI_raster_widget_base(BuiRaster *r, const BuiWidgetBase *p) { + const uint32_t ralpha = r->alpha; /* a pixel store may alias r->alpha: read it once */ const float x = p->rect[0], y = p->rect[1], w = p->rect[2], h = p->rect[3]; if (w <= 0.0f || h <= 0.0f) { return; @@ -1023,9 +1056,9 @@ void BUI_raster_widget_base(BuiRaster *r, const BuiWidgetBase *p) } const int gradient = memcmp(p->inner1, p->inner2, sizeof(float[4])) != 0; const int per_pixel_inner = p->alpha_checker || (gradient && p->shade_horizontal); - const Paint p_out = paint_from(p->outline); - const Paint p_emb = paint_from(p->emboss); - Paint p_in = paint_from(p->inner1); + const Paint p_out = paint_from(ralpha, p->outline); + const Paint p_emb = paint_from(ralpha, p->emboss); + Paint p_in = paint_from(ralpha, p->inner1); /* Columns farther than this from the left/right edges never see a corner: * there the distance field only depends on the row (computed once). */ const float cornerw = fmaxf(rad, radi) + px1 + 1.0f; @@ -1041,7 +1074,7 @@ void BUI_raster_widget_base(BuiRaster *r, const BuiWidgetBase *p) for (int k = 0; k < 4; k++) { c[k] = p->inner1[k] + (p->inner2[k] - p->inner1[k]) * t; } - p_in = paint_from(c); + p_in = paint_from(ralpha, c); } /* -- middle columns: row-constant coverage -- */ int span_a = ma, span_b = mb; @@ -1109,7 +1142,7 @@ void BUI_raster_widget_base(BuiRaster *r, const BuiWidgetBase *p) const int cell = ((int)((sx - x) / 8.0f) + (int)((sy - y) / 8.0f)) & 1; const float g = cell ? 160.0f / 255.0f : 100.0f / 255.0f; const float gc[4] = {g, g, g, 1.0f}; - put_cov(row + px, paint_from(gc), ci); + put_cov(row + px, paint_from(ralpha, gc), ci); } Paint pi = p_in; if (gradient && p->shade_horizontal) { @@ -1118,7 +1151,7 @@ void BUI_raster_widget_base(BuiRaster *r, const BuiWidgetBase *p) for (int k = 0; k < 4; k++) { c[k] = p->inner1[k] + (p->inner2[k] - p->inner1[k]) * t; } - pi = paint_from(c); + pi = paint_from(ralpha, c); } put_cov(row + px, pi, ci); } @@ -1129,15 +1162,21 @@ void BUI_raster_widget_base(BuiRaster *r, const BuiWidgetBase *p) } } -/* Premultiplied source over the frame (whose alpha byte is unused). */ -static inline uint32_t premul_over(uint32_t d, uint32_t s) +/* Premultiplied source over the frame. The alpha byte is blended only when the frame keeps + * alpha (`alpha` is BuiRaster.alpha): an opaque frame costs what it did without alpha. */ +static inline uint32_t premul_over(uint32_t d, uint32_t s, uint32_t alpha) { const uint32_t ia = 255u - (s >> 24); uint32_t rb = (d & 0xFF00FFu) * ia + 0x800080u; - uint32_t g = (d & 0x00FF00u) * ia + 0x008000u; rb = ((rb + ((rb >> 8) & 0xFF00FFu)) >> 8) & 0xFF00FFu; /* /255, rounded */ - g = ((g + ((g >> 8) & 0x00FF00u)) >> 8) & 0x00FF00u; - return (s & 0xFFFFFFu) + rb + g; + if (!alpha) { + uint32_t g = (d & 0x00FF00u) * ia + 0x008000u; + g = ((g + ((g >> 8) & 0x00FF00u)) >> 8) & 0x00FF00u; + return (s & 0xFFFFFFu) + rb + g; + } + uint32_t ag = ((d >> 8) & 0xFF00FFu) * ia + 0x800080u; + ag = (ag + ((ag >> 8) & 0xFF00FFu)) & 0xFF00FF00u; + return s + rb + ag; } /* All four channels times f/256. */ @@ -1148,17 +1187,17 @@ static inline uint32_t premul_scale(uint32_t c, uint32_t f) return ag | rb; } -/* An image pixel (premultiplied) times ga/256 over the frame. */ -static inline void put_image_px(uint32_t *d, uint32_t c, uint32_t ga) +/* An image pixel (premultiplied) times ga/256 over the frame; `alpha` is BuiRaster.alpha. */ +static inline void put_image_px(uint32_t *d, uint32_t c, uint32_t ga, uint32_t alpha) { if (ga < 256) { c = premul_scale(c, ga); } if (c >= 0xFF000000u) { - *d = c & 0xFFFFFFu; + *d = c & (0xFFFFFFu | alpha); } else if (c) { - *d = premul_over(*d, c); + *d = premul_over(*d, c, alpha); } } @@ -1343,6 +1382,7 @@ static void raster_nv12(BuiRaster *r, float x, float y, float w, float h, const float sx, float sy, float sw, float sh, uint32_t ga, int nearest, int x0, int y0, int x1, int y1) { + const uint32_t ralpha = r->alpha; /* a pixel store may alias r->alpha: read it once */ const int *k = k_yuv[img->format == BUI_PIXMAP_NV12_BT709]; const uint8_t *yp = (const uint8_t *)img->px; const uint8_t *uvp = yp + (size_t)img->w * img->h; @@ -1368,7 +1408,7 @@ static void raster_nv12(BuiRaster *r, float x, float y, float w, float h, const const int l = (yr[px + ox] - 16) * k[0]; row[px] = (uint32_t)clampi((l + rv) >> 16, 0, 255) << 16 | (uint32_t)clampi((l + gu) >> 16, 0, 255) << 8 | - (uint32_t)clampi((l + bu) >> 16, 0, 255); + (uint32_t)clampi((l + bu) >> 16, 0, 255) | ralpha; } px--; } @@ -1377,7 +1417,7 @@ static void raster_nv12(BuiRaster *r, float x, float y, float w, float h, const for (int px = x0; px < x1; px++) { const int tx = px + ox; const uint8_t *c = cr + (tx & ~1); - put_image_px(row + px, yuv_argb(k, yr[tx], c[0], c[1]), ga); + put_image_px(row + px, yuv_argb(k, yr[tx], c[0], c[1]), ga, ralpha); } } return; @@ -1414,7 +1454,7 @@ static void raster_nv12(BuiRaster *r, float x, float y, float w, float h, const const int Y = lerp2(ya[xa], ya[xb], yb[xa], yb[xb], fx, fy); const int U = lerp2(ca[ca0], ca[cb0], cb[ca0], cb[cb0], fcx, fcy); const int V = lerp2(ca[ca0 + 1], ca[cb0 + 1], cb[ca0 + 1], cb[cb0 + 1], fcx, fcy); - put_image_px(row + px, yuv_argb(k, Y, U, V), ga); + put_image_px(row + px, yuv_argb(k, Y, U, V), ga, ralpha); } } } @@ -1429,6 +1469,7 @@ void BUI_raster_image(BuiRaster *r, float alpha, int flags) { + const uint32_t ralpha = r->alpha; /* a pixel store may alias r->alpha: read it once */ int x0, y0, x1, y1; if (!img || !img->px || img->w <= 0 || img->h <= 0 || !(alpha > 0.0f) || !clip_box(r, x, y, x + w, y + h, &x0, &y0, &x1, &y1)) @@ -1486,7 +1527,7 @@ void BUI_raster_image(BuiRaster *r, uint32_t *row = r->pix + (size_t)py * r->w; const uint32_t *s = img->px + (size_t)(py - (int)y + (int)sy) * img->w + (int)sx - (int)x; for (int px = x0; px < x1; px++) { - put_image_px(row + px, s[px], ga); + put_image_px(row + px, s[px], ga, ralpha); } } return; @@ -1515,7 +1556,7 @@ void BUI_raster_image(BuiRaster *r, const int xa = clampi(tx, ix0, ix1), xb = clampi(tx + 1, ix0, ix1); c = bilerp(ra[xa], ra[xb], rb[xa], rb[xb], fx, fy); } - put_image_px(row + px, c, ga); + put_image_px(row + px, c, ga, ralpha); } } } @@ -1683,7 +1724,7 @@ void BUI_raster_polygon(BuiRaster *r, const float (*pts)[2], int n, const float free(mask); return; } - const Paint p = paint_from(rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -1762,6 +1803,7 @@ void BUI_raster_triangles(BuiRaster *r, const float (*col)[4], int count) { + const uint32_t ralpha = r->alpha; /* a pixel store may alias r->alpha: read it once */ if (r->gpu) { const int clip[4] = {r->cx0, r->cy0, r->cx1, r->cy1}; bui_gpu_triangles(r->gpu, co, col, count, clip); @@ -1794,7 +1836,7 @@ void BUI_raster_triangles(BuiRaster *r, } const int flat = memcmp(c[0], c[1], sizeof(float[4])) == 0 && memcmp(c[0], c[2], sizeof(float[4])) == 0; - const Paint flat_p = paint_from(c[0]); + const Paint flat_p = paint_from(ralpha, c[0]); const int tl0 = is_top_left(v[1][0], v[1][1], v[2][0], v[2][1]); const int tl1 = is_top_left(v[2][0], v[2][1], v[0][0], v[0][1]); const int tl2 = is_top_left(v[0][0], v[0][1], v[1][0], v[1][1]); @@ -1821,7 +1863,7 @@ void BUI_raster_triangles(BuiRaster *r, for (int k = 0; k < 4; k++) { rgba[k] = c[0][k] * b0 + c[1][k] * b1 + c[2][k] * b2; } - put_cov(row + px, paint_from(rgba), 1.0f); + put_cov(row + px, paint_from(ralpha, rgba), 1.0f); } } } @@ -2060,14 +2102,14 @@ static inline void tex_sample(const BuiTexture *t, float u, float v, int nearest } } -/* Straight-alpha source-over of color c (0..1) into a 0x00RRGGBB pixel. */ -static inline void put_straight(uint32_t *px, const float c[4]) +/* Straight-alpha source-over of color c (0..1) into a frame pixel. */ +static inline void put_straight(uint32_t alpha, uint32_t *px, const float c[4]) { const float a = c[3]; if (!(a > 0.0f)) { return; } - Paint p = paint_from(c); + Paint p = paint_from(alpha, c); if (p.a >= 256) { *px = p.rgb; } @@ -2106,6 +2148,7 @@ static void mesh_triangle(BuiRaster *r, int cx1, int cy1) { + const uint32_t ralpha = r->alpha; /* a pixel store may alias r->alpha: read it once */ int64_t X[3] = {mesh_fx(va->x + ox), mesh_fx(vb->x + ox), mesh_fx(vc->x + ox)}; int64_t Y[3] = {mesh_fx(va->y + oy), mesh_fx(vb->y + oy), mesh_fx(vc->y + oy)}; const BuiMeshVertex *v[3] = {va, vb, vc}; @@ -2185,7 +2228,7 @@ static void mesh_triangle(BuiRaster *r, continue; } if (flat_all) { - put_straight(row + px, flat); + put_straight(ralpha, row + px, flat); continue; } /* barycentric weights of vertices 0, 1, 2 */ @@ -2213,7 +2256,7 @@ static void mesh_triangle(BuiRaster *r, c[k] *= tx[k]; } } - put_straight(row + px, c); + put_straight(ralpha, row + px, c); } } } @@ -2283,6 +2326,7 @@ static void hsv_to_rgb_f(float h, float s, float v, float out[3]) void BUI_raster_hsv_circle(BuiRaster *r, float cx, float cy, float radius, float value) { + const uint32_t ralpha = r->alpha; /* a pixel store may alias r->alpha: read it once */ int x0, y0, x1, y1; if (radius <= 0.0f || !clip_box(r, cx - radius - 1, cy - radius - 1, cx + radius + 1, cy + radius + 1, &x0, &y0, @@ -2313,7 +2357,7 @@ void BUI_raster_hsv_circle(BuiRaster *r, float cx, float cy, float radius, float const float hue = atan2f(dx, -dy) * inv_2pi + 0.5f; float rgba[4] = {0, 0, 0, 1.0f}; hsv_to_rgb_f(hue, fminf(dist / radius, 1.0f), value, rgba); - put_cov(row + px, paint_from(rgba), cov); + put_cov(row + px, paint_from(ralpha, rgba), cov); } } } @@ -2328,6 +2372,7 @@ void BUI_raster_gradient(BuiRaster *r, const float br[4], const float bl[4]) { + const uint32_t ralpha = r->alpha; /* a pixel store may alias r->alpha: read it once */ int x0, y0, x1, y1; if (!clip_box(r, x, y, x + w, y + h, &x0, &y0, &x1, &y1)) { return; @@ -2357,7 +2402,7 @@ void BUI_raster_gradient(BuiRaster *r, for (int k = 0; k < 4; k++) { c[k] = left[k] + (right[k] - left[k]) * u; } - put_cov(row + px, paint_from(c), 1.0f); + put_cov(row + px, paint_from(ralpha, c), 1.0f); } } } @@ -2365,6 +2410,7 @@ void BUI_raster_gradient(BuiRaster *r, void BUI_raster_sphere( BuiRaster *r, float cx, float cy, float radius, const float light[3], const float rgba[4]) { + const uint32_t ralpha = r->alpha; /* a pixel store may alias r->alpha: read it once */ int x0, y0, x1, y1; if (radius <= 0.0f || !clip_box(r, cx - radius - 1, cy - radius - 1, cx + radius + 1, cy + radius + 1, &x0, &y0, @@ -2421,7 +2467,7 @@ void BUI_raster_sphere( c[k] = rgba[k] * (0.12f + 0.88f * diff) + 0.6f * spec; } c[3] = rgba[3]; - put_cov(row + px, paint_from(c), cov); + put_cov(row + px, paint_from(ralpha, c), cov); } } } @@ -2711,7 +2757,7 @@ static int gpu_text_glyphs(BuiRaster *r, TextRun *e, float x, float y, const flo void BUI_raster_text_run(BuiRaster *r, float x, float y, const BuiTextRun *run, const float rgba[4]) { const TextRun *e = (const TextRun *)run; - const Paint p = paint_from(rgba); + const Paint p = paint_from(r->alpha, rgba); if (!e || !e->mask || p.a <= 0) { return; } diff --git a/gpu/tests/CMakeLists.txt b/gpu/tests/CMakeLists.txt index ec1b5c9..5e17fe0 100644 --- a/gpu/tests/CMakeLists.txt +++ b/gpu/tests/CMakeLists.txt @@ -7,3 +7,7 @@ if(NOT MSVC) target_link_libraries(bui_gpu_parity PRIVATE m) endif() add_test(NAME gpu_parity COMMAND bui_gpu_parity) + +add_executable(bui_raster_alpha test_raster_alpha.c) +target_link_libraries(bui_raster_alpha PRIVATE bui_gpu) +add_test(NAME raster_alpha COMMAND bui_raster_alpha) diff --git a/gpu/tests/test_raster_alpha.c b/gpu/tests/test_raster_alpha.c new file mode 100644 index 0000000..2e980fc --- /dev/null +++ b/gpu/tests/test_raster_alpha.c @@ -0,0 +1,102 @@ +/* Frame alpha: an opaque raster keeps the alpha byte at 0 (0x00RRGGBB); a raster with + * BUI_raster_set_alpha is premultiplied 0xAARRGGBB and BUI_raster_clear makes it transparent. + * SPDX-License-Identifier: GPL-2.0-or-later */ +#include "GPU_raster.h" + +#include +#include + +static int near(int v, int want) +{ + return abs(v - want) <= 2; +} + +static int check(const char *what, int ok) +{ + if (!ok) { + printf("raster alpha: %s failed\n", what); + } + return ok; +} + +/* The same frames on the CPU and the GPU: a transparent clear, translucent and opaque paint, + * then a second frame that clears a rect over painted pixels (the GPU clear). */ +static void frames(BuiRaster *r, int frame) +{ + const float half[4] = {0.2f, 0.4f, 0.6f, 0.5f}, red[4] = {1.0f, 0.0f, 0.0f, 1.0f}; + BUI_raster_begin_frame(r, 16, 16); + if (frame == 0) { + BUI_raster_clear(r, 0, 0, 16, 16); + BUI_raster_fill_rect(r, 0, 0, 8, 16, half); + BUI_raster_fill_rect(r, 12, 0, 4, 16, red); + } + else { + BUI_raster_clear(r, 2, 2, 12, 6); + BUI_raster_fill_rect(r, 5, 3, 3, 2, half); + } + BUI_raster_end_frame(r); +} + +static int gpu_matches_cpu(void) +{ + BuiRaster *cpu = BUI_raster_create(16, 16), *gpu = BUI_raster_create(16, 16); + BUI_raster_set_alpha(cpu, 1); + BUI_raster_set_alpha(gpu, 1); + if (!BUI_raster_gpu_attach(gpu, NULL, 1)) { + printf("raster alpha: no GPU device, GPU part skipped\n"); + BUI_raster_free(cpu); + BUI_raster_free(gpu); + return 1; + } + int ok = 1; + for (int frame = 0; frame < 2; frame++) { + frames(cpu, frame); + frames(gpu, frame); + const uint32_t *a = BUI_raster_pixels(cpu), *b = BUI_raster_pixels(gpu); + for (int i = 0; i < 16 * 16; i++) { + for (int sh = 0; sh < 32; sh += 8) { + ok &= near((int)(a[i] >> sh & 0xFF), (int)(b[i] >> sh & 0xFF)); + } + } + if (!check(frame ? "GPU clear of painted pixels" : "GPU alpha frame", ok)) { + break; + } + } + BUI_raster_free(cpu); + BUI_raster_free(gpu); + return ok; +} + +int main(void) +{ + const float half[4] = {0.2f, 0.4f, 0.6f, 0.5f}, red[4] = {1.0f, 0.0f, 0.0f, 1.0f}; + BuiRaster *r = BUI_raster_create(8, 8); + int ok = 1; + + BUI_raster_begin_frame(r, 8, 8); + BUI_raster_fill_rect(r, 0, 0, 8, 8, red); + BUI_raster_fill_rect(r, 0, 0, 4, 8, half); + BUI_raster_end_frame(r); + const uint32_t *px = BUI_raster_pixels(r); + ok &= check("opaque raster has no alpha byte", (px[0] >> 24) == 0 && (px[7] >> 24) == 0); + + BUI_raster_set_alpha(r, 1); + BUI_raster_begin_frame(r, 8, 8); + BUI_raster_clear(r, 0, 0, 8, 8); + BUI_raster_fill_rect(r, 0, 0, 4, 8, half); + BUI_raster_fill_rect(r, 6, 0, 2, 8, red); + BUI_raster_end_frame(r); + px = BUI_raster_pixels(r); + const uint32_t p = px[0]; + ok &= check("cleared pixel is transparent", px[5] == 0); + ok &= check("opaque paint is opaque", px[7] == 0xFFFF0000u); + ok &= check("translucent paint is premultiplied", + near((int)(p >> 24), 128) && near((int)(p >> 16 & 0xFF), 26) && + near((int)(p >> 8 & 0xFF), 51) && near((int)(p & 0xFF), 77)); + ok &= check("BUI_raster_alpha", BUI_raster_alpha(r) == 1); + + BUI_raster_free(r); + ok &= gpu_matches_cpu(); + printf("raster alpha: %s\n", ok ? "ok" : "FAILED"); + return ok ? 0 : 1; +} diff --git a/kit/CMakeLists.txt b/kit/CMakeLists.txt index d5ebc4b..1cf9278 100644 --- a/kit/CMakeLists.txt +++ b/kit/CMakeLists.txt @@ -173,15 +173,28 @@ elseif(UNIX AND NOT APPLE) set(_wl_gen ${CMAKE_CURRENT_BINARY_DIR}/wayland) file(MAKE_DIRECTORY ${_wl_gen}) set(_wl_src) - foreach(_p stable/xdg-shell/xdg-shell unstable/xdg-decoration/xdg-decoration-unstable-v1) - get_filename_component(_n ${_p} NAME) + # wlr-layer-shell is not in wayland-protocols: kit/extern/wayland_protocols has it + set(_wl_xml ${BUI_WL_PROTOCOLS}/stable/xdg-shell/xdg-shell.xml + ${BUI_WL_PROTOCOLS}/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml + ${CMAKE_CURRENT_SOURCE_DIR}/extern/wayland_protocols/wlr-layer-shell-unstable-v1.xml) + # cursor-shape (wayland-protocols 1.32+), as GHOST: the compositor draws the cursor, so no + # cursor theme is loaded. Its code refers to the tablet tool interface. + set(_wl_cursor_shape ${BUI_WL_PROTOCOLS}/staging/cursor-shape/cursor-shape-v1.xml) + set(_wl_tablet ${BUI_WL_PROTOCOLS}/stable/tablet/tablet-v2.xml) + if(NOT EXISTS ${_wl_tablet}) + set(_wl_tablet ${BUI_WL_PROTOCOLS}/unstable/tablet/tablet-unstable-v2.xml) + endif() + if(EXISTS ${_wl_cursor_shape} AND EXISTS ${_wl_tablet}) + list(APPEND _wl_xml ${_wl_cursor_shape} ${_wl_tablet}) + target_compile_definitions(bui_kit PRIVATE BUI_WL_CURSOR_SHAPE) + endif() + foreach(_x ${_wl_xml}) + get_filename_component(_n ${_x} NAME_WE) add_custom_command( OUTPUT ${_wl_gen}/${_n}-client-protocol.h ${_wl_gen}/${_n}-client-protocol.c - COMMAND ${BUI_WL_SCANNER} client-header ${BUI_WL_PROTOCOLS}/${_p}.xml - ${_wl_gen}/${_n}-client-protocol.h - COMMAND ${BUI_WL_SCANNER} private-code ${BUI_WL_PROTOCOLS}/${_p}.xml - ${_wl_gen}/${_n}-client-protocol.c - DEPENDS ${BUI_WL_PROTOCOLS}/${_p}.xml) + COMMAND ${BUI_WL_SCANNER} client-header ${_x} ${_wl_gen}/${_n}-client-protocol.h + COMMAND ${BUI_WL_SCANNER} private-code ${_x} ${_wl_gen}/${_n}-client-protocol.c + DEPENDS ${_x}) list(APPEND _wl_src ${_wl_gen}/${_n}-client-protocol.c) endforeach() target_sources(bui_kit PRIVATE intern/kit_window_wayland.c intern/kit_window_linux.c diff --git a/kit/extern/wayland_protocols/wlr-layer-shell-unstable-v1.xml b/kit/extern/wayland_protocols/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..4a26ec2 --- /dev/null +++ b/kit/extern/wayland_protocols/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,409 @@ + + + + SPDX-License-Identifier: MIT + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + + + + + Requests an edge for the exclusive zone to apply. The exclusive + edge will be automatically deduced from anchor points when possible, + but when the surface is anchored to a corner, it will be necessary + to set it explicitly to disambiguate, as it is not possible to deduce + which one of the two corner edges should be used. + + The edge must be one the surface is anchored to, otherwise the + invalid_exclusive_edge protocol error will be raised. + + + + + diff --git a/kit/include/BUI_kit.h b/kit/include/BUI_kit.h index 41b9115..c55c77f 100644 --- a/kit/include/BUI_kit.h +++ b/kit/include/BUI_kit.h @@ -133,6 +133,9 @@ void BK_set_ref_hooks(void (*retain)(void *ref), void (*release)(void *ref)); /* -- global look ------------------------------------------------------------ */ void BK_set_scale(float scale); /* Blender's resolution scale (1 = 20 px widgets) */ float BK_scale(void); +/* Blender's UI text shadows (drop shadow, soft one on panel titles); on by default */ +void BK_set_text_shadow(int on); +int BK_text_shadow(void); int BK_unit(void); /* U.widget_unit in pixels */ void BK_set_font(const char *family); /* Themes: Blender's two presets, "dark" (Blender_Dark.xml = the built-in @@ -268,7 +271,12 @@ void BK_open_menu(BKContext *ctx, const char *title, int at_x, int at_y); -void BK_open_pie(BKContext *ctx, const BKMenuSpec *items, int n, const char *title); +/* The interactive node under (x, y) in window pixels (ui_but_find_mouse_over): the top popup + * first, then the header and the root. NULL over nothing. A pie answers for the whole window. */ +BKNode *BK_hit_at(BKContext *ctx, int x, int y); +/* The pie centre is (at_x, at_y) in window pixels, or the pointer when either is -100000. */ +void BK_open_pie( + BKContext *ctx, const BKMenuSpec *items, int n, const char *title, int at_x, int at_y); /* -- geometry managers (children of a "frame") ------------------------------------ * Tk's pack / grid / place, also used for Qt layouts. Keys (numbers): @@ -449,7 +457,7 @@ void BK_canvas_set_item(BKNode *canvas, const BKCanvasItem *item); /* insert or void BK_canvas_add_item(BKNode *canvas, const BKCanvasItem *item); /* Immediate-mode hosts that rebuild the whole list each frame: overwrite the * item at `index` (index == count appends), reusing its buffers, then call - * BK_canvas_truncate with the new count (drops the rest, redraws once). */ + * BK_canvas_truncate with the new count (drops the rest). Only items that changed are redrawn. */ void BK_canvas_put_item(BKNode *canvas, int index, const BKCanvasItem *item); void BK_canvas_truncate(BKNode *canvas, int count); /* append, ids not checked */ void BK_canvas_delete(BKNode *canvas, int id); /* id < 0: all */ @@ -661,6 +669,8 @@ typedef struct BKWindowCallbacks { /* BK_window_request_frame: called right before the frame renders, so a host * can rebuild its UI for exactly the frame that presents it. */ void (*on_frame)(void *data, BKWindow *win); + /* The window got (1) or lost (0) the keyboard focus (GHOST_kEventWindowActivate). */ + void (*on_focus)(void *data, BKWindow *win, int active); } BKWindowCallbacks; int BK_window_supported(void); BKWindow *BK_window_new(BKContext *ctx, const char *title, int width, int height); @@ -690,7 +700,17 @@ void BK_window_cancel_timer(BKWindow *win, int id); void *BK_window_handle(BKWindow *win); /* HWND / X11 Window / NSWindow * */ /* Window-manager requests (Tk's wm): "pos" (x, y), "minsize"/"maxsize" (w, h), * "resizable" (w, h), "topmost" (a), "alpha" (a), "override" (a), "state" - * (0 normal, 1 iconic, 2 withdrawn, 3 zoomed), "lift", "lower", "focus". */ + * (0 normal, 1 iconic, 2 withdrawn, 3 zoomed), "lift", "lower", "focus". + * "transparent" (a): per-pixel alpha, no region background. On Linux set it + * before show (X11 makes the window again; the OpenGL renderer uses its 32-bit visual). On Windows + * the window has no frame: with the Direct3D 11 renderer DWM blends the alpha of its frames + * (blur-behind), else the raster is shown with UpdateLayeredWindow (the window is made again). On macOS it is a + * non-opaque window with a clear background. + * "layer" (layer, keyboard; Wayland with wlr-layer-shell): a layer surface instead of a + * toplevel, layer 0 background .. 3 overlay (-1: a toplevel again), keyboard 0 none, + * 1 exclusive, 2 on demand. "anchor" (edges, exclusive zone): edges top 1, bottom 2, left 4, + * right 8; anchored at both edges of an axis, the surface fills it. wm_get("layer") gives + * (1 when the compositor has layer shell, the window's layer). */ void BK_window_wm(BKWindow *win, const char *key, double a, double b); /* Queries: "pos", "state", "screen" (w, h), "pointer" (x, y), "scale". */ int BK_window_wm_get(BKWindow *win, const char *key, double *r_a, double *r_b); diff --git a/kit/intern/kit_core.c b/kit/intern/kit_core.c index bebe8b5..9c267a1 100644 --- a/kit/intern/kit_core.c +++ b/kit/intern/kit_core.c @@ -920,6 +920,9 @@ int BK_set_num(BKNode *n, const char *key, double v) } for (size_t i = 0; i < sizeof(g_flag_props) / sizeof(g_flag_props[0]); i++) { if (strcmp(key, g_flag_props[i].key) == 0) { + if (((n->flags & g_flag_props[i].flag) != 0) == (v != 0)) { + return 1; /* unchanged: no relayout, no redraw */ + } if (v != 0) { n->flags |= g_flag_props[i].flag; } @@ -1761,29 +1764,7 @@ void bk_close_popups_after(BKContext *ctx, BKNode *after) while (ctx->npopups > keep) { BKNode *p = ctx->popups[--ctx->npopups]; bk_invalidate(p); - /* clear references into the popup before freeing it */ - if (ctx->hover && is_within(ctx->hover, p)) { - ctx->hover = NULL; - } - if (ctx->grab && is_within(ctx->grab, p)) { - ctx->grab = NULL; - } - if (ctx->focus && is_within(ctx->focus, p)) { - BKNode *f = ctx->focus; - ctx->focus = NULL; - bk_te_free(f->edit); - f->edit = NULL; - } - if (ctx->tip_node && is_within(ctx->tip_node, p)) { - ctx->tip_node = NULL; - ctx->tip_shown = 0; - } - if (ctx->last_click_node && is_within(ctx->last_click_node, p)) { - ctx->last_click_node = NULL; - } - if (ctx->modal && is_within(ctx->modal, p)) { - ctx->modal = NULL; - } + bk_forget(ctx, p); /* references into the popup, its animations, its undo steps */ /* The popover panel's body belongs to its owner, not the popup. */ if (p->cls == &bk_cls_popover_panel && p->nkids) { BKNode *body = p->kids[0]; @@ -2187,7 +2168,10 @@ static void draw_region(BKContext *ctx, const BKRect *rect) const int w = ctx->win_w, h = ctx->win_h; bk_clip_reset(w, h); bk_clip_push(rect->xmin, rect->ymin, bk_rw(rect), bk_rh(rect)); - if (!(ctx->root && (ctx->root->flags & NF_OPAQUE))) { + if (BUI_raster_alpha(bk_draw.r)) { /* a transparent window: no region background */ + BUI_raster_clear(bk_draw.r, rect->xmin, rect->ymin, bk_rw(rect), bk_rh(rect)); + } + else if (!(ctx->root && (ctx->root->flags & NF_OPAQUE))) { bk_fill(rect->xmin, rect->ymin, bk_rw(rect), bk_rh(rect), bk_theme.region_bg); } const int top = ctx->header_h, view_h = h - top; @@ -2792,6 +2776,7 @@ static int in_BK_key(BKContext *ctx, const char *text, const char *keysym, int m BKEvent e = make_event(ctx, NULL, ctx->pointer_x, ctx->pointer_y, mods); e.text = text ? text : ""; e.keysym = keysym ? keysym : ""; + snprintf(ctx->key_held, sizeof(ctx->key_held), "%s", e.keysym); ctx->interaction++; for (BKNode *n = ctx->focus ? ctx->focus : ctx->hover; n; n = n->parent) { if (n->flags & NF_EVENTS) { /* Tk/Qt key bindings see the key first */ @@ -3417,6 +3402,13 @@ int BK_key_release(BKContext *ctx, const char *keysym, int mods) BKEvent e = make_event(ctx, NULL, ctx->pointer_x, ctx->pointer_y, mods); e.text = ""; e.keysym = keysym ? keysym : ""; + if (bk_keysym_is(e.keysym, ctx->key_held)) { + ctx->key_held[0] = '\0'; + } + BKNode *top = ctx->npopups ? ctx->popups[ctx->npopups - 1] : NULL; + if (top && top->cls->key_up && top->cls->key_up(top, &e)) { + return 1; + } for (BKNode *n = ctx->focus ? ctx->focus : ctx->hover; n; n = n->parent) { if (BK_IS_CUSTOM(n) && NODE_ENABLED(n)) { BKCustomEvent ce; @@ -3710,9 +3702,10 @@ static void anim_step(BKContext *ctx) for (int i = ctx->nanim - 1; i >= 0; i--) { BKNode *n = ctx->anim[i]; const float target = (n->flags & NF_COLLAPSED) ? 0.0f : 1.0f; + const double time = n->cls->anim_time; /* do_animate: fac = (now - starttime) / ANIMATION_TIME; fac = min(sqrt(fac), 1) */ - float fac = (float)((now - ctx->anim_start[i]) / ANIMATION_TIME); - fac = BK_MIN(sqrtf(BK_MAX(fac, 0.0f)), 1.0f); + float fac = (float)((now - ctx->anim_start[i]) / (time > 0.0 ? time : ANIMATION_TIME)); + fac = BK_MIN(time > 0.0 ? BK_MAX(fac, 0.0f) : sqrtf(BK_MAX(fac, 0.0f)), 1.0f); n->anim = fac >= 1.0f ? target : ctx->anim_from[i] + (target - ctx->anim_from[i]) * fac; if (n->anim == target) { const int last = --ctx->nanim; @@ -3769,7 +3762,21 @@ BKNode *bk_next_editable(BKNode *from, int dir) return list[(at + dir + count) % count]; } -BKNode *bk_hit_at(BKContext *ctx, int x, int y) +void bk_window_focus(BKContext *ctx, BKWindow *win, const BKWindowCallbacks *cb, int active) +{ + if (!active) { + /* pie_handler WINDEACTIVATE: the pie key may be released outside, so cancel the pie */ + ctx->key_held[0] = '\0'; + if (ctx->npopups && ctx->popups[ctx->npopups - 1]->cls == &bk_cls_pie) { + BK_close_popups(ctx); + } + } + if (cb->on_focus) { + cb->on_focus(cb->data, win, active); + } +} + +BKNode *BK_hit_at(BKContext *ctx, int x, int y) { return hit_all(ctx, x, y); } diff --git a/kit/intern/kit_draw.c b/kit/intern/kit_draw.c index 210b80b..77d8849 100644 --- a/kit/intern/kit_draw.c +++ b/kit/intern/kit_draw.c @@ -1092,6 +1092,17 @@ float bk_text_baseline(const BKRect *rect) * 0 none, 1 a sharp drop shadow (the widget style), 3 a 3x3 blurred one (panel * titles). Black at half the text's alpha, 1 px down (shady = -1). */ int bk_text_shadow = 1; +static bool g_text_shadow_on = true; /* BK_set_text_shadow */ + +void BK_set_text_shadow(int on) +{ + g_text_shadow_on = on != 0; +} + +int BK_text_shadow(void) +{ + return g_text_shadow_on; +} void bk_text_f(float x, float baseline, const char *s, int len, const float c[4], bool bold) { @@ -1106,11 +1117,12 @@ void bk_text_f(float x, float baseline, const char *s, int len, const float c[4] } BUI_raster_set_font(bk_draw.r, text_family(), bold ? BUI_FONT_BOLD : 0); const float px = floorf(x + 0.5f), py = floorf(baseline + 0.5f); - if (bk_text_shadow == 1) { + const int shadow = g_text_shadow_on ? bk_text_shadow : 0; + if (shadow == 1) { const float sc[4] = {0.0f, 0.0f, 0.0f, 0.5f * c[3]}; BUI_raster_text(bk_draw.r, px, py + 1.0f, s, len, bk_font_size(), sc); } - else if (bk_text_shadow == 3) { + else if (shadow == 3) { /* BLF's 3x3 blur kernel (1 2 1 / 2 4 2 / 1 2 1) / 16 as weighted copies */ static const float k[3] = {1.0f, 2.0f, 1.0f}; for (int dy = -1; dy <= 1; dy++) { @@ -1159,6 +1171,12 @@ float bk_line_height(void) } static int g_text_pad = -1; /* bk_text_in_pad: the margin instead of bk_pad() */ +static int g_text_ul = -1; /* bk_text_underline: the byte to underline, -1 none */ + +void bk_text_underline(int index) +{ + g_text_ul = index; +} float bk_text_in_pad(const BKRect *rect, const char *s, const uchar color[4], int align, bool bold, int pad) @@ -1221,6 +1239,13 @@ float bk_text_in(const BKRect *rect, const char *s, const uchar color[4], int al } bk_clip_push(rect->xmin, rect->ymin, bk_rw(rect), bk_rh(rect)); bk_text(x, bk_text_baseline(rect), buf, blen, color, bold); + if (g_text_ul >= 0 && g_text_ul < n) { + /* widget_draw_text: the menu key's underline, a "_" glyph centered under its character */ + const float gx = bk_text_width(buf, g_text_ul, bold); + const float gw = bk_text_width(buf, g_text_ul + 1, bold) - gx; + const float uw = bk_text_width("_", 1, bold); + bk_text(x + gx + (gw - uw) * 0.5f, bk_text_baseline(rect) + bk_px(), "_", 1, color, bold); + } bk_clip_pop(); return tw; } diff --git a/kit/intern/kit_frame.c b/kit/intern/kit_frame.c index abbf005..1916470 100644 --- a/kit/intern/kit_frame.c +++ b/kit/intern/kit_frame.c @@ -64,7 +64,9 @@ enum { HAS_W = 1, HAS_H = 2, HAS_RW = 4, HAS_RH = 8 }; void BK_geom_set(BKNode *n, const char *key, double v) { + const bool fresh = !n->geom; BKGeom *g = geom_ensure(n); + const BKGeom old = *g; const int iv = (int)floor(v + 0.5); #define K(name) (strcmp(key, name) == 0) if (K("side")) g->side = iv; @@ -94,7 +96,9 @@ void BK_geom_set(BKNode *n, const char *key, double v) else if (K("reqheight")) g->reqh = iv; else if (K("reset_place")) { g->x = g->y = g->relx = g->rely = 0; g->has = 0; } #undef K - bk_relayout(n); + if (fresh || memcmp(&old, g, sizeof(old)) != 0) { /* unchanged: no relayout */ + bk_relayout(n); + } } double BK_geom_get(const BKNode *n, const char *key) @@ -977,6 +981,28 @@ void BK_canvas_add_item(BKNode *n, const BKCanvasItem *item) canvas_store(n, item, false); } +/* Whether slot `c` already holds `it`. Image items never match: a borrowed buffer can change + * under the same pointer. */ +static bool citem_same(const CItem *c, const BKCanvasItem *it) +{ + const BKCanvasItem *o = &c->it; + if (it->type == BK_ITEM_IMAGE || o->type != it->type || o->id != it->id || + o->ncoords != it->ncoords || o->hidden != it->hidden || o->anchor != it->anchor || + o->text_flags != it->text_flags || o->clip_on != it->clip_on || o->family != it->family || + o->width != it->width || o->radius != it->radius || o->size != it->size || + o->start != it->start || o->extent != it->extent) + { + return false; + } + if (memcmp(o->fill, it->fill, sizeof(it->fill)) || memcmp(o->outline, it->outline, sizeof(it->outline)) || + memcmp(o->clip, it->clip, sizeof(it->clip)) || + memcmp(c->coords, it->coords, sizeof(float) * (size_t)it->ncoords)) + { + return false; + } + return (!o->text && !it->text) || (o->text && it->text && strcmp(o->text, it->text) == 0); +} + void BK_canvas_put_item(BKNode *n, int index, const BKCanvasItem *item) { if (n->cls != &bk_cls_canvas) { @@ -991,7 +1017,14 @@ void BK_canvas_put_item(BKNode *n, int index, const BKCanvasItem *item) } memset(&d->items[d->n++], 0, sizeof(CItem)); } + else if (citem_same(&d->items[index], item)) { + return; /* the same item: nothing to redraw */ + } + else { + canvas_damage(n, &d->items[index]); /* where it was */ + } citem_assign(&d->items[index], item); + canvas_damage(n, &d->items[index]); /* where it is */ d->idx_valid = false; /* ids may have changed */ } @@ -1002,11 +1035,11 @@ void BK_canvas_truncate(BKNode *n, int count) } CanvasData *d = CANVAS(n); for (int i = BK_MAX(0, count); i < d->n; i++) { + canvas_damage(n, &d->items[i]); citem_free(&d->items[i]); } d->n = BK_CLAMP(count, 0, d->n); d->idx_valid = false; - bk_invalidate(n); } void BK_canvas_delete(BKNode *n, int id) diff --git a/kit/intern/kit_intern.h b/kit/intern/kit_intern.h index a34a564..bf0bf5e 100644 --- a/kit/intern/kit_intern.h +++ b/kit/intern/kit_intern.h @@ -166,6 +166,8 @@ void bk_text(float x, float baseline, const char *s, int len, const uchar color[ void bk_text_f(float x, float baseline, const char *s, int len, const float c[4], bool bold); /* Draw `s` inside rect with alignment; returns drawn width. Ellipsizes. */ float bk_text_in(const BKRect *rect, const char *s, const uchar color[4], int align, bool bold); +/* Underline byte `index` of the text the next bk_text_in calls draw (-1: none). */ +void bk_text_underline(int index); float bk_text_in_pad(const BKRect *rect, const char *s, const uchar color[4], int align, bool bold, int pad); /* Text + optional icon in rect (Blender's widget_draw_text_icon). */ @@ -295,6 +297,9 @@ typedef struct BKClass { unsigned flags; /* CLS_* */ int cursor; /* hover cursor */ int drag_cursor; /* while dragged (BK_CURSOR_NONE = continuous grab) */ + /* bk_anim_start: seconds to animate n->anim to its target, linearly; 0: a panel's + * ANIMATION_TIME, eased (do_animate) */ + double anim_time; int drag_group; /* drag-toggle group (0 = none) */ void (*init)(BKNode *n); void (*free_ext)(BKNode *n); @@ -311,6 +316,7 @@ typedef struct BKClass { int (*move)(BKNode *n, BKEvent *e); int (*leave)(BKNode *n); int (*key)(BKNode *n, BKEvent *e); + int (*key_up)(BKNode *n, BKEvent *e); /* a key release, sent to the top popup */ int (*wheel)(BKNode *n, BKEvent *e); int (*cursor_at)(BKNode *n, int x, int y); void (*finish_edit)(BKNode *n, bool confirm); @@ -472,6 +478,8 @@ void bk_natural(BKNode *n, int *r_w, int *r_h); void bk_arrange(BKNode *n, int x, int y, int w, int h); void bk_draw_tree(BKNode *n); BKNode *bk_hit(BKNode *n, int x, int y); +/* Window hosts: the window got or lost the keyboard focus. Calls cb->on_focus. */ +void bk_window_focus(BKContext *ctx, BKWindow *win, const BKWindowCallbacks *cb, int active); bool bk_contains(const BKNode *n, int x, int y); BKRect bk_node_rect(const BKNode *n); bool bk_hovered(const BKNode *n); @@ -565,6 +573,7 @@ struct BKContext { int press_x, press_y, off_x, off_y, skip_x, skip_y; int ungrab_x, ungrab_y; /* where a continuous drag puts the pointer back */ int press_button; /* the button that started the grab (1, 2, 3) */ + char key_held[32]; /* wmWindow.pie_event_type_last: the key down now, "" = none */ bool has_ungrab; bool has_skip; int pointer_x, pointer_y; @@ -626,7 +635,6 @@ void bk_undo_record(BKNode *n, int kind); void bk_anim_start(BKNode *n); BKNode *bk_next_editable(BKNode *from, int dir); void bk_event(BKNode *n, int kind, int mods, const BKEvent *e); -BKNode *bk_hit_at(BKContext *ctx, int x, int y); bool bk_anim_running(const BKNode *n); void bk_edit_next(BKNode *from, int dir); int bk_textarea_set_text(BKNode *n, const char *s); diff --git a/kit/intern/kit_menus.c b/kit/intern/kit_menus.c index a0b66e2..1bf0843 100644 --- a/kit/intern/kit_menus.c +++ b/kit/intern/kit_menus.c @@ -870,12 +870,25 @@ const char *BK_menu_row_text(BKNode *menu, int i) static const int g_pie_rdir[8] = {6, 2, 4, 0, 7, 1, 5, 3}; static const short g_pie_angle[8] = {90, 45, 0, 315, 270, 225, 180, 135}; +/* pie_menu_begin / pie_handler (interface_handlers.cc): U.pie_tap_timeout (20 cs) and + * PIE_CLICK_THRESHOLD_SQ (interface_intern.hh). */ +#define PIE_TAP_TIMEOUT_S 0.2 +#define PIE_MAX_ITEMS 8 +#define PIE_ANIMATION_TIMEOUT_S 0.06 /* U.pie_animation_timeout (6 cs) */ +#define PIE_CLICK_THRESHOLD_SQ 50.0f + typedef struct PieData { MenuData menu; /* items in menu.all */ float cx, cy; int at_x, at_y; - float dir[2]; /* pie_data->pie_dir: normalized pointer direction, y-up */ - bool dir_valid; /* !(flags & PIE_INVALID_DIR) */ + int init_x, init_y; /* pie_center_init: the pointer at open */ + float dir[2]; /* pie_data->pie_dir: normalized pointer direction, y-up */ + bool dir_valid; /* !(flags & PIE_INVALID_DIR) */ + bool click_style; /* PIE_CLICK_STYLE: pick on click, not on the key release */ + bool drag_style; /* PIE_DRAG_STYLE: moved, so a quick release still picks */ + char key[32]; /* pie_data->event_type: the key that opened the pie */ + char accel[PIE_MAX_ITEMS]; /* Button.menu_key: 'a' .. 'z', or 0 */ + double opened; /* bk_time at open (the scroll timer's time_duration) */ } PieData; #define PIE(n) ((PieData *)(n)->ext) @@ -914,8 +927,10 @@ static BKRect pie_item_rect(BKNode *n, int i) pie_rdir_vec(g_pie_rdir[i], vec); const float fx = vec[0] > 0.01f ? 0.0f : (vec[0] < -0.01f ? -1.0f : -0.5f); const float fy = vec[1] > 0.99f ? 0.0f : (vec[1] < -0.99f ? -1.0f : -0.5f); - const float x = pd->cx + vec[0] * pie_radius() + fx * w; - const float y_up = vec[1] * pie_radius() + fy * h; /* bottom edge relative to center, y-up */ + /* pie_handler's animation: the item's center moves out from the pie center by n->anim */ + const float fac = n->anim; + const float x = pd->cx + (vec[0] * pie_radius() + (fx + 0.5f) * w) * fac - 0.5f * w; + const float y_up = (vec[1] * pie_radius() + (fy + 0.5f) * h) * fac - 0.5f * h; return bk_rect((int)x, (int)(pd->cy - y_up - h), (int)w, (int)h); } @@ -961,7 +976,7 @@ static bool pie_isect_seg(PieData *pd, int i, int mask) static int pie_dir_hot(BKNode *n, int x, int y) { PieData *pd = PIE(n); - const int count = BK_MIN(8, pd->menu.nall); + const int count = pd->menu.nall; const float dx = x - pd->cx, dy = pd->cy - y; /* y-up */ const float len = hypotf(dx, dy); pd->dir_valid = len >= pie_threshold(); @@ -1004,10 +1019,22 @@ static void pie_pick(BKNode *n, int i) if (!ctx) { return; } - if (i < 0 || i >= BK_MIN(8, pd->menu.nall) || !pd->menu.all[i].enabled) { + if (i < 0 || i >= pd->menu.nall || !pd->menu.all[i].enabled) { BK_close_popups(ctx); return; } + if (pd->menu.all[i].nsub) { + /* pie_menu_level_invoke / wm.call_menu_pie: the sub-pie opens at the pointer, titled by its + * item. Copied first: closing the pie frees its items. */ + const int nsub = pd->menu.all[i].nsub; + BKMenuSpec *sub = bk_menuspec_copy(pd->menu.all[i].sub, nsub); + char *title = bk_strdup(pd->menu.all[i].text); + BK_close_popups(ctx); + BK_open_pie(ctx, sub, nsub, title, PLACE_NONE, PLACE_NONE); + bk_menuspec_free(sub, nsub); + free(title); + return; + } void (*fn)(void *) = pd->menu.all[i].fn; void *data = pd->menu.all[i].data, *ref = pd->menu.all[i].ref; bk_retain(ref); @@ -1024,6 +1051,10 @@ static void pie_pick(BKNode *n, int i) static int pie_move(BKNode *n, BKEvent *e) { PieData *pd = PIE(n); + const float ix = (float)(e->x - pd->init_x), iy = (float)(e->y - pd->init_y); + if (!pd->click_style && ix * ix + iy * iy > PIE_CLICK_THRESHOLD_SQ) { + pd->drag_style = true; + } const bool was_valid = pd->dir_valid; const int hot = pie_dir_hot(n, e->x, e->y); /* the center sector tracks the pointer direction, so redraw whenever it is shown */ @@ -1060,8 +1091,30 @@ static int pie_press(BKNode *n, BKEvent *e) static int pie_key(BKNode *n, BKEvent *e) { - if (e->text && e->text[0] >= '1' && e->text[0] <= '8' && !e->text[1]) { - pie_pick(n, e->text[0] - '1'); + PieData *pd = PIE(n); + if (!pd->click_style && bk_keysym_is(e->keysym, pd->key)) { + return 1; /* auto-repeat of the held pie key */ + } + /* CASE_NUM_TO_DIR: the number keys are the numpad's directions (5 is the centre) */ + static const int num_rdir[10] = {-1, 5, 4, 3, 6, -1, 2, 7, 0, 1}; + if (e->text && e->text[0] >= '1' && e->text[0] <= '9' && !e->text[1]) { + const int rdir = num_rdir[e->text[0] - '0']; + for (int i = 0; i < pd->menu.nall; i++) { + if (rdir >= 0 && g_pie_rdir[i] == rdir) { + pie_pick(n, i); /* block_pie_dir_activate */ + return 1; + } + } + return 1; + } + /* EVT_AKEY .. EVT_ZKEY with no modifier but Alt: the item with that menu key */ + if (e->text && isalpha((uchar)e->text[0]) && !e->text[1] && !(e->mods & (BK_SHIFT | BK_CTRL))) { + for (int i = 0; i < pd->menu.nall; i++) { + if (pd->accel[i] == tolower((uchar)e->text[0])) { + pie_pick(n, i); + return 1; + } + } return 1; } if (bk_keysym_is(e->keysym, "Return") || bk_keysym_is(e->keysym, "KP_Enter")) { @@ -1071,6 +1124,64 @@ static int pie_key(BKNode *n, BKEvent *e) return 0; } +/* pie_handler, event_type release: a quick tap without a drag keeps the pie open as a click + * pie, a later release picks the item in the pointer direction (none: cancel). */ +static int pie_key_up(BKNode *n, BKEvent *e) +{ + PieData *pd = PIE(n); + if (pd->click_style || !bk_keysym_is(e->keysym, pd->key)) { + return 0; + } + if (bk_time() - pd->opened < PIE_TAP_TIMEOUT_S && !pd->drag_style) { + pd->click_style = true; + return 1; + } + pie_pick(n, pie_dir_hot(n, e->x, e->y)); + return 1; +} + +/* menu_block_set_keyaccels (interface.cc): the first free first letter of a word, then in a + * second pass any free letter. */ +static void pie_set_keyaccels(PieData *pd) +{ + unsigned mask = 0; + for (int pass = 0; pass < 2; pass++) { + for (int i = 0; i < pd->menu.nall; i++) { + const char *s = pd->menu.all[i].text; + if (pd->accel[i] || !s || !s[0] || !strcmp(s, "-")) { + continue; + } + do { + const int c = tolower((uchar)*s); + if (c >= 'a' && c <= 'z' && !(mask & 1u << (c - 'a'))) { + mask |= 1u << (c - 'a'); + pd->accel[i] = (char)c; + break; + } + if (pass == 0) { /* first pass: skip to the next word */ + while (isalpha((uchar)*s)) { + s++; + } + if (*s) { + s++; + } + } + else { + s++; + } + } while (*s); + } + } +} + +/* widget_draw_text: the accelerator's position in the text, upper case first; -1 none. */ +static int pie_accel_index(const char *text, char accel) +{ + const char *p = accel ? strchr(text, accel - 32) : NULL; + p = p ? p : (accel ? strchr(text, accel) : NULL); + return p ? (int)(p - text) : -1; +} + static void pie_draw(BKNode *n) { PieData *pd = PIE(n); @@ -1084,7 +1195,7 @@ static void pie_draw(BKNode *n) pie_draw_disk(pd->cx, pd->cy, 0.0f, 2.0f * (float)M_PI, r_in, r_out, c); if (pd->dir_valid) { int mask = 0; - for (int i = 0; i < BK_MIN(8, pd->menu.nall); i++) { + for (int i = 0; i < pd->menu.nall; i++) { mask |= 1 << g_pie_rdir[i]; } const bool has_aligned = (mask & 0x55) != 0, has_diagonal = (mask & 0xAA) != 0; @@ -1104,7 +1215,7 @@ static void pie_draw(BKNode *n) bk_widget_menu_back(&tr, CNR_ALL); bk_text_in(&tr, pd->menu.title, bk_theme.menu_back.text, BK_ALIGN_CENTER, false); } - for (int i = 0; i < BK_MIN(8, pd->menu.nall); i++) { + for (int i = 0; i < pd->menu.nall; i++) { BKRect r = pie_item_rect(n, i); const BKMenuSpec *it = &pd->menu.all[i]; int st = (i == n->hot ? ST_HOVER : 0) | (it->enabled ? 0 : ST_DISABLED); @@ -1114,9 +1225,19 @@ static void pie_draw(BKNode *n) memcpy(w.text, w.text_sel, 3); memcpy(w.inner, w.item, 4); } - bk_shadow(&r, bk_widget_radius(&w)); + if (n->anim < 1.0f) { /* widget_menu_pie_itembut: alphafac while the pie animates */ + uchar *alpha[] = {&w.inner[3], &w.inner_sel[3], &w.item[3], + &w.text[3], &w.text_sel[3], &w.outline[3]}; + for (int k = 0; k < (int)(sizeof(alpha) / sizeof(*alpha)); k++) { + *alpha[k] = (uchar)(*alpha[k] * n->anim); + } + } + else { + bk_shadow(&r, bk_widget_radius(&w)); + } BKRect wr = r; bk_widget_but(&w, &wr, 0, CNR_ALL); + bk_text_underline(it->text ? pie_accel_index(it->text, pd->accel[i]) : -1); if (it->icon && it->text && it->text[0]) { /* BUT_ICON_LEFT pie item (widget_draw_text_icon): rect.xmin += 0.3 * widget_unit, icon * at xmin + 4 * scale, text centered in the space after icon_size + 2 * scale. */ @@ -1130,6 +1251,7 @@ static void pie_draw(BKNode *n) else { bk_text_icon(&r, it->text, it->icon, w.text, BK_ALIGN_CENTER, it->enabled); } + bk_text_underline(-1); } } @@ -1143,27 +1265,53 @@ static void pie_free(BKNode *n) const BKClass bk_cls_pie = {.name = "pie", .flags = CLS_INTERACTIVE | CLS_POPUP, + .anim_time = PIE_ANIMATION_TIMEOUT_S, .free_ext = pie_free, .arrange = pie_arrange, .draw = pie_draw, .hit = pie_hit, .press = pie_press, .move = pie_move, - .key = pie_key}; - -void BK_open_pie(BKContext *ctx, const BKMenuSpec *items, int n, const char *title) -{ + .key = pie_key, + .key_up = pie_key_up}; + +void BK_open_pie( + BKContext *ctx, const BKMenuSpec *items, int n, const char *title, int at_x, int at_y) +{ + const bool at = at_x > PLACE_NONE && at_y > PLACE_NONE; + /* pie_menu_level_create: past PIE_MAX_ITEMS, the last slot is "More" with the rest */ + BKMenuSpec level[PIE_MAX_ITEMS]; + if (n > PIE_MAX_ITEMS) { + memcpy(level, items, sizeof(BKMenuSpec) * (PIE_MAX_ITEMS - 1)); + level[PIE_MAX_ITEMS - 1] = (BKMenuSpec){.text = "More", + .icon = BK_icon_id("PLUS"), + .checked = -1, + .enabled = 1, + .sub = items + PIE_MAX_ITEMS - 1, + .nsub = n - (PIE_MAX_ITEMS - 1)}; + items = level; + n = PIE_MAX_ITEMS; + } BKNode *pie = bk_node_new_cls(&bk_cls_pie); PieData *pd = (PieData *)calloc(1, sizeof(PieData)); place_init(&pd->menu.place); - pd->menu.all = bk_menuspec_copy(items, BK_MIN(n, 8)); - pd->menu.nall = BK_MIN(n, 8); + pd->menu.all = bk_menuspec_copy(items, n); + pd->menu.nall = n; + pie_set_keyaccels(pd); pd->menu.title = title ? bk_strdup(title) : NULL; - pd->at_x = ctx->pointer_x; - pd->at_y = ctx->pointer_y; + pd->at_x = at ? at_x : ctx->pointer_x; + pd->at_y = at ? at_y : ctx->pointer_y; + pd->init_x = ctx->pointer_x; + pd->init_y = ctx->pointer_y; + /* pie_menu_begin: opened while a key is down, its release picks; else click style */ + snprintf(pd->key, sizeof(pd->key), "%s", ctx->key_held); + pd->click_style = !pd->key[0]; + pd->opened = bk_time(); pie->ext = pd; pie->hot = -1; + pie->anim = 0.0f; /* items grow out of the center (none with reduced motion) */ bk_open_popup(ctx, pie, ctx->root, NULL); + bk_anim_start(pie); } /** \} */ diff --git a/kit/intern/kit_widgets.c b/kit/intern/kit_widgets.c index 123b591..1cc85d8 100644 --- a/kit/intern/kit_widgets.c +++ b/kit/intern/kit_widgets.c @@ -2054,7 +2054,7 @@ static int multi_drag(BKNode *n, BKEvent *e) /* DRAG_MULTINUM_THRESHOLD_DRAG_X: UI_UNIT_Y / 4 */ const int threshold = BK_MAX(3, (int)(0.25f * u())); if (n->dragmode == 2) { /* collecting */ - BKNode *under = bk_hit_at(ctx, n->x + n->w / 2, e->y); + BKNode *under = BK_hit_at(ctx, n->x + n->w / 2, e->y); if (under && under != n && is_number(under) && NODE_ENABLED(under) && !(under->flags & NF_SELECTED) && ctx->nmulti < BK_MAX_MULTI) { diff --git a/kit/intern/kit_window_cocoa.m b/kit/intern/kit_window_cocoa.m index 920e65e..d54c09e 100644 --- a/kit/intern/kit_window_cocoa.m +++ b/kit/intern/kit_window_cocoa.m @@ -331,7 +331,8 @@ - (void)cursorUpdate:(NSEvent *)event } } -/* The raster as a CGImage: 0x00RRGGBB words are little-endian XRGB. */ +/* The raster as a CGImage: 0x00RRGGBB words are little-endian XRGB, and 0xAARRGGBB words + * (a transparent window) premultiplied little-endian ARGB. */ - (void)drawRect:(NSRect)dirty { CGContextRef cg = [[NSGraphicsContext currentContext] CGContext]; @@ -341,11 +342,17 @@ - (void)drawRect:(NSRect)dirty const NSRect b = [self bounds]; BuiRaster *r = bk ? bk->raster : NULL; const int iw = r ? BUI_raster_width(r) : 0, ih = r ? BUI_raster_height(r) : 0; + const bool alpha = r && BUI_raster_alpha(r); const CGFloat px = bk ? bk->px : 1.0; const CGFloat wpt = iw / px, hpt = ih / px; if (!r || iw <= 1 || ih <= 1 || wpt < b.size.width || hpt < b.size.height) { - CGContextSetRGBFillColor(cg, 0.188, 0.188, 0.188, 1.0); /* Blender's background */ - CGContextFillRect(cg, NSRectToCGRect(dirty)); + if (alpha) { + CGContextClearRect(cg, NSRectToCGRect(dirty)); + } + else { + CGContextSetRGBFillColor(cg, 0.188, 0.188, 0.188, 1.0); /* Blender's background */ + CGContextFillRect(cg, NSRectToCGRect(dirty)); + } } if (!r || iw <= 1 || ih <= 1) { return; @@ -358,7 +365,8 @@ - (void)drawRect:(NSRect)dirty 32, (size_t)iw * 4, srgb(), - (CGBitmapInfo)kCGImageAlphaNoneSkipFirst | + (CGBitmapInfo)(alpha ? kCGImageAlphaPremultipliedFirst : + kCGImageAlphaNoneSkipFirst) | kCGBitmapByteOrder32Little, prov, NULL, @@ -616,6 +624,14 @@ - (void)windowDidBecomeKey:(NSNotification *)notification { if (bk) { bk->flags = [NSEvent modifierFlags]; /* modifiers may have changed elsewhere */ + bk_window_focus(bk->ctx, bk, &bk->cb, 1); + } +} + +- (void)windowDidResignKey:(NSNotification *)notification +{ + if (bk) { + bk_window_focus(bk->ctx, bk, &bk->cb, 0); } } @@ -1011,6 +1027,9 @@ static void present(BKWindow *win, int all) } } [v displayIfNeeded]; + if (BUI_raster_alpha(win->raster)) { + [win->nswin invalidateShadow]; /* the shadow follows the drawn alpha */ + } } static void do_render(BKWindow *win) @@ -1597,6 +1616,16 @@ void BK_window_wm(BKWindow *win, const char *key, double a, double b) else if (!strcmp(key, "alpha")) { [nw setAlphaValue:BK_CLAMP(a, 0.0, 1.0)]; } + else if (!strcmp(key, "transparent")) { /* per-pixel alpha: a clear, non-opaque window */ + const int on = a != 0.0; + if (on != BUI_raster_alpha(win->raster)) { + BUI_raster_set_alpha(win->raster, on); + [nw setOpaque:!on]; + [nw setBackgroundColor:on ? [NSColor clearColor] : [NSColor windowBackgroundColor]]; + win->full = 1; + host_request(win); + } + } else if (!strcmp(key, "override")) { win->override = a != 0.0; [nw setStyleMask:win->override ? NSWindowStyleMaskBorderless : win->style]; diff --git a/kit/intern/kit_window_wayland.c b/kit/intern/kit_window_wayland.c index 06b9bb7..8159a50 100644 --- a/kit/intern/kit_window_wayland.c +++ b/kit/intern/kit_window_wayland.c @@ -4,7 +4,8 @@ * Modelled on Blender's GHOST_SystemWayland / GHOST_WindowWayland: xdg-shell toplevels with * server-side decorations when the compositor offers them, a wl_seat with xkbcommon for keys * (key repeat is ours, the compositor only sends the rate and delay), the wl_pointer, wl_data_device - * for the clipboard, wl_cursor for cursors, buffer scale from the output. libwayland-client and + * for the clipboard, cursor-shape for cursors (wl_cursor where the compositor has no cursor-shape), + * buffer scale from the output. libwayland-client and * libwayland-cursor come through Blender's wayland_dynload (extern/wayland_dynload, loaded when the * first window opens) and libxkbcommon is dlopen()ed here, so nothing is linked and a build runs * where Wayland is absent. @@ -28,6 +29,10 @@ # include +# include "wlr-layer-shell-unstable-v1-client-protocol.h" +# ifdef BUI_WL_CURSOR_SHAPE +# include "cursor-shape-v1-client-protocol.h" +# endif # include "xdg-decoration-unstable-v1-client-protocol.h" # include "xdg-shell-client-protocol.h" @@ -115,9 +120,15 @@ struct BKWindow { struct xdg_surface *xdg_surface; struct xdg_toplevel *toplevel; struct zxdg_toplevel_decoration_v1 *decoration; + struct zwlr_layer_surface_v1 *layer_surface; struct wl_callback *frame_cb; Buf bufs[2]; - int open, configured; + char *title; + int open, configured, hidden; + bool frameless; /* "override": no server-side frame (BUI draws none either) */ + /* "layer": a layer-shell layer (0 background .. 3 overlay), -1 a toplevel; its keyboard + * interactivity; "anchor": edges (top 1, bottom 2, left 4, right 8), exclusive zone */ + int layer, keyboard, anchor, exclusive; int render_pending, frames, cursor, tip_id, next_timer, full; int want_frame, frame_vsync; double frame_start, hold_until; @@ -145,6 +156,11 @@ static struct { struct wl_shm *shm; struct xdg_wm_base *wm_base; struct zxdg_decoration_manager_v1 *decoration_manager; + struct zwlr_layer_shell_v1 *layer_shell; +# ifdef BUI_WL_CURSOR_SHAPE + struct wp_cursor_shape_manager_v1 *cursor_shape_manager; + struct wp_cursor_shape_device_v1 *cursor_shape_device; /* of the pointer, made on first use */ +# endif struct wl_seat *seat; struct wl_pointer *pointer; struct wl_keyboard *keyboard; @@ -244,7 +260,10 @@ static bool buf_create(BKWindow *win, Buf *b, int w, int h) return false; } struct wl_shm_pool *pool = wl_shm_create_pool(g.shm, fd, (int32_t)size); - b->buffer = wl_shm_pool_create_buffer(pool, 0, w, h, stride, WL_SHM_FORMAT_XRGB8888); + /* ARGB8888 is premultiplied: the raster's frame when it keeps alpha (transparent) */ + b->buffer = wl_shm_pool_create_buffer(pool, 0, w, h, stride, + BUI_raster_alpha(win->raster) ? WL_SHM_FORMAT_ARGB8888 : + WL_SHM_FORMAT_XRGB8888); wl_shm_pool_destroy(pool); close(fd); wl_buffer_add_listener(b->buffer, &buffer_listener, win); @@ -306,11 +325,10 @@ static void present(BKWindow *win, Buf *b, int all) const uint32_t *px = BUI_raster_pixels(win->raster); const int(*rects)[4] = NULL; const int n = all ? 0 : BUI_raster_damage(win->raster, &rects); + const bool damaged_all = all || n > MAX_RECTS; /* this buffer holds the frame before the last: it needs that frame's damage too */ - const bool whole = all || !b->valid || win->last_full || n > MAX_RECTS; - if (whole) { + if (damaged_all || !b->valid || win->last_full) { memcpy(b->data, px, (size_t)w * (size_t)h * 4); - wl_surface_damage_buffer(win->surface, 0, 0, w, h); } else { for (int i = 0; i < win->last_n; i++) { @@ -318,11 +336,18 @@ static void present(BKWindow *win, Buf *b, int all) } for (int i = 0; i < n; i++) { copy_rect(b, px, w, rects[i]); - wl_surface_damage_buffer(win->surface, rects[i][0], rects[i][1], rects[i][2], rects[i][3]); } } - win->last_full = whole; - win->last_n = whole ? 0 : n; + /* the surface changed only where this frame's raster did */ + if (damaged_all) { + wl_surface_damage_buffer(win->surface, 0, 0, w, h); + } + for (int i = 0; !damaged_all && i < n; i++) { + wl_surface_damage_buffer(win->surface, rects[i][0], rects[i][1], rects[i][2], rects[i][3]); + } + /* the frame's own damage, not the copy: a full copy must not make every later copy full */ + win->last_full = damaged_all; + win->last_n = damaged_all ? 0 : n; for (int i = 0; i < win->last_n; i++) { memcpy(win->last_rects[i], rects[i], sizeof(int) * 4); } @@ -442,22 +467,64 @@ static void send_key(BKWindow *win, uint32_t evdev, bool press) /* -- seat: pointer -- */ -/* The cursors are xcursor names, as GHOST_SystemWayland's cursor table (X11's shapes). */ +# ifdef BUI_WL_CURSOR_SHAPE +/* BK_CURSOR_* as cursor-shape shapes, from gwl_seat_cursor_find_wl_shape_from_ghost. */ +static bool apply_cursor_shape(int c) +{ + static const uint32_t shapes[BK_CURSOR_PROGRESS + 1] = { + WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE, + WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE, WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT, + WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR, WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER, + WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE, WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, + WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE, WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE, + WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED, WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_WAIT, + WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_PROGRESS}; + if (!g.cursor_shape_manager) { + return false; + } + if (!g.cursor_shape_device) { + g.cursor_shape_device = wp_cursor_shape_manager_v1_get_pointer(g.cursor_shape_manager, + g.pointer); + } + wp_cursor_shape_device_v1_set_shape(g.cursor_shape_device, g.pointer_serial, shapes[c]); + return true; +} +# endif + +/* The cursor theme, loaded on first use: loading reads every cursor in the theme. */ +static bool cursor_theme_ensure(void) +{ + if (!g.cursor_theme && wayland_dynload_cursor_init(false)) { + const char *size = getenv("XCURSOR_SIZE"); + g.cursor_theme = wl_cursor_theme_load(getenv("XCURSOR_THEME"), + (size ? atoi(size) : 24) * g.cursor_scale, g.shm); + g.cursor_surface = wl_compositor_create_surface(g.compositor); + } + return g.cursor_theme && g.cursor_surface; +} + +/* Without cursor-shape: xcursor names, as GHOST_SystemWayland's cursor table (X11's shapes). */ static void apply_cursor(BKWindow *win) { if (!g.pointer || g.pointer_win != win) { return; } - static const char *names[12] = {"left_ptr", "sb_h_double_arrow", "sb_v_double_arrow", - "xterm", "crosshair", "hand2", - "fleur", "left_ptr", "bottom_left_corner", - "bottom_right_corner", "X_cursor", "watch"}; - const int c = BK_CLAMP(win->cursor, 0, 11); + static const char *names[BK_CURSOR_PROGRESS + 1] = { + "left_ptr", "sb_h_double_arrow", "sb_v_double_arrow", "xterm", + "crosshair", "hand2", "fleur", "left_ptr", + "bottom_left_corner", "bottom_right_corner", "X_cursor", "watch", + "watch"}; + const int c = BK_CLAMP(win->cursor, 0, BK_CURSOR_PROGRESS); if (c == BK_CURSOR_NONE) { wl_pointer_set_cursor(g.pointer, g.pointer_serial, NULL, 0, 0); return; } - if (!g.cursor_theme || !g.cursor_surface) { +# ifdef BUI_WL_CURSOR_SHAPE + if (apply_cursor_shape(c)) { + return; + } +# endif + if (!cursor_theme_ensure()) { return; } struct wl_cursor *cursor = wl_cursor_theme_get_cursor(g.cursor_theme, names[c]); @@ -597,11 +664,18 @@ static void keyboard_enter(void *d, struct wl_keyboard *kb, uint32_t serial, str { g.keyboard_win = find_window(s); g.serial = serial; + if (g.keyboard_win) { + bk_window_focus(g.keyboard_win->ctx, g.keyboard_win, &g.keyboard_win->cb, 1); + } } static void keyboard_leave(void *d, struct wl_keyboard *kb, uint32_t serial, struct wl_surface *s) { - if (g.keyboard_win == find_window(s)) { + BKWindow *win = find_window(s); + if (win) { + bk_window_focus(win->ctx, win, &win->cb, 0); + } + if (g.keyboard_win == win) { g.keyboard_win = NULL; } g.repeat_key = 0; @@ -852,6 +926,15 @@ static void registry_global(void *d, struct wl_registry *reg, uint32_t name, con else if (!strcmp(iface, zxdg_decoration_manager_v1_interface.name)) { g.decoration_manager = wl_registry_bind(reg, name, &zxdg_decoration_manager_v1_interface, 1); } + else if (!strcmp(iface, zwlr_layer_shell_v1_interface.name)) { + g.layer_shell = wl_registry_bind(reg, name, &zwlr_layer_shell_v1_interface, + BK_MIN(version, 4u)); + } +# ifdef BUI_WL_CURSOR_SHAPE + else if (!strcmp(iface, wp_cursor_shape_manager_v1_interface.name)) { + g.cursor_shape_manager = wl_registry_bind(reg, name, &wp_cursor_shape_manager_v1_interface, 1); + } +# endif else if (!strcmp(iface, wl_seat_interface.name) && !g.seat) { g.seat = wl_registry_bind(reg, name, &wl_seat_interface, BK_MIN(version, 5u)); wl_seat_add_listener(g.seat, &seat_listener, NULL); @@ -937,12 +1020,6 @@ static int open_display(void) for (int i = 0; i < g.noutputs; i++) { g.cursor_scale = BK_MAX(g.cursor_scale, g.outputs[i].scale); } - if (wayland_dynload_cursor_init(false)) { - const char *size = getenv("XCURSOR_SIZE"); - g.cursor_theme = wl_cursor_theme_load(getenv("XCURSOR_THEME"), - (size ? atoi(size) : 24) * g.cursor_scale, g.shm); - g.cursor_surface = wl_compositor_create_surface(g.compositor); - } g.repeat_rate = 25; g.repeat_delay = 600; state = 1; @@ -1016,6 +1093,33 @@ static void host_emit(void *data, BKNode *node, int kind, void *ref, double a, d /* -- xdg-shell -- */ +static void role_destroy(BKWindow *win) +{ + if (win->frame_cb) { + wl_callback_destroy(win->frame_cb); + win->frame_cb = NULL; + } + if (win->decoration) { + zxdg_toplevel_decoration_v1_destroy(win->decoration); + } + if (win->toplevel) { + xdg_toplevel_destroy(win->toplevel); + } + if (win->layer_surface) { + zwlr_layer_surface_v1_destroy(win->layer_surface); + win->layer_surface = NULL; + } + if (win->xdg_surface) { + xdg_surface_destroy(win->xdg_surface); + } + win->decoration = NULL; + win->toplevel = NULL; + win->xdg_surface = NULL; + win->configured = 0; +} + +static void role_create(BKWindow *win); + static void close_window(BKWindow *win) { if (!win->open) { @@ -1029,21 +1133,10 @@ static void close_window(BKWindow *win) g.keyboard_win = NULL; g.repeat_key = 0; } - if (win->frame_cb) { - wl_callback_destroy(win->frame_cb); - win->frame_cb = NULL; - } - if (win->decoration) { - zxdg_toplevel_decoration_v1_destroy(win->decoration); - } - xdg_toplevel_destroy(win->toplevel); - xdg_surface_destroy(win->xdg_surface); + role_destroy(win); wl_surface_destroy(win->surface); buf_destroy(&win->bufs[0]); buf_destroy(&win->bufs[1]); - win->decoration = NULL; - win->toplevel = NULL; - win->xdg_surface = NULL; win->surface = NULL; g_open--; wl_display_flush(g.display); @@ -1060,11 +1153,10 @@ static void xdg_surface_configure(void *d, struct xdg_surface *s, uint32_t seria static struct xdg_surface_listener xdg_surface_listener = {.configure = xdg_surface_configure}; -static void toplevel_configure(void *d, struct xdg_toplevel *tl, int32_t w, int32_t h, - struct wl_array *states) +/* A configure's size in logical units; 0: the client picks. */ +static void configure_size(BKWindow *win, int32_t w, int32_t h) { - BKWindow *win = (BKWindow *)d; - if (w > 0 && h > 0 && (w != win->lw || h != win->lh)) { /* 0: the client picks */ + if (w > 0 && h > 0 && (w != win->lw || h != win->lh)) { win->lw = w; win->lh = h; win->w = w * win->bscale; @@ -1073,14 +1165,58 @@ static void toplevel_configure(void *d, struct xdg_toplevel *tl, int32_t w, int3 } } -static void toplevel_close(void *d, struct xdg_toplevel *tl) +static void close_requested(BKWindow *win) { - BKWindow *win = (BKWindow *)d; if (!win->cb.on_close || win->cb.on_close(win->cb.data, win)) { close_window(win); } } +static void toplevel_configure(void *d, struct xdg_toplevel *tl, int32_t w, int32_t h, + struct wl_array *states) +{ + configure_size((BKWindow *)d, w, h); +} + +static void toplevel_close(void *d, struct xdg_toplevel *tl) +{ + close_requested((BKWindow *)d); +} + +/* -- wlr-layer-shell -- */ + +static void layer_configure(void *d, struct zwlr_layer_surface_v1 *ls, uint32_t serial, + uint32_t w, uint32_t h) +{ + BKWindow *win = (BKWindow *)d; + zwlr_layer_surface_v1_ack_configure(ls, serial); + configure_size(win, (int32_t)w, (int32_t)h); + win->configured = 1; + win->render_pending = 1; +} + +static void layer_closed(void *d, struct zwlr_layer_surface_v1 *ls) +{ + close_requested((BKWindow *)d); +} + +static struct zwlr_layer_surface_v1_listener layer_listener = {.configure = layer_configure, + .closed = layer_closed}; + +/* Size, anchor and exclusive zone. An axis anchored at both edges takes the output's size (0). */ +static void layer_place(BKWindow *win) +{ + const int a = win->anchor; + const bool fill_x = (a & ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT) && + (a & ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); + const bool fill_y = (a & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP) && + (a & ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM); + zwlr_layer_surface_v1_set_size(win->layer_surface, fill_x ? 0 : (uint32_t)win->lw, + fill_y ? 0 : (uint32_t)win->lh); + zwlr_layer_surface_v1_set_anchor(win->layer_surface, (uint32_t)a); + zwlr_layer_surface_v1_set_exclusive_zone(win->layer_surface, win->exclusive); +} + static struct xdg_toplevel_listener toplevel_listener = {.configure = toplevel_configure, .close = toplevel_close}; @@ -1119,6 +1255,22 @@ static struct wl_surface_listener surface_listener = { /* -- windows -- */ +/* GHOST_WindowWayland (as SDL does): dispatch until the role's first configure, so the window can + * draw at once. A display error ends the wait. */ +static void wait_configure(BKWindow *win) +{ + while (!win->configured && wl_display_flush(g.display) != -1 && + wl_display_dispatch(g.display) != -1) + { + } +} + +static void surface_create(BKWindow *win) +{ + win->surface = wl_compositor_create_surface(g.compositor); + wl_surface_add_listener(win->surface, &surface_listener, win); +} + BKWindow *bk_wl_window_new(BKContext *ctx, const char *title, int width, int height) { if (!open_display()) { @@ -1131,20 +1283,10 @@ BKWindow *bk_wl_window_new(BKContext *ctx, const char *title, int width, int hei win->next_timer = 1; win->lw = width; win->lh = height; - win->surface = wl_compositor_create_surface(g.compositor); - wl_surface_add_listener(win->surface, &surface_listener, win); - win->xdg_surface = xdg_wm_base_get_xdg_surface(g.wm_base, win->surface); - xdg_surface_add_listener(win->xdg_surface, &xdg_surface_listener, win); - win->toplevel = xdg_surface_get_toplevel(win->xdg_surface); - xdg_toplevel_add_listener(win->toplevel, &toplevel_listener, win); - const char *app_id = getenv("BUI_APP_ID"); - xdg_toplevel_set_app_id(win->toplevel, app_id ? app_id : "bui"); - if (g.decoration_manager) { /* title bar and borders from the compositor, as GHOST asks */ - win->decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(g.decoration_manager, - win->toplevel); - zxdg_toplevel_decoration_v1_set_mode(win->decoration, - ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); - } + surface_create(win); + win->title = bk_strdup(title ? title : ""); + win->layer = -1; + role_create(win); win->open = 1; apply_scale(win, 1); /* BUI_UI_SCALE sizes the window in design pixels, as the X11 host does */ @@ -1155,15 +1297,71 @@ BKWindow *bk_wl_window_new(BKContext *ctx, const char *title, int width, int hei win->next = g_windows; g_windows = win; g_open++; - bk_wl_window_set_title(win, title); BKHost host = {win, host_request, host_set_cursor, host_warp, host_clipboard_get, host_clipboard_set, host_schedule, host_emit}; BK_context_set_host(ctx, &host); wl_surface_commit(win->surface); /* the first commit asks for the configure */ - wl_display_flush(g.display); + wait_configure(win); return win; } +/* GHOST asks for server-side decorations; "override" asks for client-side ones, of which BUI + * draws none, so the window has no frame. */ +static void set_frame(BKWindow *win) +{ + zxdg_toplevel_decoration_v1_set_mode(win->decoration, + win->frameless ? + ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE : + ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); +} + +/* The xdg_toplevel role. A new role needs a surface with no buffer, and a commit after it + * asks for the configure. */ +static void role_create(BKWindow *win) +{ + const char *app_id = getenv("BUI_APP_ID"); + if (win->layer >= 0 && g.layer_shell) { + win->layer_surface = zwlr_layer_shell_v1_get_layer_surface( + g.layer_shell, win->surface, NULL, (uint32_t)win->layer, app_id ? app_id : "bui"); + zwlr_layer_surface_v1_add_listener(win->layer_surface, &layer_listener, win); + zwlr_layer_surface_v1_set_keyboard_interactivity(win->layer_surface, + (uint32_t)win->keyboard); + layer_place(win); + return; + } + win->xdg_surface = xdg_wm_base_get_xdg_surface(g.wm_base, win->surface); + xdg_surface_add_listener(win->xdg_surface, &xdg_surface_listener, win); + win->toplevel = xdg_surface_get_toplevel(win->xdg_surface); + xdg_toplevel_add_listener(win->toplevel, &toplevel_listener, win); + xdg_toplevel_set_app_id(win->toplevel, app_id ? app_id : "bui"); + xdg_toplevel_set_title(win->toplevel, win->title); + if (g.decoration_manager) { /* title bar and borders from the compositor, as GHOST asks */ + win->decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(g.decoration_manager, + win->toplevel); + set_frame(win); + } +} + +/* Unmap (a NULL buffer, then no role) or map again with a new role. */ +static void set_hidden(BKWindow *win, int hidden) +{ + if (win->hidden == hidden) { + return; + } + win->hidden = hidden; + if (hidden) { + wl_surface_attach(win->surface, NULL, 0, 0); + wl_surface_commit(win->surface); + role_destroy(win); + } + else { + role_create(win); + wl_surface_commit(win->surface); + wait_configure(win); + win->full = 1; + } +} + void bk_wl_window_free(BKWindow *win) { if (!win) { @@ -1180,6 +1378,7 @@ void bk_wl_window_free(BKWindow *win) BK_context_set_host(win->ctx, &win->prev); } BUI_raster_free(win->raster); + free(win->title); free(win); } @@ -1191,6 +1390,7 @@ void bk_wl_window_set_callbacks(BKWindow *win, const BKWindowCallbacks *cb) void bk_wl_window_show(BKWindow *win) { if (win->open) { /* a Wayland surface shows with its first buffer, after the configure */ + set_hidden(win, 0); win->full = 1; win->render_pending = 1; } @@ -1209,8 +1409,12 @@ int bk_wl_window_is_open(BKWindow *win) void bk_wl_window_set_title(BKWindow *win, const char *title) { if (win->open) { - xdg_toplevel_set_title(win->toplevel, title ? title : ""); - wl_display_flush(g.display); + free(win->title); + win->title = bk_strdup(title ? title : ""); + if (win->toplevel) { + xdg_toplevel_set_title(win->toplevel, win->title); + wl_display_flush(g.display); + } } } @@ -1223,7 +1427,13 @@ void bk_wl_window_set_size(BKWindow *win, int width, int height) win->lh = BK_MAX(1, (int)(height * win->ui_scale / win->bscale)); win->w = win->lw * win->bscale; win->h = win->lh * win->bscale; - xdg_surface_set_window_geometry(win->xdg_surface, 0, 0, win->lw, win->lh); + if (win->xdg_surface) { + xdg_surface_set_window_geometry(win->xdg_surface, 0, 0, win->lw, win->lh); + } + if (win->layer_surface) { + layer_place(win); + wl_surface_commit(win->surface); + } win->full = 1; win->render_pending = 1; } @@ -1303,6 +1513,46 @@ void bk_wl_window_wm(BKWindow *win, const char *key, double a, double b) if (!win->open) { return; } + if (!strcmp(key, "state")) { + set_hidden(win, a == 2); /* 2: withdrawn */ + } + else if (!strcmp(key, "layer")) { + /* a wl_surface keeps its first role: a new role needs a new surface */ + const int shown = !win->hidden; + set_hidden(win, 1); + wl_surface_destroy(win->surface); + surface_create(win); + win->layer = BK_CLAMP((int)a, -1, 3); + win->keyboard = BK_CLAMP((int)b, 0, 2); + if (shown) { + set_hidden(win, 0); + } + } + else if (!strcmp(key, "anchor")) { + win->anchor = (int)a & 15; + win->exclusive = (int)b; + if (win->layer_surface) { + layer_place(win); + wl_surface_commit(win->surface); + } + } + else if (!strcmp(key, "override")) { + win->frameless = a != 0; + if (win->decoration) { + set_frame(win); + } + } + else if (!strcmp(key, "transparent") && (a != 0) != BUI_raster_alpha(win->raster)) { + BUI_raster_set_alpha(win->raster, a != 0); + buf_destroy(&win->bufs[0]); /* new buffers in the other format */ + buf_destroy(&win->bufs[1]); + win->full = 1; + win->render_pending = 1; + } + if (!win->toplevel) { + wl_display_flush(g.display); + return; + } if (!strcmp(key, "minsize")) { xdg_toplevel_set_min_size(win->toplevel, (int)(a / win->bscale), (int)(b / win->bscale)); } @@ -1339,6 +1589,11 @@ int bk_wl_window_wm_get(BKWindow *win, const char *key, double *r_a, double *r_b *r_b = g.py; return g.pointer_win != NULL; } + if (!strcmp(key, "layer")) { /* has the compositor layer shell; the window's layer */ + *r_a = g.layer_shell != NULL; + *r_b = win ? win->layer : -1; + return 1; + } return 0; /* "pos", "state": not available to a Wayland client */ } diff --git a/kit/intern/kit_window_win32.c b/kit/intern/kit_window_win32.c index 4bf7986..a6cac6d 100644 --- a/kit/intern/kit_window_win32.c +++ b/kit/intern/kit_window_win32.c @@ -22,6 +22,7 @@ # endif # define WIN32_LEAN_AND_MEAN # include +# include /* types only: the calls go through dwm_proc */ # include # include "kit_ime_win32.h" @@ -33,6 +34,11 @@ # define ID_GPU 4 /* checks the GPU device until it is ready */ # define ID_USER_BASE 1000 +/* How a transparent window shows its frame: UpdateLayeredWindow copies the software raster + * (every frame, on the CPU); LAYER_GPU presents the Direct3D 11 frames to the window and DWM + * blends their alpha (blur-behind), and falls back to LAYER_ULW without the device. */ +enum { LAYER_NONE = 0, LAYER_ULW = 1, LAYER_GPU = 2 }; + struct BKWindow { HWND hwnd; BKContext *ctx; @@ -47,12 +53,70 @@ struct BKWindow { int fixed_w, fixed_h; /* axes the user may not resize (Tk's resizable) */ int user_sizing; /* inside a user move/resize loop */ DWORD style; /* framed style restored by "override" 0 */ + int override; /* "override": no frame */ + int layered; /* "transparent": a LAYER_* way to show per-pixel alpha */ + BYTE opacity; /* "alpha" as 0..255 */ + struct { /* LAYER_ULW: the frame as a DIB section */ + HDC dc; + HBITMAP bmp, old; + uint32_t *bits; + int w, h; + } layer; BKImeWin32 ime; /* the IME (GHOST_ImeWin32) */ WPARAM last_vk; /* the virtual key of the last WM_KEYDOWN */ + struct WinTimer { /* armed timers: reopen() moves them to the new window */ + UINT_PTR id; + ULONGLONG due; /* GetTickCount64 */ + } *timers; + int ntimers; }; static int g_open = 0; +static void timer_arm(BKWindow *win, UINT_PTR id, UINT ms) +{ + int i = 0; + while (i < win->ntimers && win->timers[i].id != id) { + i++; + } + if (i == win->ntimers) { + struct WinTimer *t = realloc(win->timers, sizeof(*t) * (size_t)(i + 1)); + if (t) { + win->timers = t; + win->ntimers++; + } + else { + i = -1; /* not tracked: it only misses a reopen */ + } + } + if (i >= 0) { + win->timers[i].id = id; + win->timers[i].due = GetTickCount64() + ms; + } + SetTimer(win->hwnd, id, ms, NULL); +} + +static void timer_kill(BKWindow *win, UINT_PTR id) +{ + KillTimer(win->hwnd, id); + for (int i = 0; i < win->ntimers; i++) { + if (win->timers[i].id == id) { + win->timers[i] = win->timers[--win->ntimers]; + return; + } + } +} + +/* The window handle changed: arm the timers again on it, for their time that is left. */ +static void timers_rearm(BKWindow *win) +{ + const ULONGLONG now = GetTickCount64(); + for (int i = 0; i < win->ntimers; i++) { + const ULONGLONG due = win->timers[i].due; + SetTimer(win->hwnd, win->timers[i].id, (UINT)(due > now ? due - now : 1), NULL); + } +} + /* Wake BK_window_run's GetMessage without WM_QUIT: a quit message would also * end any other loop on this thread (e.g. a Tk mainloop in the same app). */ static void wake_loop(void) @@ -278,11 +342,11 @@ static void host_schedule(void *data, int ms, int timer) { BKWindow *win = (BKWindow *)data; if (timer == BK_TIMER_CUSTOM) { /* its own timer: tooltips keep theirs */ - SetTimer(win->hwnd, ID_WAKE, (UINT)(ms > 0 ? ms : 1), NULL); + timer_arm(win, ID_WAKE, (UINT)(ms > 0 ? ms : 1)); return; } win->tip_id = timer; - SetTimer(win->hwnd, ID_TIP, (UINT)ms, NULL); + timer_arm(win, ID_TIP, (UINT)ms); } static void host_emit(void *data, BKNode *node, int kind, void *ref, double a, double b) @@ -295,6 +359,17 @@ static void host_emit(void *data, BKNode *node, int kind, void *ref, double a, d /* -- rendering -- */ +static FARPROC dwm_proc(const char *name) +{ + static HMODULE dwm = NULL; + static int loaded = 0; + if (!loaded) { + loaded = 1; + dwm = LoadLibraryW(L"dwmapi.dll"); + } + return dwm ? GetProcAddress(dwm, name) : NULL; +} + typedef HRESULT(WINAPI *DwmFlushFn)(void); static void vsync(void) @@ -303,8 +378,7 @@ static void vsync(void) static int loaded = 0; if (!loaded) { loaded = 1; - HMODULE dwm = LoadLibraryW(L"dwmapi.dll"); - flush = dwm ? (DwmFlushFn)(void *)GetProcAddress(dwm, "DwmFlush") : NULL; + flush = (DwmFlushFn)(void *)dwm_proc("DwmFlush"); } if (flush) { flush(); @@ -332,7 +406,7 @@ static double refresh_ms(void) static void gpu_attach(BKWindow *win) { if (BUI_raster_gpu_attach(win->raster, win->hwnd, 0)) { - SetTimer(win->hwnd, ID_GPU, (UINT)refresh_ms(), NULL); + timer_arm(win, ID_GPU, (UINT)refresh_ms()); } } @@ -364,8 +438,97 @@ static void ime_sync(BKWindow *win) bk_ime_win32_begin(&win->ime, win->hwnd, s.x, s.y, s.x, s.y, s.restart != 0); } +/* The raster on screen: a layered window shows the whole frame, the others the damage. */ +static void gpu_window_check(BKWindow *win); + +static void layer_free(BKWindow *win) +{ + if (win->layer.old) { + SelectObject(win->layer.dc, win->layer.old); + } + if (win->layer.bmp) { + DeleteObject(win->layer.bmp); + } + if (win->layer.dc) { + DeleteDC(win->layer.dc); + } + memset(&win->layer, 0, sizeof(win->layer)); +} + +/* The DIB section at the frame's size; true when it was made now (its pixels are empty). */ +static bool layer_fit(BKWindow *win, int w, int h) +{ + if (win->layer.bmp && win->layer.w == w && win->layer.h == h) { + return false; + } + layer_free(win); + BITMAPINFO bmi; + memset(&bmi, 0, sizeof(bmi)); + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = w; + bmi.bmiHeader.biHeight = -h; /* top-down, like the raster */ + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + win->layer.dc = CreateCompatibleDC(NULL); + win->layer.bmp = win->layer.dc ? CreateDIBSection(win->layer.dc, &bmi, DIB_RGB_COLORS, + (void **)&win->layer.bits, NULL, 0) : + NULL; + if (!win->layer.bmp) { + layer_free(win); + return false; + } + win->layer.old = (HBITMAP)SelectObject(win->layer.dc, win->layer.bmp); + win->layer.w = w; + win->layer.h = h; + return true; +} + +/* LAYER_ULW: the changed pixels go to the DIB, which UpdateLayeredWindow then shows whole. + * The raster is premultiplied 0xAARRGGBB, as ULW_ALPHA with AC_SRC_ALPHA needs. */ +static void present_layered(BKWindow *win, int full) +{ + BuiRaster *r = win->raster; + const int w = BUI_raster_width(r), h = BUI_raster_height(r); + const uint32_t *pix = BUI_raster_pixels(r); + if (w <= 0 || h <= 0 || !pix) { + return; + } + full |= layer_fit(win, w, h); + if (!win->layer.bmp) { + return; + } + const int(*rects)[4]; + const int n = BUI_raster_damage(r, &rects); + if (full) { + memcpy(win->layer.bits, pix, (size_t)w * (size_t)h * sizeof(uint32_t)); + } + for (int i = 0; i < n && !full; i++) { + for (int y = rects[i][1]; y < rects[i][1] + rects[i][3]; y++) { + memcpy(win->layer.bits + (size_t)y * w + rects[i][0], pix + (size_t)y * w + rects[i][0], + (size_t)rects[i][2] * sizeof(uint32_t)); + } + } + const BLENDFUNCTION blend = {AC_SRC_OVER, 0, win->opacity, AC_SRC_ALPHA}; + const POINT src = {0, 0}; + SIZE size = {w, h}; /* a layered window's surface follows this size, not the window's */ + UpdateLayeredWindow(win->hwnd, NULL, NULL, &size, win->layer.dc, &src, 0, + (BLENDFUNCTION *)&blend, ULW_ALPHA); +} + +static void present(BKWindow *win, int full) +{ + if (win->layered == LAYER_ULW) { + present_layered(win, full); + } + else { + BUI_raster_present_native(win->raster, win->hwnd, full); + } +} + static void do_render(BKWindow *win) { + gpu_window_check(win); RECT rc; GetClientRect(win->hwnd, &rc); const int w = BK_MAX(1, rc.right - rc.left), h = BK_MAX(1, rc.bottom - rc.top); @@ -387,7 +550,7 @@ static void do_render(BKWindow *win) QueryPerformanceCounter(&t1); win->full = 0; if (regions || resized) { - BUI_raster_present_native(win->raster, win->hwnd, resized); + present(win, resized); win->frames++; } QueryPerformanceCounter(&t2); @@ -499,10 +662,15 @@ static LRESULT CALLBACK wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) EndPaint(hwnd, &ps); win->full = 1; do_render(win); - BUI_raster_present_native(win->raster, hwnd, 1); + present(win, 1); return 0; } case WM_ERASEBKGND: + if (win->layered == LAYER_GPU) { /* zero alpha: DWM blends the surface with what is behind */ + RECT rc; + GetClientRect(hwnd, &rc); + PatBlt((HDC)wp, 0, 0, rc.right, rc.bottom, BLACKNESS); + } return 1; case WM_SIZE: host_request(win); @@ -703,31 +871,35 @@ static LRESULT CALLBACK wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) } case WM_TIMER: if (wp == ID_TIP) { - KillTimer(hwnd, ID_TIP); + timer_kill(win, ID_TIP); BK_timer(ctx, win->tip_id); } else if (wp == ID_WAKE) { - KillTimer(hwnd, ID_WAKE); + timer_kill(win, ID_WAKE); BK_timer(ctx, BK_TIMER_CUSTOM); } else if (wp == ID_GPU) { /* a GPU device that is ready switches an idle window over too */ if (!BUI_raster_gpu_pending(win->raster)) { - KillTimer(hwnd, ID_GPU); + timer_kill(win, ID_GPU); } else if (BUI_raster_gpu_poll(win->raster)) { - KillTimer(hwnd, ID_GPU); + timer_kill(win, ID_GPU); win->full = 1; host_request(win); } + gpu_window_check(win); } else if (wp >= ID_USER_BASE) { - KillTimer(hwnd, wp); + timer_kill(win, wp); if (win->cb.on_timer) { win->cb.on_timer(win->cb.data, win, (int)(wp - ID_USER_BASE)); } } return 0; + case WM_ACTIVATE: + bk_window_focus(win->ctx, win, &win->cb, LOWORD(wp) != WA_INACTIVE); + break; case WM_CLOSE: if (win->cb.on_close && !win->cb.on_close(win->cb.data, win)) { return 0; @@ -767,6 +939,7 @@ BKWindow *BK_window_new(BKContext *ctx, const char *title, int width, int height win->prev = ctx->host; win->raster = BUI_raster_create(1, 1); win->next_timer = 1; + win->opacity = 255; win->scale = window_dpi_scale(NULL); { /* system DPI until the window exists */ @@ -816,8 +989,10 @@ void BK_window_free(BKWindow *win) if (win->ctx) { BK_context_set_host(win->ctx, &win->prev); } + layer_free(win); BUI_raster_free(win->raster); bk_ime_win32_free(&win->ime); + free(win->timers); free(win); } @@ -883,7 +1058,7 @@ BuiRaster *BK_window_raster(BKWindow *win) void BK_window_set_gpu(BKWindow *win, int on) { - if (win && on) { + if (win && on && win->layered != LAYER_ULW) { /* that way of showing alpha is software */ gpu_attach(win); host_request(win); } @@ -911,7 +1086,7 @@ int BK_window_timer(BKWindow *win, int ms) { const int id = win->next_timer++; if (win->hwnd) { - SetTimer(win->hwnd, ID_USER_BASE + (UINT_PTR)id, (UINT)BK_MAX(0, ms), NULL); + timer_arm(win, ID_USER_BASE + (UINT_PTR)id, (UINT)BK_MAX(0, ms)); } return id; } @@ -926,7 +1101,7 @@ void BK_window_request_frame(BKWindow *win, int vsync) void BK_window_cancel_timer(BKWindow *win, int id) { if (win->hwnd) { - KillTimer(win->hwnd, ID_USER_BASE + (UINT_PTR)id); + timer_kill(win, ID_USER_BASE + (UINT_PTR)id); } } @@ -943,6 +1118,123 @@ static void set_style(HWND hwnd, DWORD style) SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); } +/* No frame while "override" or "transparent" is on: a layered window is drawn from the + * raster alone. */ +static void set_frame(BKWindow *win) +{ + if (!win->style) { + win->style = (DWORD)GetWindowLongPtrW(win->hwnd, GWL_STYLE) & ~WS_VISIBLE; + } + set_style(win->hwnd, + (win->override || win->layered) ? WS_POPUP | WS_CLIPCHILDREN : win->style); +} + +/* The "alpha" of a window that is not transparent: one opacity for the whole window. */ +static void set_opacity(BKWindow *win) +{ + const LONG_PTR ex = GetWindowLongPtrW(win->hwnd, GWL_EXSTYLE); + SetWindowLongPtrW(win->hwnd, GWL_EXSTYLE, ex | WS_EX_LAYERED); + SetLayeredWindowAttributes(win->hwnd, 0, win->opacity, LWA_ALPHA); +} + +/* The same window made again with other extended styles (UpdateLayeredWindow fails once + * SetLayeredWindowAttributes was used: WS_EX_LAYERED must be made again). */ +static int reopen(BKWindow *win, LONG_PTR ex) +{ + HWND old = win->hwnd; + RECT rc; + wchar_t title[256]; + GetWindowRect(old, &rc); + GetWindowTextW(old, title, 256); + const LONG_PTR style = GetWindowLongPtrW(old, GWL_STYLE); + HWND h = CreateWindowExW((DWORD)ex, k_class, title, (DWORD)(style & ~WS_VISIBLE), rc.left, + rc.top, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, + GetModuleHandleW(NULL), NULL); + if (!h) { + return 0; + } + SetWindowLongPtrW(old, GWLP_USERDATA, 0); /* the old window's messages end here */ + SetWindowLongPtrW(h, GWLP_USERDATA, (LONG_PTR)win); + win->hwnd = h; + timers_rearm(win); + DestroyWindow(old); + if (style & WS_VISIBLE) { + ShowWindow(h, SW_SHOW); + } + return 1; +} + +/* Per-pixel alpha for a window drawn by the GPU: DWM uses the alpha channel of the surface that + * Present fills (DwmEnableBlurBehindWindow over the whole client area). */ +static void set_blur_behind(HWND hwnd, int on) +{ + typedef HRESULT(WINAPI * BlurFn)(HWND, const DWM_BLURBEHIND *); + BlurFn blur = (BlurFn)(void *)dwm_proc("DwmEnableBlurBehindWindow"); + if (blur) { + DWM_BLURBEHIND bb = { + DWM_BB_ENABLE | DWM_BB_BLURREGION, on, CreateRectRgn(0, 0, -1, -1), FALSE}; + blur(hwnd, &bb); + DeleteObject(bb.hRgnBlur); + } +} + +/* "transparent": per-pixel alpha. The raster keeps alpha and has no frame to draw (the + * window has none either). The GPU renderer stays only with LAYER_GPU. */ +static void set_layering(BKWindow *win, int mode) +{ + if (mode == win->layered) { + return; + } + const int gpu = BUI_raster_is_gpu(win->raster) || BUI_raster_gpu_pending(win->raster); + const LONG_PTR old_ex = GetWindowLongPtrW(win->hwnd, GWL_EXSTYLE); + const LONG_PTR ex = (mode == LAYER_ULW) ? old_ex | WS_EX_LAYERED : + old_ex & ~(LONG_PTR)WS_EX_LAYERED; + BUI_raster_gpu_release(win->raster); /* the device holds the swap chain of the old window */ + if (ex != old_ex && !reopen(win, ex)) { + if (gpu) { + gpu_attach(win); + } + return; + } + if (win->layered == LAYER_ULW) { + layer_free(win); + } + if (win->layered == LAYER_GPU) { + set_blur_behind(win->hwnd, FALSE); + } + win->layered = mode; + BUI_raster_set_alpha(win->raster, mode != LAYER_NONE); + set_frame(win); + if (mode == LAYER_GPU) { + set_blur_behind(win->hwnd, TRUE); + } + if (mode == LAYER_NONE && win->opacity != 255) { + set_opacity(win); + } + if (mode == LAYER_GPU || (mode == LAYER_NONE && gpu)) { + gpu_attach(win); /* made with the alpha setting above */ + } + win->full = 1; + host_request(win); +} + +static void set_transparent(BKWindow *win, int on) +{ + const int gpu = BUI_raster_is_gpu(win->raster) || BUI_raster_gpu_pending(win->raster); + set_layering(win, !on ? LAYER_NONE : gpu ? LAYER_GPU : LAYER_ULW); +} + +/* A LAYER_GPU window shows nothing without its device (it failed to start, or was lost): the + * software raster takes over through UpdateLayeredWindow. */ +static void gpu_window_check(BKWindow *win) +{ + if (win->layered == LAYER_GPU && !BUI_raster_is_gpu(win->raster) && + !BUI_raster_gpu_pending(win->raster)) + { + set_layering(win, LAYER_ULW); + } +} + void BK_window_wm(BKWindow *win, const char *key, double a, double b) { HWND h = win->hwnd; @@ -973,15 +1265,21 @@ void BK_window_wm(BKWindow *win, const char *key, double a, double b) SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); } else if (!strcmp(key, "alpha")) { - const LONG_PTR ex = GetWindowLongPtrW(h, GWL_EXSTYLE); - SetWindowLongPtrW(h, GWL_EXSTYLE, ex | WS_EX_LAYERED); - SetLayeredWindowAttributes(h, 0, (BYTE)(BK_CLAMP(a, 0.0, 1.0) * 255.0 + 0.5), LWA_ALPHA); + win->opacity = (BYTE)(BK_CLAMP(a, 0.0, 1.0) * 255.0 + 0.5); + if (win->layered == LAYER_ULW) { /* the next present scales the frame */ + win->full = 1; + host_request(win); + } + else if (win->layered == LAYER_NONE) { /* GPU frames with alpha are not scaled */ + set_opacity(win); + } } else if (!strcmp(key, "override")) { - if (!win->style) { - win->style = (DWORD)GetWindowLongPtrW(h, GWL_STYLE) & ~WS_VISIBLE; - } - set_style(h, a ? WS_POPUP | WS_CLIPCHILDREN : win->style); + win->override = a != 0.0; + set_frame(win); + } + else if (!strcmp(key, "transparent")) { + set_transparent(win, a != 0.0); } else if (!strcmp(key, "state")) { static const int show[4] = {SW_RESTORE, SW_MINIMIZE, SW_HIDE, SW_MAXIMIZE}; diff --git a/kit/intern/kit_window_x11.c b/kit/intern/kit_window_x11.c index 58243f1..27cb743 100644 --- a/kit/intern/kit_window_x11.c +++ b/kit/intern/kit_window_x11.c @@ -89,7 +89,13 @@ F(XSetWMNormalHints) \ F(XSetWMProtocols) \ F(XStoreName) \ - F(XWarpPointer) + F(XWarpPointer) \ + F(XIconifyWindow) \ + F(XWithdrawWindow) \ + F(XMatchVisualInfo) \ + F(XCreateColormap) \ + F(XFreeColormap) \ + F(XGetWindowAttributes) # define BK_X11_DECL(f) static __typeof__(&f) p_##f; BK_X11_FUNCS(BK_X11_DECL) @@ -163,6 +169,12 @@ static int x11_load(void) # define XSetWMProtocols (*p_XSetWMProtocols) # define XStoreName (*p_XStoreName) # define XWarpPointer (*p_XWarpPointer) +# define XIconifyWindow (*p_XIconifyWindow) +# define XWithdrawWindow (*p_XWithdrawWindow) +# define XMatchVisualInfo (*p_XMatchVisualInfo) +# define XCreateColormap (*p_XCreateColormap) +# define XFreeColormap (*p_XFreeColormap) +# define XGetWindowAttributes (*p_XGetWindowAttributes) # define MAX_TIMERS 64 @@ -179,6 +191,11 @@ struct BKWindow { BKWindowCallbacks cb; XImage *image; GC gc; + Visual *visual; /* the window's: 32-bit ARGB when transparent */ + int depth; + Colormap cmap; /* for the ARGB visual, else None */ + char *title; + bool frameless, above; /* "override", "topmost": kept for a new X window */ int render_pending, frames, cursor, tip_id, next_timer, full; int want_frame, frame_vsync; double frame_start; @@ -405,7 +422,8 @@ static void host_emit(void *data, BKNode *node, int kind, void *ref, double a, d static void gpu_attach(BKWindow *win) { if (win->xid) { - BuiGpuX11Window target = {g_dpy, (unsigned long)win->xid}; + BuiGpuX11Window target = {g_dpy, (unsigned long)win->xid, + win->visual->visualid}; BUI_raster_gpu_attach(win->raster, &target, 0); } } @@ -426,12 +444,12 @@ static void present(BKWindow *win, int all) win->image->data = NULL; /* the raster owns the pixels */ XDestroyImage(win->image); } - Visual *vis = DefaultVisual(g_dpy, DefaultScreen(g_dpy)); - win->image = XCreateImage(g_dpy, vis, 24, ZPixmap, 0, NULL, (unsigned)w, (unsigned)h, 32, - w * 4); + win->image = XCreateImage(g_dpy, win->visual, (unsigned)win->depth, ZPixmap, 0, NULL, + (unsigned)w, (unsigned)h, 32, w * 4); all = 1; } - /* 0xAARRGGBB words are B, G, R, A bytes: the 24-bit TrueColor layout */ + /* 0xAARRGGBB words are B, G, R, A bytes: the 24-bit TrueColor layout, and the 32-bit ARGB + * visual's (premultiplied, as compositors read it) */ win->image->data = (char *)BUI_raster_pixels(win->raster); if (all) { XPutImage(g_dpy, win->xid, win->gc, win->image, 0, 0, 0, 0, (unsigned)w, (unsigned)h); @@ -644,6 +662,13 @@ static void handle_event(XEvent *ev) } handle_key(win, &ev->xkey, ev->type == KeyPress); break; + case FocusIn: + case FocusOut: + /* a pointer or keyboard grab keeps the focus */ + if (ev->xfocus.mode != NotifyGrab && ev->xfocus.mode != NotifyUngrab) { + bk_window_focus(win->ctx, win, &win->cb, ev->type == FocusIn); + } + break; case ClientMessage: if ((Atom)ev->xclient.data.l[0] == a_delete) { if (!win->cb.on_close || win->cb.on_close(win->cb.data, win)) { @@ -656,6 +681,105 @@ static void handle_event(XEvent *ev) /* -- windows -- */ +/* GHOST_WindowX11::netwmMaximized: ask the window manager to add or remove two _NET_WM_STATE + * properties (the second may be NULL). Before the window is mapped, the client sets the property + * itself (EWMH), so an add is appended there. */ +static void net_wm_state(BKWindow *win, bool add, const char *prop1, const char *prop2) +{ + XWindowAttributes wa; + if (XGetWindowAttributes(g_dpy, win->xid, &wa) && wa.map_state == IsUnmapped) { + if (add) { + const Atom atoms[2] = {XInternAtom(g_dpy, prop1, False), + prop2 ? XInternAtom(g_dpy, prop2, False) : None}; + XChangeProperty(g_dpy, win->xid, XInternAtom(g_dpy, "_NET_WM_STATE", False), XA_ATOM, 32, + PropModeAppend, (const unsigned char *)atoms, prop2 ? 2 : 1); + } + return; + } + XEvent xev; + memset(&xev, 0, sizeof(xev)); + xev.xclient.type = ClientMessage; + xev.xclient.send_event = True; + xev.xclient.window = win->xid; + xev.xclient.message_type = XInternAtom(g_dpy, "_NET_WM_STATE", False); + xev.xclient.format = 32; + xev.xclient.data.l[0] = add ? 1 : 0; /* _NET_WM_STATE_ADD / _REMOVE */ + xev.xclient.data.l[1] = (long)XInternAtom(g_dpy, prop1, False); + xev.xclient.data.l[2] = prop2 ? (long)XInternAtom(g_dpy, prop2, False) : 0; + XSendEvent(g_dpy, RootWindow(g_dpy, DefaultScreen(g_dpy)), False, + SubstructureRedirectMask | SubstructureNotifyMask, &xev); +} + +/* GHOST_WindowX11::motifShowWindowFrame: _MOTIF_WM_HINTS decorations on or off. */ +static void motif_frame(BKWindow *win, bool show) +{ + const long hints[4] = {1L << 1 /* MWM_HINTS_DECORATIONS */, 0, show ? 1 : 0, 0}; + const Atom a = XInternAtom(g_dpy, "_MOTIF_WM_HINTS", False); + XChangeProperty(g_dpy, win->xid, a, a, 32, PropModeReplace, (const unsigned char *)hints, 4); +} + +/* The X window, with the default visual or, transparent, a 32-bit ARGB one (GHOST's + * alpha background). A window's visual is fixed, so "transparent" makes a new window. */ +static bool create_xid(BKWindow *win, bool transparent) +{ + const int screen = DefaultScreen(g_dpy); + const Window root = RootWindow(g_dpy, screen); + XSetWindowAttributes attr; + memset(&attr, 0, sizeof(attr)); + attr.event_mask = ExposureMask | StructureNotifyMask | PointerMotionMask | ButtonPressMask | + ButtonReleaseMask | KeyPressMask | KeyReleaseMask | LeaveWindowMask | + FocusChangeMask; + unsigned long mask = CWBackPixel | CWEventMask; + XVisualInfo vi; + win->visual = DefaultVisual(g_dpy, screen); + win->depth = 24; + win->cmap = None; + if (transparent && XMatchVisualInfo(g_dpy, screen, 32, TrueColor, &vi)) { + win->visual = vi.visual; + win->depth = 32; + win->cmap = XCreateColormap(g_dpy, root, vi.visual, AllocNone); + attr.colormap = win->cmap; + mask |= CWColormap | CWBorderPixel; /* a visual other than the parent's needs both */ + } + else { + attr.background_pixel = BlackPixel(g_dpy, screen); + } + win->xid = XCreateWindow(g_dpy, root, 0, 0, (unsigned)win->w, (unsigned)win->h, 0, + win->depth == 32 ? 32 : CopyFromParent, InputOutput, + win->depth == 32 ? win->visual : CopyFromParent, mask, &attr); + if (!win->xid) { + return false; + } + XSetWMProtocols(g_dpy, win->xid, &a_delete, 1); + win->gc = XCreateGC(g_dpy, win->xid, 0, NULL); + BK_window_set_title(win, win->title); + BUI_raster_set_alpha(win->raster, win->depth == 32); + if (win->frameless) { + motif_frame(win, false); + } + if (win->above) { + net_wm_state(win, true, "_NET_WM_STATE_ABOVE", NULL); + } + return true; +} + +/* Drop the X window and what was made for it, before a new one (the window stays open). */ +static void destroy_xid(BKWindow *win) +{ + BUI_raster_gpu_release(win->raster); + if (win->image) { + win->image->data = NULL; + XDestroyImage(win->image); + win->image = NULL; + } + XFreeGC(g_dpy, win->gc); + XDestroyWindow(g_dpy, win->xid); + if (win->cmap) { + XFreeColormap(g_dpy, win->cmap); + } + win->xid = 0; +} + BKWindow *BK_window_new(BKContext *ctx, const char *title, int width, int height) { if (!open_display()) { @@ -670,24 +794,13 @@ BKWindow *BK_window_new(BKContext *ctx, const char *title, int width, int height BK_set_scale(win->scale); win->w = (int)(width * win->scale); win->h = (int)(height * win->scale); - const int screen = DefaultScreen(g_dpy); - XSetWindowAttributes attr; - memset(&attr, 0, sizeof(attr)); - attr.background_pixel = BlackPixel(g_dpy, screen); - attr.event_mask = ExposureMask | StructureNotifyMask | PointerMotionMask | ButtonPressMask | - ButtonReleaseMask | KeyPressMask | KeyReleaseMask | LeaveWindowMask | - FocusChangeMask; - win->xid = XCreateWindow(g_dpy, RootWindow(g_dpy, screen), 0, 0, (unsigned)win->w, - (unsigned)win->h, 0, CopyFromParent, InputOutput, CopyFromParent, - CWBackPixel | CWEventMask, &attr); - if (!win->xid) { + win->title = bk_strdup(title ? title : ""); + if (!create_xid(win, false)) { BUI_raster_free(win->raster); + free(win->title); free(win); return NULL; } - XSetWMProtocols(g_dpy, win->xid, &a_delete, 1); - win->gc = XCreateGC(g_dpy, win->xid, 0, NULL); - BK_window_set_title(win, title); win->next = g_windows; g_windows = win; g_open++; @@ -719,10 +832,14 @@ void BK_window_free(BKWindow *win) if (win->gc) { XFreeGC(g_dpy, win->gc); } + if (win->cmap) { + XFreeColormap(g_dpy, win->cmap); + } if (win->ctx) { BK_context_set_host(win->ctx, &win->prev); } BUI_raster_free(win->raster); + free(win->title); free(win); } @@ -756,6 +873,11 @@ void BK_window_set_title(BKWindow *win, const char *title) return; } const char *t = title ? title : ""; + if (t != win->title) { + free(win->title); + win->title = bk_strdup(t); + t = win->title; + } XChangeProperty(g_dpy, win->xid, XInternAtom(g_dpy, "_NET_WM_NAME", False), a_utf8, 8, PropModeReplace, (const unsigned char *)t, (int)strlen(t)); XStoreName(g_dpy, win->xid, t); @@ -853,6 +975,28 @@ void BK_window_wm(BKWindow *win, const char *key, double a, double b) if (!strcmp(key, "pos")) { XMoveWindow(g_dpy, win->xid, (int)a, (int)b); } + else if (!strcmp(key, "override")) { /* no frame (not override_redirect: that gets no keys) */ + win->frameless = a != 0; + motif_frame(win, !win->frameless); + } + else if (!strcmp(key, "topmost")) { + win->above = a != 0; + net_wm_state(win, win->above, "_NET_WM_STATE_ABOVE", NULL); + } + else if (!strcmp(key, "transparent")) { /* before show: the window is made again */ + if ((a != 0) != (win->depth == 32)) { + const bool gpu = BUI_raster_is_gpu(win->raster) || BUI_raster_gpu_pending(win->raster); + destroy_xid(win); + if (!create_xid(win, a != 0)) { + g_open--; /* no window: closed */ + return; + } + if (gpu) { /* the device was made for the old window: GLX needs a config for the new visual */ + gpu_attach(win); + } + win->full = 1; + } + } else if (!strcmp(key, "minsize") || !strcmp(key, "maxsize") || !strcmp(key, "resizable")) { XSizeHints *hints = XAllocSizeHints(); long supplied; @@ -890,6 +1034,20 @@ void BK_window_wm(BKWindow *win, const char *key, double a, double b) else if (!strcmp(key, "focus")) { XSetInputFocus(g_dpy, win->xid, RevertToParent, CurrentTime); } + else if (!strcmp(key, "state")) { /* GHOST_WindowX11::setState; 2 hides (ICCCM withdraw) */ + const int state = BK_CLAMP((int)a, 0, 3); + if (state == 1) { + XIconifyWindow(g_dpy, win->xid, DefaultScreen(g_dpy)); + } + else if (state == 2) { + XWithdrawWindow(g_dpy, win->xid, DefaultScreen(g_dpy)); + } + else { + XMapWindow(g_dpy, win->xid); + net_wm_state(win, state == 3, "_NET_WM_STATE_MAXIMIZED_HORZ", + "_NET_WM_STATE_MAXIMIZED_VERT"); + } + } XFlush(g_dpy); } diff --git a/tools/gen_notices.py b/tools/gen_notices.py index 2e35f6a..9009284 100644 --- a/tools/gen_notices.py +++ b/tools/gen_notices.py @@ -108,6 +108,10 @@ def stb_text() -> str: ("Tailwind CSS v4 theme values", "The default theme behind the Tailwind support (blenderui-tailwind)", "MIT", "https://tailwindcss.com", lambda: MIT.format(holder="Tailwind Labs, Inc.")), + ("wlr-layer-shell-unstable-v1 (version 5)", "Layer-shell windows on Wayland (kit/extern/wayland_protocols)", + "MIT", "https://gitlab.freedesktop.org/wlroots/wlr-protocols", + lambda: (lambda t: t[t.index("") + 11:t.index("")].strip())( + file_text("kit/extern/wayland_protocols/wlr-layer-shell-unstable-v1.xml"))), ("Lua 5.4.7", "The Lua runtime embedded in bui-lua (bindings/lua, fetched from lua.org)", "MIT", "https://www.lua.org", lambda: MIT.format(holder="1994-2024 Lua.org, PUC-Rio")), diff --git a/website/src/content/docs/c/c-api.mdx b/website/src/content/docs/c/c-api.mdx index ac5f68f..3e99384 100644 --- a/website/src/content/docs/c/c-api.mdx +++ b/website/src/content/docs/c/c-api.mdx @@ -219,7 +219,7 @@ BK_window_free(win); - **Platforms:** native Win32, X11 and Cocoa windows. `BK_window_supported()` is 0 where there's no window host, and on macOS off the main thread (AppKit allows windows only there). - **Control:** `BK_window_close`, `set_title`, `set_size`, `get_size`, and `BK_window_wm(win, key, a, b)` for the rest: `"pos"`, `"minsize"`, `"maxsize"`, `"resizable"`, `"topmost"`, `"alpha"`, `"state"`, `"lift"`, `"lower"`, `"focus"`. -- **Your own loop:** `BK_window_pump()` handles pending messages and returns the number of open windows. `BK_window_timer(win, ms)` sets a one-shot timer; `BK_window_set_callbacks` gets you `on_timer`, `on_close` and `on_frame`. +- **Your own loop:** `BK_window_pump()` handles pending messages and returns the number of open windows. `BK_window_timer(win, ms)` sets a one-shot timer; `BK_window_set_callbacks` gets you `on_timer`, `on_close`, `on_frame` and `on_focus` (the window got or lost the keyboard focus; losing it cancels an open pie, as in Blender). `BK_window_wm(win, "state", 2, 0)` hides the window and `"state"` 0 shows it again, on every host. `BK_window_wm(win, "transparent", 1, 0)` (before `BK_window_show` on Linux) gives a window with per-pixel alpha: no region background, so only the widgets show (a 32-bit ARGB visual on X11, ARGB8888 buffers on Wayland, `UpdateLayeredWindow` on Windows, DWM blur-behind with the Direct3D 11 renderer, a non-opaque window on macOS; no frame on Windows). `"override"` removes the frame on Linux too (`_MOTIF_WM_HINTS` on X11, client-side decorations on Wayland), and `"topmost"` keeps an X11 window above others (`_NET_WM_STATE_ABOVE`); Wayland has no request for that, so there a window goes on a layer instead: on compositors with wlr-layer-shell (KDE Plasma, wlroots compositors such as Sway and Hyprland, COSMIC; not GNOME), `BK_window_wm(win, "layer", 3, 1)` makes it an overlay layer surface with exclusive keyboard focus, and `"anchor"` (edges: top 1, bottom 2, left 4, right 8; then the exclusive zone) places it, `15, -1` covering the whole output. `BK_window_wm_get(win, "layer", &has, &layer)` tells whether the compositor has layer shell. - **Other threads:** `BK_window_wake_init(cb)` on the loop's thread, then `BK_window_wake()` from any thread makes the loop call `cb` soon. That's how Node.js drives BUI's UI thread. **On the GPU:** `BUI_gpu_set_default(1)` puts every new window on the GPU (Direct3D 11 on Windows, OpenGL 3.3 on Linux); `BK_window_set_gpu(win, 1)` does it for one window. The device starts in the background, so the first frames use the software raster, and `BK_window_gpu(win)` returns 1 once frames draw on the GPU. The `BUI_GPU=1` or `0` environment variable overrides both. @@ -252,7 +252,8 @@ BK_custom_set(node, &cls, my_data); | Area | API | | --- | --- | -| Menus and pie menus | `BK_open_menu`, `BK_open_pie`, `BK_menubutton_set_items`, all from a `BKMenuSpec` array | +| Menus and pie menus | `BK_open_menu`, `BK_open_pie`, `BK_menubutton_set_items`, all from a `BKMenuSpec` array; both open at the pointer or at a given point | +| Hit test | `BK_hit_at(ctx, x, y)`: the interactive node under a point, or `NULL` | | Canvas | `BK_canvas_set_item`, `add_item`, `put_item`, `truncate`, `delete`, `raise` | | Text areas | `BK_textarea_insert`, `delete`, `caret`, `selection`, `set_spans` (coloured ranges), `index_at`, `see` | | Trees | `BK_tree_item_new`, `add`, `set_values`; `BK_tree_set_roots`, `BK_tree_selected` | @@ -261,8 +262,10 @@ BK_custom_set(node, &cls, my_data); | Number expressions | `BK_eval_number("2*pi", &v)`: Blender's expression evaluator | | File browser | `BK_node_new("filebrowser")` in any window, or `BK_file_dialog(...)` for a ready-made dialog | | Text | `BUI_text_measure`, `BUI_raster_text`, `BUI_text_add_font` | +| Raster alpha | `BUI_raster_set_alpha`, `BUI_raster_alpha`, `BUI_raster_clear`: premultiplied alpha in a CPU raster; opaque rasters draw the same bytes | | Meshes and textures | `BUI_texture_create` / `update` / `free`, `BUI_raster_mesh`: indexed triangles, the same pixels on every renderer | | Scale | `BK_set_scale(s)`, `BK_scale()`: 1 means Blender's 20 px widgets | +| Text shadow | `BK_set_text_shadow(on)`, `BK_text_shadow()`: Blender's UI text shadows, on by default | `BK_eval_number` is the same expression evaluator that lets Blender users type `2*pi` into a rotation field. Now your C program can, too. Your calculator app may feel threatened. diff --git a/website/src/content/docs/concepts/text-and-fonts.mdx b/website/src/content/docs/concepts/text-and-fonts.mdx index ddb7b18..678596b 100644 --- a/website/src/content/docs/concepts/text-and-fonts.mdx +++ b/website/src/content/docs/concepts/text-and-fonts.mdx @@ -30,7 +30,7 @@ Blender draws text with its font library, **BLF**, on top of FreeType. BUI vendo - **Optical size:** the variable Inter is set to the text's size, clamped to the axis as Blender does. UI text gets Inter's 14 pt design. - **Bold** is the font's weight-700 instance, not a smeared regular. - **No kerning**, matching Blender, whose FreeType is built without `GPOS` kerning. -- **Shadows:** widget text gets Blender's 1 px drop shadow, and panel titles its soft 3×3 one. +- **Shadows:** widget text gets Blender's 1 px drop shadow, and panel titles its soft 3×3 one. `BK_set_text_shadow(0)` turns them off; the Python binding starts with them off (`buipy.kit_set_text_shadow(True)` turns them back on). FreeType is on by default (CMake `BUI_TEXT_FREETYPE`). With it off, stb_truetype rasterizes glyphs instead; stb_truetype also reads character maps, metrics and kerning tables in both cases. diff --git a/website/src/content/docs/concepts/window-hosts.mdx b/website/src/content/docs/concepts/window-hosts.mdx index 8fa3dbb..37d838c 100644 --- a/website/src/content/docs/concepts/window-hosts.mdx +++ b/website/src/content/docs/concepts/window-hosts.mdx @@ -48,7 +48,7 @@ Wayland sessions can run it through XWayland, which every mainstream desktop pro ### Linux (Wayland) Experimental -A native Wayland host with the same API, modelled on Blender's `GHOST_SystemWayland`: xdg-shell toplevels (with server-side decorations when the compositor offers them), a `wl_seat` with xkbcommon for keys and key repeat, the pointer, the clipboard through `wl_data_device`, cursors from the cursor theme, the output's buffer scale, and frame callbacks for pacing. Damaged rectangles are copied into `wl_shm` buffers. libwayland-client and libwayland-cursor load at run time through Blender's `wayland_dynload`, so nothing is linked. A session with `WAYLAND_DISPLAY` gets this host; set `BUI_WINDOW_BACKEND=x11` to use X11, or `wayland` to force this one. It draws on the CPU: with the [GPU renderer](../rendering/) on (`BUI_GPU=1`), a Wayland session keeps X11 through XWayland until Wayland has its own EGL renderer. Per-window `BK_window_set_gpu` on a Wayland window is ignored for the same reason. Not done yet: IME, client-side decorations when the compositor has no server-side ones (GNOME shows no title bar), pointer warping, window positions. +A native Wayland host with the same API, modelled on Blender's `GHOST_SystemWayland`: xdg-shell toplevels (with server-side decorations when the compositor offers them), a `wl_seat` with xkbcommon for keys and key repeat, the pointer, the clipboard through `wl_data_device`, cursors through the cursor-shape protocol (the cursor theme where the compositor has none), the output's buffer scale, and frame callbacks for pacing. Damaged rectangles are copied into `wl_shm` buffers. libwayland-client and libwayland-cursor load at run time through Blender's `wayland_dynload`, so nothing is linked. A session with `WAYLAND_DISPLAY` gets this host; set `BUI_WINDOW_BACKEND=x11` to use X11, or `wayland` to force this one. It draws on the CPU: with the [GPU renderer](../rendering/) on (`BUI_GPU=1`), a Wayland session keeps X11 through XWayland until Wayland has its own EGL renderer. Per-window `BK_window_set_gpu` on a Wayland window is ignored for the same reason. Not done yet: IME, client-side decorations when the compositor has no server-side ones (GNOME shows no title bar), pointer warping, window positions. ### macOS (Cocoa) diff --git a/website/src/content/docs/javascript/native-api.mdx b/website/src/content/docs/javascript/native-api.mdx index 436be07..e52ddc5 100644 --- a/website/src/content/docs/javascript/native-api.mdx +++ b/website/src/content/docs/javascript/native-api.mdx @@ -61,9 +61,9 @@ win.on('close', () => console.log('bye')) | `headless` | No native windows: renders off-screen, for tests and CI. | | `quitOnLastWindowClosed` | Quit when the last window closes. Default `true`. | -`app.window(options)` takes `title`, `width`, `height`, `resizable` and `gpu` (to choose per window). `app.quit()` closes every window and ends the UI thread, so Node can exit. +`app.window(options)` takes `title`, `width`, `height`, `resizable`, `gpu` (to choose per window), `transparent` (per-pixel alpha), `alwaysOnTop` (Windows, macOS and X11), `frame: false` (no frame) and `layerShell` (`{ layer, anchor, exclusiveZone, keyboard }`, a Wayland layer-shell surface; `layer` is `'background'`, `'bottom'`, `'top'` or `'overlay'`, `anchor` a list of `'top'`, `'bottom'`, `'left'` and `'right'`, `keyboard` `'none'`, `'exclusive'` or `'on-demand'`). A Wayland toplevel cannot stay on top: use `layerShell`. `app.quit()` closes every window and ends the UI thread, so Node can exit. -A window gives you `body`, `createElement(tag)`, `createTextNode(text)`, `requestAnimationFrame(cb)`, `screenshot()`, `click(x, y)`, `close()` and `on('close', fn)`. +A window gives you `body`, `createElement(tag)`, `createTextNode(text)`, `requestAnimationFrame(cb)`, `screenshot()`, `click(x, y)`, `close()` and `on(event, fn)` for `close`, `focus` and `blur` (the window got or lost the keyboard focus). ## Elements and attributes diff --git a/website/src/content/docs/python/components-reference.mdx b/website/src/content/docs/python/components-reference.mdx index 3c2d02e..30f6206 100644 --- a/website/src/content/docs/python/components-reference.mdx +++ b/website/src/content/docs/python/components-reference.mdx @@ -153,7 +153,7 @@ In lists and trees, arrow keys navigate, Left and Right co | `MenuButton` | `MenuButton(text, items, *, icon=None)` | A pulldown button (`File`, `Edit` …). | | `MenuBar` | `MenuBar(("File", items), ("Edit", items), ..., right=())` | Blender's top bar, for `App(header=...)`. `right` holds components at the right end. | | `Menu` | `Menu.show(ctx, owner, items, title=None, at=None, on_pick=None)` | Opens a popup menu from code, by `owner` or at `at=(x, y)`. Plain-string items call `on_pick(text)`. | -| `PieMenu` | `PieMenu.show(ctx, items, title="")` | A pie around the pointer: up to 8 items, placed W, E, S, N, NW, NE, SW, SE. Flick toward one and click, or press 1–8. | +| `PieMenu` | `PieMenu.show(ctx, items, title="", at=None)` | A pie around the pointer, or around `at=(x, y)`: up to 8 items, placed W, E, S, N, NW, NE, SW, SE. Flick toward one and click, or press the number key in its numpad direction (8 north) or its underlined letter. Opened by a key: release the key to pick; a quick tap keeps it open. A `submenu` item opens a sub-pie; past 8 items the 8th is **More**. | Items in any menu are `MenuItem`s, plain strings, or `"-"` for a separator. diff --git a/website/src/content/docs/python/components.mdx b/website/src/content/docs/python/components.mdx index b6a772d..3f5bc2c 100644 --- a/website/src/content/docs/python/components.mdx +++ b/website/src/content/docs/python/components.mdx @@ -49,11 +49,11 @@ App(title="BUI", size=(420, 640), *children, padding=None, spacing=None, header= | Member | What it does | | --- | --- | | `app.run(native=None)` | Opens the window and runs the event loop until it closes. `native=False` forces the Tk-hosted window. | -| `app.open(native=None)` | Opens the window without running the loop; returns the window. | +| `app.open(native=None)` | Opens the window without running the loop; returns the window. Its `wm(key, a, b)` sends window requests (`"transparent"`, `"override"`, `"topmost"`, and on Wayland `"layer"` and `"anchor"`), and `on_focus(active)` reports keyboard focus. | | `app.add(*components)` | Appends to the root column. | | `app.root` | The root `Column`. | | `app.bind("ctrl+s", fn)` | A window hotkey: `"q"`, `"shift+f3"`, `"ctrl+alt+z"` … | -| `app.ctx` | The kit context: popups, focus, and undo with `app.ctx.undo()` / `app.ctx.redo()`. | +| `app.ctx` | The kit context: popups, focus, and undo with `app.ctx.undo()` / `app.ctx.redo()`. `app.ctx.hit(x, y)` gives the interactive node under a point. | | `app.render_raster(w, h)` | Draws a frame without a window, for tests and screenshots. | | `app.press`, `release`, `motion`, `key`, `wheel`, `context_menu` | Simulate input without a window. See [headless testing](../headless-testing/). | @@ -205,9 +205,9 @@ app.run() - **`MenuBar(("File", items), ("Edit", items), right=[...])`** is Blender's top bar; `right` holds components at the right end. - **`MenuButton(text, items, *, icon=None)`** is a single pulldown button you can put anywhere. - **`Menu.show(app.ctx, owner, items, title=None, at=None, on_pick=None)`** opens a popup menu from code, next to the `owner` component or at `at=(x, y)`. Plain-string items call `on_pick(text)`. -- **`PieMenu.show(app.ctx, items, title="")`** opens a pie menu around the pointer, with up to 8 items in Blender's order: W, E, S, N, NW, NE, SW, SE. +- **`PieMenu.show(app.ctx, items, title="", at=None)`** opens a pie menu around the pointer, or around `at=(x, y)`, with up to 8 items in Blender's order: W, E, S, N, NW, NE, SW, SE. An item with a `submenu` opens a sub-pie at the pointer. Past 8 items, the 8th slot is **More**, a sub-pie with the rest. -Menus behave as in Blender. Press, drag to an item and release to pick it in one gesture. Hover switches between menu-bar menus and opens submenus. **Typing searches** every item in the menu. In a pie, flick toward an item and click, or press 1 to 8. +Menus behave as in Blender. Press, drag to an item and release to pick it in one gesture. Hover switches between menu-bar menus and opens submenus. **Typing searches** every item in the menu. In a pie, flick toward an item and click, or press the number key in the item's numpad direction (8 north, 6 east, 2 south, 4 west, and so on), or its underlined letter. A pie opened by a key follows Blender: hold the key, move toward an item and release the key to pick it. A quick tap keeps the pie open for a click. The items grow out of the center as the pie opens (Blender's pie animation, off with reduced motion). Every value also has a **right-click menu** for free, with Copy, Paste and Reset. diff --git a/website/src/content/docs/rust/gpui-syntax.mdx b/website/src/content/docs/rust/gpui-syntax.mdx index 7cb5c82..72da8ab 100644 --- a/website/src/content/docs/rust/gpui-syntax.mdx +++ b/website/src/content/docs/rust/gpui-syntax.mdx @@ -63,7 +63,7 @@ fn main() { | `impl Render for View` | The same: `fn render(&mut self, window, cx) -> impl IntoElement` | | `Entity`, `WeakEntity`, `cx.new` | `Copy` handles into the app's entity slots: `read` (returns `&T`), `update`, `downgrade`; `cx.release(e)` drops a value | | `Context`: `notify`, `listener`, `entity` | The same; it derefs to `App` | -| `WindowOptions { window_bounds, titlebar, .. }`, `WindowBounds::Windowed(..)` | The same, plus `is_resizable`, `window_min_size` and `gpu` | +| `WindowOptions { window_bounds, titlebar, .. }`, `WindowBounds::Windowed(..)` | The same, plus `is_resizable`, `window_min_size`, `gpu`, `window_background` (`WindowBackgroundAppearance::Transparent`), `kind` (`WindowKind::Floating` stays on top, `PopUp` is on top without a frame, `LayerShell(LayerShellOptions { layer, anchor, exclusive_zone, keyboard_interactivity })` is a Wayland layer-shell surface) and `window_decorations` (`WindowDecorations::Client`: no frame). A Wayland toplevel cannot stay on top: use `LayerShell` | | `cx.set_global`, `cx.global` | The same, plus `try_global` and `global_mut` | | `cx.stop_propagation()` | The same | | `div().flex().gap_2()`, `.when(..)`, `.when_some(..)`, `.map(..)` | The same, from the `Styled` and `FluentBuilder` traits | @@ -101,7 +101,7 @@ let win = cx.open_window(opts, |_, cx| cx.new(|_| MyView)).unwrap(); - **`WindowBounds`** is `Windowed`, `Maximized` or `Fullscreen`. Without bounds, a window opens at 1024 × 700; without a title, it's called "BUI". - **`gpu: Some(true)`** draws this window on the GPU; `None` follows `Application::gpu` and `BUI_GPU`. - **`WindowHandle`** has `root()` (the view's entity) and `update(cx, |view, window, cx| ...)`. -- **On `App`:** `close_window(id)`, `on_window_closed(|id, cx| ...)`, `notify_all()`, `quit()`. `activate(..)` exists for compatibility and does nothing, because windows come to the front when shown. +- **On `App`:** `close_window(id)`, `on_window_closed(|id, cx| ...)`, `on_window_activation(|id, active, cx| ...)`, `notify_all()`, `quit()`. `activate(..)` exists for compatibility and does nothing, because windows come to the front when shown. The event loop runs until every window is closed.