From 81ef50560825a6976b09b96ad169c676f2244bda Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 13:21:59 +0530 Subject: [PATCH 01/51] pie: open at a point --- bindings/lua/src/bui_context.c | 4 +++- bindings/python/bui/components/menus.py | 6 +++--- bindings/python/src/buipy_kit.c | 15 +++++++++++---- bindings/python/tests/test_components.py | 12 ++++++++++++ kit/include/BUI_kit.h | 4 +++- kit/intern/kit_menus.c | 8 +++++--- .../content/docs/python/components-reference.mdx | 2 +- website/src/content/docs/python/components.mdx | 2 +- 8 files changed, 39 insertions(+), 14 deletions(-) diff --git a/bindings/lua/src/bui_context.c b/bindings/lua/src/bui_context.c index b716f2c..70f84a3 100644 --- a/bindings/lua/src/bui_context.c +++ b/bindings/lua/src/bui_context.c @@ -555,7 +555,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; } diff --git a/bindings/python/bui/components/menus.py b/bindings/python/bui/components/menus.py index 6eb7fb9..d879bae 100644 --- a/bindings/python/bui/components/menus.py +++ b/bindings/python/bui/components/menus.py @@ -93,10 +93,10 @@ 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")``. + """Radial menu around the pointer or ``at=(x, y)``: ``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.""" @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/src/buipy_kit.c b/bindings/python/src/buipy_kit.c index e6b153b..50b9732 100644 --- a/bindings/python/src/buipy_kit.c +++ b/bindings/python/src/buipy_kit.c @@ -1798,11 +1798,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; @@ -1810,7 +1816,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; } @@ -1902,7 +1908,8 @@ static PyMethodDef kitctx_methods[] = { {"picker_part", (PyCFunction)kitctx_picker_part, METH_VARARGS, "Color picker part."}, {"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}, }; diff --git a/bindings/python/tests/test_components.py b/bindings/python/tests/test_components.py index 4b31504..0318ef0 100644 --- a/bindings/python/tests/test_components.py +++ b/bindings/python/tests/test_components.py @@ -487,6 +487,18 @@ def test_pie_menu(): assert got == ["N"] and not d.popups() +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() + + # -- color ------------------------------------------------------------------------------ diff --git a/kit/include/BUI_kit.h b/kit/include/BUI_kit.h index 41b9115..6d03189 100644 --- a/kit/include/BUI_kit.h +++ b/kit/include/BUI_kit.h @@ -268,7 +268,9 @@ 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 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): diff --git a/kit/intern/kit_menus.c b/kit/intern/kit_menus.c index a0b66e2..84e33dd 100644 --- a/kit/intern/kit_menus.c +++ b/kit/intern/kit_menus.c @@ -1151,16 +1151,18 @@ const BKClass bk_cls_pie = {.name = "pie", .move = pie_move, .key = pie_key}; -void BK_open_pie(BKContext *ctx, const BKMenuSpec *items, int n, const char *title) +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; 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.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; pie->ext = pd; pie->hot = -1; bk_open_popup(ctx, pie, ctx->root, NULL); diff --git a/website/src/content/docs/python/components-reference.mdx b/website/src/content/docs/python/components-reference.mdx index 3c2d02e..7f78f60 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 1–8. | 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..62313be 100644 --- a/website/src/content/docs/python/components.mdx +++ b/website/src/content/docs/python/components.mdx @@ -205,7 +205,7 @@ 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. 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. From 92340fd518fad22406fb703ee00f8e0d297fb08e Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 13:25:57 +0530 Subject: [PATCH 02/51] kit: public hit test --- bindings/lua/src/bui_context.c | 32 +++++++++++++----------- bindings/lua/tests/test_context.lua | 8 +++++- bindings/python/src/buipy_kit.c | 10 ++++++++ bindings/python/tests/test_components.py | 9 +++++++ docs/c-api.md | 2 +- kit/include/BUI_kit.h | 3 +++ kit/intern/kit_core.c | 2 +- kit/intern/kit_intern.h | 1 - kit/intern/kit_widgets.c | 2 +- website/src/content/docs/c/c-api.mdx | 3 ++- 10 files changed, 51 insertions(+), 21 deletions(-) diff --git a/bindings/lua/src/bui_context.c b/bindings/lua/src/bui_context.c index 70f84a3..0de6d0d 100644 --- a/bindings/lua/src/bui_context.c +++ b/bindings/lua/src/bui_context.c @@ -344,10 +344,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)); @@ -360,20 +358,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) { @@ -651,6 +652,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 b49c287..7206480 100644 --- a/bindings/lua/tests/test_context.lua +++ b/bindings/lua/tests/test_context.lua @@ -220,7 +220,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() @@ -234,6 +234,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/python/src/buipy_kit.c b/bindings/python/src/buipy_kit.c index 50b9732..7e617ff 100644 --- a/bindings/python/src/buipy_kit.c +++ b/bindings/python/src/buipy_kit.c @@ -1769,6 +1769,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}; @@ -1906,6 +1915,7 @@ 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)(void (*)(void))kitctx_open_pie, METH_VARARGS | METH_KEYWORDS, diff --git a/bindings/python/tests/test_components.py b/bindings/python/tests/test_components.py index 0318ef0..bab2fc3 100644 --- a/bindings/python/tests/test_components.py +++ b/bindings/python/tests/test_components.py @@ -499,6 +499,15 @@ def test_pie_menu_at(): 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/docs/c-api.md b/docs/c-api.md index 64e5011..df1db5e 100644 --- a/docs/c-api.md +++ b/docs/c-api.md @@ -84,7 +84,7 @@ int main(void) | **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. | | **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` | +| **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/kit/include/BUI_kit.h b/kit/include/BUI_kit.h index 6d03189..dfcd344 100644 --- a/kit/include/BUI_kit.h +++ b/kit/include/BUI_kit.h @@ -268,6 +268,9 @@ void BK_open_menu(BKContext *ctx, const char *title, int at_x, int at_y); +/* 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); diff --git a/kit/intern/kit_core.c b/kit/intern/kit_core.c index bc3f09a..64111a2 100644 --- a/kit/intern/kit_core.c +++ b/kit/intern/kit_core.c @@ -3733,7 +3733,7 @@ BKNode *bk_next_editable(BKNode *from, int dir) return list[(at + dir + count) % count]; } -BKNode *bk_hit_at(BKContext *ctx, int x, int y) +BKNode *BK_hit_at(BKContext *ctx, int x, int y) { return hit_all(ctx, x, y); } diff --git a/kit/intern/kit_intern.h b/kit/intern/kit_intern.h index 6542312..7471548 100644 --- a/kit/intern/kit_intern.h +++ b/kit/intern/kit_intern.h @@ -617,7 +617,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_widgets.c b/kit/intern/kit_widgets.c index 441d478..e754561 100644 --- a/kit/intern/kit_widgets.c +++ b/kit/intern/kit_widgets.c @@ -2034,7 +2034,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/website/src/content/docs/c/c-api.mdx b/website/src/content/docs/c/c-api.mdx index ac5f68f..13e6beb 100644 --- a/website/src/content/docs/c/c-api.mdx +++ b/website/src/content/docs/c/c-api.mdx @@ -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` | From 006e313892e00a217f6564078fadd8bfb0902c20 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 13:28:28 +0530 Subject: [PATCH 03/51] pie: pick on key release --- bindings/python/bui/components/menus.py | 3 +- bindings/python/tests/test_components.py | 41 ++++++++++++++++ kit/intern/kit_core.c | 8 ++++ kit/intern/kit_intern.h | 2 + kit/intern/kit_menus.c | 47 +++++++++++++++++-- .../docs/python/components-reference.mdx | 2 +- .../src/content/docs/python/components.mdx | 2 +- 7 files changed, 99 insertions(+), 6 deletions(-) diff --git a/bindings/python/bui/components/menus.py b/bindings/python/bui/components/menus.py index d879bae..31a4d87 100644 --- a/bindings/python/bui/components/menus.py +++ b/bindings/python/bui/components/menus.py @@ -95,7 +95,8 @@ def show(ctx, owner, items, title: str = None, at=None, on_pick=None) -> None: class PieMenu: """Radial menu around the pointer or ``at=(x, y)``: ``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.""" + an item and click, or press 1-8. Opened by a held key, releasing the key picks + the item; a quick tap keeps the pie open, as in Blender.""" @staticmethod def show(ctx, items, title: str = "", at=None) -> None: diff --git a/bindings/python/tests/test_components.py b/bindings/python/tests/test_components.py index bab2fc3..ebe4846 100644 --- a/bindings/python/tests/test_components.py +++ b/bindings/python/tests/test_components.py @@ -487,6 +487,47 @@ 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_menu_at(): from bui.components import MenuItem, PieMenu got = [] diff --git a/kit/intern/kit_core.c b/kit/intern/kit_core.c index 64111a2..65e9ae0 100644 --- a/kit/intern/kit_core.c +++ b/kit/intern/kit_core.c @@ -2775,6 +2775,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 */ @@ -3381,6 +3382,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; diff --git a/kit/intern/kit_intern.h b/kit/intern/kit_intern.h index 7471548..fd3a299 100644 --- a/kit/intern/kit_intern.h +++ b/kit/intern/kit_intern.h @@ -304,6 +304,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); @@ -556,6 +557,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; diff --git a/kit/intern/kit_menus.c b/kit/intern/kit_menus.c index 84e33dd..30831f0 100644 --- a/kit/intern/kit_menus.c +++ b/kit/intern/kit_menus.c @@ -870,12 +870,22 @@ 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_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 */ + double opened; /* bk_time at open (the scroll timer's time_duration) */ } PieData; #define PIE(n) ((PieData *)(n)->ext) @@ -1024,6 +1034,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,6 +1074,10 @@ static int pie_press(BKNode *n, BKEvent *e) static int pie_key(BKNode *n, BKEvent *e) { + PieData *pd = PIE(n); + if (!pd->click_style && bk_keysym_is(e->keysym, pd->key)) { + return 1; /* auto-repeat of the held pie key */ + } if (e->text && e->text[0] >= '1' && e->text[0] <= '8' && !e->text[1]) { pie_pick(n, e->text[0] - '1'); return 1; @@ -1071,6 +1089,22 @@ 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; +} + static void pie_draw(BKNode *n) { PieData *pd = PIE(n); @@ -1149,7 +1183,8 @@ const BKClass bk_cls_pie = {.name = "pie", .hit = pie_hit, .press = pie_press, .move = pie_move, - .key = pie_key}; + .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) @@ -1163,6 +1198,12 @@ void BK_open_pie( pd->menu.title = title ? bk_strdup(title) : NULL; 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; bk_open_popup(ctx, pie, ctx->root, NULL); diff --git a/website/src/content/docs/python/components-reference.mdx b/website/src/content/docs/python/components-reference.mdx index 7f78f60..406bfee 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="", 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 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 1–8. Opened by a key: release the key to pick; a quick tap keeps it open. | 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 62313be..29cf831 100644 --- a/website/src/content/docs/python/components.mdx +++ b/website/src/content/docs/python/components.mdx @@ -207,7 +207,7 @@ app.run() - **`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="", 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. -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 1 to 8. 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. Every value also has a **right-click menu** for free, with Copy, Paste and Reset. From abeac81351581b65c1f4887f6e3d4e1ccaecec69 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 13:29:26 +0530 Subject: [PATCH 04/51] pie: sub-pies and the More level --- bindings/python/bui/components/menus.py | 5 ++- bindings/python/tests/test_components.py | 31 +++++++++++++++ kit/intern/kit_menus.c | 38 ++++++++++++++++--- .../docs/python/components-reference.mdx | 2 +- .../src/content/docs/python/components.mdx | 2 +- 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/bindings/python/bui/components/menus.py b/bindings/python/bui/components/menus.py index 31a4d87..977ad7c 100644 --- a/bindings/python/bui/components/menus.py +++ b/bindings/python/bui/components/menus.py @@ -94,9 +94,10 @@ def show(ctx, owner, items, title: str = None, at=None, on_pick=None) -> None: class PieMenu: """Radial menu around the pointer or ``at=(x, y)``: ``PieMenu.show(ctx, items, "Mode")``. - Up to 8 items in Blender's order W, E, S, N, NW, NE, SW, SE; move toward + Items in Blender's order W, E, S, N, NW, NE, SW, SE; move toward an item and click, or press 1-8. Opened by a held key, releasing the key picks - the item; a quick tap keeps the pie open, as in Blender.""" + 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 = "", at=None) -> None: diff --git a/bindings/python/tests/test_components.py b/bindings/python/tests/test_components.py index ebe4846..3387654 100644 --- a/bindings/python/tests/test_components.py +++ b/bindings/python/tests/test_components.py @@ -528,6 +528,37 @@ def test_pie_tap_stays_open(): 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("8", "8", 0, mods()) # the 8th slot is "More" + d.frame() + assert d.popups() == 1 and not got + d.app.key("3", "3", 0, mods()) # items 7, 8, 9: the third is 9 + assert got == [9] and not d.popups() + + def test_pie_menu_at(): from bui.components import MenuItem, PieMenu got = [] diff --git a/kit/intern/kit_menus.c b/kit/intern/kit_menus.c index 30831f0..77e6c82 100644 --- a/kit/intern/kit_menus.c +++ b/kit/intern/kit_menus.c @@ -873,6 +873,7 @@ 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_CLICK_THRESHOLD_SQ 50.0f typedef struct PieData { @@ -971,7 +972,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(); @@ -1014,10 +1015,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); @@ -1118,7 +1131,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; @@ -1138,7 +1151,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); @@ -1190,11 +1203,24 @@ 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; pd->menu.title = title ? bk_strdup(title) : NULL; pd->at_x = at ? at_x : ctx->pointer_x; pd->at_y = at ? at_y : ctx->pointer_y; diff --git a/website/src/content/docs/python/components-reference.mdx b/website/src/content/docs/python/components-reference.mdx index 406bfee..cd0f7bd 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="", 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 1–8. Opened by a key: release the key to pick; a quick tap keeps it open. | +| `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 1–8. 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 29cf831..d147a00 100644 --- a/website/src/content/docs/python/components.mdx +++ b/website/src/content/docs/python/components.mdx @@ -205,7 +205,7 @@ 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="", 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. +- **`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. 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. From 297b2785d12f72dd5a0cead4252ef45c95fea4c3 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 13:33:41 +0530 Subject: [PATCH 05/51] window: focus callback and hiding on Linux --- bindings/python/bui/host.py | 11 ++- bindings/python/src/buipy_kit.c | 45 ++++++++-- kit/include/BUI_kit.h | 2 + kit/intern/kit_core.c | 14 +++ kit/intern/kit_intern.h | 2 + kit/intern/kit_window_cocoa.m | 8 ++ kit/intern/kit_window_wayland.c | 122 ++++++++++++++++++++------- kit/intern/kit_window_win32.c | 3 + kit/intern/kit_window_x11.c | 45 +++++++++- website/src/content/docs/c/c-api.mdx | 2 +- 10 files changed, 214 insertions(+), 40 deletions(-) diff --git a/bindings/python/bui/host.py b/bindings/python/bui/host.py index 7a06a2e..5414766 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()) diff --git a/bindings/python/src/buipy_kit.c b/bindings/python/src/buipy_kit.c index 7e617ff..af9fac7 100644 --- a/bindings/python/src/buipy_kit.c +++ b/bindings/python/src/buipy_kit.c @@ -2305,6 +2305,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) @@ -2345,6 +2346,25 @@ 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) +{ + GIL_ENTER; + win_on_focus(data, w, active); + GIL_LEAVE; +} + static void win_on_timer_gil(void *data, BKWindow *w, int id) { GIL_ENTER; @@ -2384,7 +2404,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; } @@ -2399,6 +2421,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); } @@ -2407,6 +2430,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; } @@ -2414,6 +2438,7 @@ static int kitwin_clear(KitWindowObject *self) { Py_CLEAR(self->timers); Py_CLEAR(self->on_close); + Py_CLEAR(self->on_focus); return 0; } @@ -2566,14 +2591,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; } @@ -2601,8 +2628,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}, }; diff --git a/kit/include/BUI_kit.h b/kit/include/BUI_kit.h index dfcd344..0794b31 100644 --- a/kit/include/BUI_kit.h +++ b/kit/include/BUI_kit.h @@ -666,6 +666,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); diff --git a/kit/intern/kit_core.c b/kit/intern/kit_core.c index 65e9ae0..5244303 100644 --- a/kit/intern/kit_core.c +++ b/kit/intern/kit_core.c @@ -3741,6 +3741,20 @@ BKNode *bk_next_editable(BKNode *from, int dir) return list[(at + dir + count) % count]; } +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_intern.h b/kit/intern/kit_intern.h index fd3a299..3b25360 100644 --- a/kit/intern/kit_intern.h +++ b/kit/intern/kit_intern.h @@ -464,6 +464,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); diff --git a/kit/intern/kit_window_cocoa.m b/kit/intern/kit_window_cocoa.m index 1c26c73..cdf9798 100644 --- a/kit/intern/kit_window_cocoa.m +++ b/kit/intern/kit_window_cocoa.m @@ -616,6 +616,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); } } diff --git a/kit/intern/kit_window_wayland.c b/kit/intern/kit_window_wayland.c index 06b9bb7..059e02f 100644 --- a/kit/intern/kit_window_wayland.c +++ b/kit/intern/kit_window_wayland.c @@ -117,7 +117,8 @@ struct BKWindow { struct zxdg_toplevel_decoration_v1 *decoration; struct wl_callback *frame_cb; Buf bufs[2]; - int open, configured; + char *title; + int open, configured, hidden; int render_pending, frames, cursor, tip_id, next_timer, full; int want_frame, frame_vsync; double frame_start, hold_until; @@ -597,11 +598,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; @@ -1016,6 +1024,29 @@ 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->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 +1060,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); @@ -1133,18 +1153,8 @@ BKWindow *bk_wl_window_new(BKContext *ctx, const char *title, int width, int hei 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); - } + win->title = bk_strdup(title ? title : ""); + 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,7 +1165,6 @@ 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); @@ -1164,6 +1173,44 @@ BKWindow *bk_wl_window_new(BKContext *ctx, const char *title, int width, int hei return win; } +/* 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) +{ + 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"); + 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); + zxdg_toplevel_decoration_v1_set_mode(win->decoration, + ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); + } +} + +/* 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); + win->full = 1; + } +} + void bk_wl_window_free(BKWindow *win) { if (!win) { @@ -1180,6 +1227,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 +1239,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 +1258,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 +1276,9 @@ 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); + } win->full = 1; win->render_pending = 1; } @@ -1303,6 +1358,13 @@ 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 */ + } + 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)); } diff --git a/kit/intern/kit_window_win32.c b/kit/intern/kit_window_win32.c index 4bf7986..ad27b8c 100644 --- a/kit/intern/kit_window_win32.c +++ b/kit/intern/kit_window_win32.c @@ -728,6 +728,9 @@ static LRESULT CALLBACK wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) } } 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; diff --git a/kit/intern/kit_window_x11.c b/kit/intern/kit_window_x11.c index 58243f1..eadae70 100644 --- a/kit/intern/kit_window_x11.c +++ b/kit/intern/kit_window_x11.c @@ -89,7 +89,9 @@ F(XSetWMNormalHints) \ F(XSetWMProtocols) \ F(XStoreName) \ - F(XWarpPointer) + F(XWarpPointer) \ + F(XIconifyWindow) \ + F(XWithdrawWindow) # define BK_X11_DECL(f) static __typeof__(&f) p_##f; BK_X11_FUNCS(BK_X11_DECL) @@ -163,6 +165,8 @@ 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 MAX_TIMERS 64 @@ -644,6 +648,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)) { @@ -844,6 +855,24 @@ void *BK_window_handle(BKWindow *win) return (void *)(uintptr_t)win->xid; } +/* GHOST_WindowX11::netwmMaximized: ask the window manager to add or remove two _NET_WM_STATE + * properties (the second may be NULL). */ +static void net_wm_state(BKWindow *win, bool add, const char *prop1, const char *prop2) +{ + 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: size hints for minsize / maxsize / resizable, moves for pos */ void BK_window_wm(BKWindow *win, const char *key, double a, double b) { @@ -890,6 +919,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/website/src/content/docs/c/c-api.mdx b/website/src/content/docs/c/c-api.mdx index 13e6beb..6543f7d 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. - **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. From 2a54944fb7986f59cf1911ca3757d9cef7f0f9c2 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 13:38:04 +0530 Subject: [PATCH 06/51] raster: optional frame alpha --- gpu/include/GPU_raster.h | 7 ++ gpu/intern/gpu_raster.c | 119 +++++++++++++++++++++------------- gpu/tests/CMakeLists.txt | 4 ++ gpu/tests/test_raster_alpha.c | 53 +++++++++++++++ 4 files changed, 137 insertions(+), 46 deletions(-) create mode 100644 gpu/tests/test_raster_alpha.c diff --git a/gpu/include/GPU_raster.h b/gpu/include/GPU_raster.h index 034a6f0..79d6f9c 100644 --- a/gpu/include/GPU_raster.h +++ b/gpu/include/GPU_raster.h @@ -53,6 +53,13 @@ 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. CPU frames only. */ +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. CPU frames only. */ +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); diff --git a/gpu/intern/gpu_raster.c b/gpu/intern/gpu_raster.c index e2e3eb7..48d99c7 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,24 @@ 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(const BuiRaster *rs, 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 | rs->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, all four bytes (alpha 0 stays 0). */ 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; + const uint32_t ag = (((s >> 8) & 0xFF00FFu) * f + ((d >> 8) & 0xFF00FFu) * nf) & 0xFF00FF00u; + return rb | ag; } /* Write with coverage cov (0..1) scaled by paint alpha. */ @@ -469,6 +472,30 @@ 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 (r->gpu || !clip_box(r, (float)x, (float)y, (float)(x + w), (float)(y + h), &x0, &y0, &x1, + &y1)) + { + 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 +542,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, rgba); if (p.a <= 0) { return; } @@ -604,7 +631,7 @@ void BUI_raster_round_rect( } return; } - const Paint p = paint_from(rgba); + const Paint p = paint_from(r, rgba); if (p.a <= 0) { return; } @@ -694,7 +721,7 @@ void BUI_raster_circle( } return; } - const Paint p = paint_from(rgba); + const Paint p = paint_from(r, rgba); if (p.a <= 0) { return; } @@ -753,7 +780,7 @@ void BUI_raster_line( } return; } - const Paint p = paint_from(rgba); + const Paint p = paint_from(r, rgba); if (p.a <= 0) { return; } @@ -805,7 +832,7 @@ void BUI_raster_ellipse(BuiRaster *r, } return; } - const Paint p = paint_from(rgba); + const Paint p = paint_from(r, rgba); if (p.a <= 0) { return; } @@ -902,9 +929,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, top) : (Paint){0, 0}; + const Paint p_out = outline ? paint_from(r, outline) : (Paint){0, 0}; + const Paint p_emb = has_emboss ? paint_from(r, 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 +945,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, c); } const int row_inside = (sy - y) >= safe && (y + h - sy) >= safe; /* Interior span of this row: filled directly, no per-pixel SDF. */ @@ -1023,9 +1050,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(r, p->outline); + const Paint p_emb = paint_from(r, p->emboss); + Paint p_in = paint_from(r, 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 +1068,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(r, c); } /* -- middle columns: row-constant coverage -- */ int span_a = ma, span_b = mb; @@ -1109,7 +1136,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(r, gc), ci); } Paint pi = p_in; if (gradient && p->shade_horizontal) { @@ -1118,7 +1145,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(r, c); } put_cov(row + px, pi, ci); } @@ -1129,15 +1156,15 @@ void BUI_raster_widget_base(BuiRaster *r, const BuiWidgetBase *p) } } -/* Premultiplied source over the frame (whose alpha byte is unused). */ +/* Premultiplied source over the frame, all four bytes. */ static inline uint32_t premul_over(uint32_t d, uint32_t s) { const uint32_t ia = 255u - (s >> 24); uint32_t rb = (d & 0xFF00FFu) * ia + 0x800080u; - uint32_t g = (d & 0x00FF00u) * ia + 0x008000u; + uint32_t ag = ((d >> 8) & 0xFF00FFu) * ia + 0x800080u; rb = ((rb + ((rb >> 8) & 0xFF00FFu)) >> 8) & 0xFF00FFu; /* /255, rounded */ - g = ((g + ((g >> 8) & 0x00FF00u)) >> 8) & 0x00FF00u; - return (s & 0xFFFFFFu) + rb + g; + ag = (ag + ((ag >> 8) & 0xFF00FFu)) & 0xFF00FF00u; + return s + rb + ag; } /* All four channels times f/256. */ @@ -1148,17 +1175,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) & (0xFFFFFFu | alpha); } } @@ -1368,7 +1395,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) | r->alpha; } px--; } @@ -1377,7 +1404,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, r->alpha); } } return; @@ -1414,7 +1441,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, r->alpha); } } } @@ -1486,7 +1513,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, r->alpha); } } return; @@ -1515,7 +1542,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, r->alpha); } } } @@ -1683,7 +1710,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, rgba); if (p.a <= 0) { return; } @@ -1794,7 +1821,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(r, 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 +1848,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(r, rgba), 1.0f); } } } @@ -2060,14 +2087,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(const BuiRaster *r, 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(r, c); if (p.a >= 256) { *px = p.rgb; } @@ -2185,7 +2212,7 @@ static void mesh_triangle(BuiRaster *r, continue; } if (flat_all) { - put_straight(row + px, flat); + put_straight(r, row + px, flat); continue; } /* barycentric weights of vertices 0, 1, 2 */ @@ -2213,7 +2240,7 @@ static void mesh_triangle(BuiRaster *r, c[k] *= tx[k]; } } - put_straight(row + px, c); + put_straight(r, row + px, c); } } } @@ -2313,7 +2340,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(r, rgba), cov); } } } @@ -2357,7 +2384,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(r, c), 1.0f); } } } @@ -2421,7 +2448,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(r, c), cov); } } } @@ -2711,7 +2738,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, 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..7ce2e90 --- /dev/null +++ b/gpu/tests/test_raster_alpha.c @@ -0,0 +1,53 @@ +/* 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; +} + +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); + printf("raster alpha: %s\n", ok ? "ok" : "FAILED"); + return ok ? 0 : 1; +} From 096288e6e8c297c5131dd368e4426ff4a0d01f0e Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 13:40:56 +0530 Subject: [PATCH 07/51] window: transparent windows on Linux --- bindings/python/bui/host.py | 2 +- bindings/python/src/buipy_kit.c | 2 +- kit/include/BUI_kit.h | 4 +- kit/intern/kit_core.c | 5 +- kit/intern/kit_window_wayland.c | 12 ++- kit/intern/kit_window_x11.c | 114 ++++++++++++++++++++++----- website/src/content/docs/c/c-api.mdx | 2 +- 7 files changed, 115 insertions(+), 26 deletions(-) diff --git a/bindings/python/bui/host.py b/bindings/python/bui/host.py index 5414766..c5249a3 100644 --- a/bindings/python/bui/host.py +++ b/bindings/python/bui/host.py @@ -277,7 +277,7 @@ 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 (native Linux windows, before show).""" if self.native: self._win.wm(key, float(a), float(b)) return diff --git a/bindings/python/src/buipy_kit.c b/bindings/python/src/buipy_kit.c index af9fac7..a5704a6 100644 --- a/bindings/python/src/buipy_kit.c +++ b/bindings/python/src/buipy_kit.c @@ -2612,7 +2612,7 @@ 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)."}, {"wm_get", (PyCFunction)kitwin_wm_get, METH_VARARGS, "wm_get(key) -> (a, b) or None: pos, state, screen, pointer, scale."}, {"render_now", (PyCFunction)kitwin_render_now, METH_NOARGS, "Render + present now."}, diff --git a/kit/include/BUI_kit.h b/kit/include/BUI_kit.h index 0794b31..14fcfba 100644 --- a/kit/include/BUI_kit.h +++ b/kit/include/BUI_kit.h @@ -697,7 +697,9 @@ 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, Linux): per-pixel alpha, no region background; set it before show (X11 + * makes the window again) and keep the software renderer. */ 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 5244303..e96c263 100644 --- a/kit/intern/kit_core.c +++ b/kit/intern/kit_core.c @@ -2170,7 +2170,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; diff --git a/kit/intern/kit_window_wayland.c b/kit/intern/kit_window_wayland.c index 059e02f..44a962d 100644 --- a/kit/intern/kit_window_wayland.c +++ b/kit/intern/kit_window_wayland.c @@ -245,7 +245,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); @@ -1361,6 +1364,13 @@ void bk_wl_window_wm(BKWindow *win, const char *key, double a, double b) if (!strcmp(key, "state")) { set_hidden(win, a == 2); /* 2: withdrawn */ } + 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; diff --git a/kit/intern/kit_window_x11.c b/kit/intern/kit_window_x11.c index eadae70..f662573 100644 --- a/kit/intern/kit_window_x11.c +++ b/kit/intern/kit_window_x11.c @@ -91,7 +91,10 @@ F(XStoreName) \ F(XWarpPointer) \ F(XIconifyWindow) \ - F(XWithdrawWindow) + F(XWithdrawWindow) \ + F(XMatchVisualInfo) \ + F(XCreateColormap) \ + F(XFreeColormap) # define BK_X11_DECL(f) static __typeof__(&f) p_##f; BK_X11_FUNCS(BK_X11_DECL) @@ -167,6 +170,9 @@ static int x11_load(void) # 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 MAX_TIMERS 64 @@ -183,6 +189,10 @@ 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; int render_pending, frames, cursor, tip_id, next_timer, full; int want_frame, frame_vsync; double frame_start; @@ -430,12 +440,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); @@ -667,6 +677,62 @@ static void handle_event(XEvent *ev) /* -- windows -- */ +/* 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); + 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()) { @@ -681,24 +747,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++; @@ -730,10 +785,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); } @@ -767,6 +826,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); @@ -799,7 +863,7 @@ BuiRaster *BK_window_raster(BKWindow *win) void BK_window_set_gpu(BKWindow *win, int on) { - if (win && on) { + if (win && on && win->depth != 32) { /* a transparent window keeps the software raster */ gpu_attach(win); host_request(win); } @@ -882,6 +946,16 @@ 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, "transparent")) { /* before show: the window is made again */ + if ((a != 0) != (win->depth == 32)) { + destroy_xid(win); + if (!create_xid(win, a != 0)) { + g_open--; /* no window: closed */ + return; + } + win->full = 1; + } + } else if (!strcmp(key, "minsize") || !strcmp(key, "maxsize") || !strcmp(key, "resizable")) { XSizeHints *hints = XAllocSizeHints(); long supplied; diff --git a/website/src/content/docs/c/c-api.mdx b/website/src/content/docs/c/c-api.mdx index 6543f7d..bdf85aa 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`, `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. +- **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. On Linux, `BK_window_wm(win, "transparent", 1, 0)` before `BK_window_show` 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; software rendering). - **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. From 9fd8a6a552f2c85cbc9edfbdeebcc6d0ac9d4bd2 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 13:45:17 +0530 Subject: [PATCH 08/51] window: frameless and topmost on Linux --- kit/intern/kit_window_wayland.c | 20 +++++++- kit/intern/kit_window_x11.c | 74 +++++++++++++++++++++------- website/src/content/docs/c/c-api.mdx | 2 +- 3 files changed, 74 insertions(+), 22 deletions(-) diff --git a/kit/intern/kit_window_wayland.c b/kit/intern/kit_window_wayland.c index 44a962d..beb61c5 100644 --- a/kit/intern/kit_window_wayland.c +++ b/kit/intern/kit_window_wayland.c @@ -119,6 +119,7 @@ struct BKWindow { Buf bufs[2]; char *title; int open, configured, hidden; + bool frameless; /* "override": no server-side frame (BUI draws none either) */ int render_pending, frames, cursor, tip_id, next_timer, full; int want_frame, frame_vsync; double frame_start, hold_until; @@ -1176,6 +1177,16 @@ BKWindow *bk_wl_window_new(BKContext *ctx, const char *title, int width, int hei 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) @@ -1190,8 +1201,7 @@ static void role_create(BKWindow *win) 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); + set_frame(win); } } @@ -1364,6 +1374,12 @@ void bk_wl_window_wm(BKWindow *win, const char *key, double a, double b) if (!strcmp(key, "state")) { set_hidden(win, a == 2); /* 2: withdrawn */ } + 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 */ diff --git a/kit/intern/kit_window_x11.c b/kit/intern/kit_window_x11.c index f662573..46ac2c6 100644 --- a/kit/intern/kit_window_x11.c +++ b/kit/intern/kit_window_x11.c @@ -94,7 +94,8 @@ F(XWithdrawWindow) \ F(XMatchVisualInfo) \ F(XCreateColormap) \ - F(XFreeColormap) + F(XFreeColormap) \ + F(XGetWindowAttributes) # define BK_X11_DECL(f) static __typeof__(&f) p_##f; BK_X11_FUNCS(BK_X11_DECL) @@ -173,6 +174,7 @@ static int x11_load(void) # define XMatchVisualInfo (*p_XMatchVisualInfo) # define XCreateColormap (*p_XCreateColormap) # define XFreeColormap (*p_XFreeColormap) +# define XGetWindowAttributes (*p_XGetWindowAttributes) # define MAX_TIMERS 64 @@ -193,6 +195,7 @@ struct BKWindow { 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; @@ -677,6 +680,43 @@ 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) @@ -713,6 +753,12 @@ static bool create_xid(BKWindow *win, bool transparent) 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; } @@ -919,24 +965,6 @@ void *BK_window_handle(BKWindow *win) return (void *)(uintptr_t)win->xid; } -/* GHOST_WindowX11::netwmMaximized: ask the window manager to add or remove two _NET_WM_STATE - * properties (the second may be NULL). */ -static void net_wm_state(BKWindow *win, bool add, const char *prop1, const char *prop2) -{ - 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: size hints for minsize / maxsize / resizable, moves for pos */ void BK_window_wm(BKWindow *win, const char *key, double a, double b) { @@ -946,6 +974,14 @@ 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)) { destroy_xid(win); diff --git a/website/src/content/docs/c/c-api.mdx b/website/src/content/docs/c/c-api.mdx index bdf85aa..b3db58e 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`, `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. On Linux, `BK_window_wm(win, "transparent", 1, 0)` before `BK_window_show` 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; software rendering). +- **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. On Linux, `BK_window_wm(win, "transparent", 1, 0)` before `BK_window_show` 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; software rendering). `"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. - **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. From d880a93cb7aaa2074294d2d6ee6bcf19a5ec5ced Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 13:49:36 +0530 Subject: [PATCH 09/51] window: layer-shell windows on Wayland --- THIRD_PARTY_NOTICES.md | 32 ++ bindings/python/bui/host.py | 4 +- bindings/python/src/buipy_kit.c | 4 +- kit/CMakeLists.txt | 15 +- .../wlr-layer-shell-unstable-v1.xml | 409 ++++++++++++++++++ kit/include/BUI_kit.h | 7 +- kit/intern/kit_window_wayland.c | 119 ++++- tools/gen_notices.py | 4 + website/src/content/docs/c/c-api.mdx | 2 +- 9 files changed, 575 insertions(+), 21 deletions(-) create mode 100644 kit/extern/wayland_protocols/wlr-layer-shell-unstable-v1.xml 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/python/bui/host.py b/bindings/python/bui/host.py index c5249a3..8379e2e 100644 --- a/bindings/python/bui/host.py +++ b/bindings/python/bui/host.py @@ -277,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, transparent (native Linux windows, before show).""" + lift, lower, focus, transparent (native Linux windows, before show), 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/buipy_kit.c b/bindings/python/src/buipy_kit.c index a5704a6..5cbcf9e 100644 --- a/bindings/python/src/buipy_kit.c +++ b/bindings/python/src/buipy_kit.c @@ -2612,9 +2612,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, transparent (Linux, before show)."}, + "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)."}, diff --git a/kit/CMakeLists.txt b/kit/CMakeLists.txt index d5ebc4b..cf70ca9 100644 --- a/kit/CMakeLists.txt +++ b/kit/CMakeLists.txt @@ -173,15 +173,16 @@ 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 + foreach(_x ${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) + 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 14fcfba..03d48b5 100644 --- a/kit/include/BUI_kit.h +++ b/kit/include/BUI_kit.h @@ -699,7 +699,12 @@ void *BK_window_handle(BKWindow *win); /* HWND / X11 Window / NSWindow * * * "resizable" (w, h), "topmost" (a), "alpha" (a), "override" (a), "state" * (0 normal, 1 iconic, 2 withdrawn, 3 zoomed), "lift", "lower", "focus". * "transparent" (a, Linux): per-pixel alpha, no region background; set it before show (X11 - * makes the window again) and keep the software renderer. */ + * makes the window again) and keep the software renderer. + * "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_window_wayland.c b/kit/intern/kit_window_wayland.c index beb61c5..5b5f0f0 100644 --- a/kit/intern/kit_window_wayland.c +++ b/kit/intern/kit_window_wayland.c @@ -28,6 +28,7 @@ # include +# include "wlr-layer-shell-unstable-v1-client-protocol.h" # include "xdg-decoration-unstable-v1-client-protocol.h" # include "xdg-shell-client-protocol.h" @@ -115,11 +116,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]; 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; @@ -147,6 +152,7 @@ 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; struct wl_seat *seat; struct wl_pointer *pointer; struct wl_keyboard *keyboard; @@ -864,6 +870,10 @@ 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)); + } 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); @@ -1040,6 +1050,10 @@ static void role_destroy(BKWindow *win) 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); } @@ -1084,11 +1098,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; @@ -1097,14 +1110,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}; @@ -1143,6 +1200,12 @@ static struct wl_surface_listener surface_listener = { /* -- windows -- */ +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()) { @@ -1155,9 +1218,9 @@ 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); + surface_create(win); win->title = bk_strdup(title ? title : ""); + win->layer = -1; role_create(win); win->open = 1; apply_scale(win, 1); @@ -1191,11 +1254,20 @@ static void set_frame(BKWindow *win) * 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); - const char *app_id = getenv("BUI_APP_ID"); 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 */ @@ -1292,6 +1364,10 @@ void bk_wl_window_set_size(BKWindow *win, int width, int height) 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; } @@ -1374,6 +1450,26 @@ void bk_wl_window_wm(BKWindow *win, const char *key, double a, double b) 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) { @@ -1427,6 +1523,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/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 b3db58e..6478901 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`, `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. On Linux, `BK_window_wm(win, "transparent", 1, 0)` before `BK_window_show` 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; software rendering). `"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. +- **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. On Linux, `BK_window_wm(win, "transparent", 1, 0)` before `BK_window_show` 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; software rendering). `"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. From aff3a5d4093b811e706c27764b3c130f5e612e6e Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 14:04:19 +0530 Subject: [PATCH 10/51] pie: number keys are numpad directions --- bindings/python/bui/components/menus.py | 2 +- bindings/python/tests/test_components.py | 17 +++++++++++++++-- kit/intern/kit_menus.c | 12 ++++++++++-- .../docs/python/components-reference.mdx | 2 +- website/src/content/docs/python/components.mdx | 2 +- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/bindings/python/bui/components/menus.py b/bindings/python/bui/components/menus.py index 977ad7c..37f969d 100644 --- a/bindings/python/bui/components/menus.py +++ b/bindings/python/bui/components/menus.py @@ -95,7 +95,7 @@ def show(ctx, owner, items, title: str = None, at=None, on_pick=None) -> None: class PieMenu: """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, or press 1-8. Opened by a held key, releasing the key picks + an item and click, or press a number key in its numpad direction (8 north, 6 east). 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".""" diff --git a/bindings/python/tests/test_components.py b/bindings/python/tests/test_components.py index 3387654..04d3d16 100644 --- a/bindings/python/tests/test_components.py +++ b/bindings/python/tests/test_components.py @@ -552,13 +552,26 @@ def test_pie_more_level(): 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("8", "8", 0, mods()) # the 8th slot is "More" + 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("3", "3", 0, mods()) # items 7, 8, 9: the third is 9 + 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_menu_at(): from bui.components import MenuItem, PieMenu got = [] diff --git a/kit/intern/kit_menus.c b/kit/intern/kit_menus.c index 77e6c82..66f104c 100644 --- a/kit/intern/kit_menus.c +++ b/kit/intern/kit_menus.c @@ -1091,8 +1091,16 @@ static int pie_key(BKNode *n, BKEvent *e) if (!pd->click_style && bk_keysym_is(e->keysym, pd->key)) { return 1; /* auto-repeat of the held pie key */ } - if (e->text && e->text[0] >= '1' && e->text[0] <= '8' && !e->text[1]) { - pie_pick(n, e->text[0] - '1'); + /* 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; } if (bk_keysym_is(e->keysym, "Return") || bk_keysym_is(e->keysym, "KP_Enter")) { diff --git a/website/src/content/docs/python/components-reference.mdx b/website/src/content/docs/python/components-reference.mdx index cd0f7bd..262a6c8 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="", 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 1–8. 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**. | +| `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). 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 d147a00..87c28a8 100644 --- a/website/src/content/docs/python/components.mdx +++ b/website/src/content/docs/python/components.mdx @@ -207,7 +207,7 @@ app.run() - **`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="", 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. 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. +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). 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. Every value also has a **right-click menu** for free, with Copy, Paste and Reset. From 3b3e480e6fabba1d166571ff2ce6cf72888b89b8 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 14:06:31 +0530 Subject: [PATCH 11/51] pie: accelerator keys --- bindings/python/bui/components/menus.py | 3 +- bindings/python/tests/test_components.py | 15 +++++ kit/intern/kit_draw.c | 13 +++++ kit/intern/kit_intern.h | 2 + kit/intern/kit_menus.c | 56 +++++++++++++++++++ .../docs/python/components-reference.mdx | 2 +- .../src/content/docs/python/components.mdx | 2 +- 7 files changed, 90 insertions(+), 3 deletions(-) diff --git a/bindings/python/bui/components/menus.py b/bindings/python/bui/components/menus.py index 37f969d..cc5aa8b 100644 --- a/bindings/python/bui/components/menus.py +++ b/bindings/python/bui/components/menus.py @@ -95,7 +95,8 @@ def show(ctx, owner, items, title: str = None, at=None, on_pick=None) -> None: class PieMenu: """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, or press a number key in its numpad direction (8 north, 6 east). Opened by a held key, releasing the key picks + 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".""" diff --git a/bindings/python/tests/test_components.py b/bindings/python/tests/test_components.py index 04d3d16..4cb085f 100644 --- a/bindings/python/tests/test_components.py +++ b/bindings/python/tests/test_components.py @@ -572,6 +572,21 @@ def test_pie_number_keys(): 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_menu_at(): from bui.components import MenuItem, PieMenu got = [] diff --git a/kit/intern/kit_draw.c b/kit/intern/kit_draw.c index 210b80b..8bd98be 100644 --- a/kit/intern/kit_draw.c +++ b/kit/intern/kit_draw.c @@ -1159,6 +1159,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 +1227,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_intern.h b/kit/intern/kit_intern.h index 3b25360..6ad4c86 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). */ diff --git a/kit/intern/kit_menus.c b/kit/intern/kit_menus.c index 66f104c..e97dda9 100644 --- a/kit/intern/kit_menus.c +++ b/kit/intern/kit_menus.c @@ -886,6 +886,7 @@ typedef struct PieData { 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; @@ -1103,6 +1104,16 @@ static int pie_key(BKNode *n, BKEvent *e) } 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")) { pie_pick(n, n->hot); return 1; @@ -1126,6 +1137,48 @@ static int pie_key_up(BKNode *n, BKEvent *e) 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); @@ -1172,6 +1225,7 @@ static void pie_draw(BKNode *n) 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. */ @@ -1185,6 +1239,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); } } @@ -1229,6 +1284,7 @@ void BK_open_pie( place_init(&pd->menu.place); 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 = at ? at_x : ctx->pointer_x; pd->at_y = at ? at_y : ctx->pointer_y; diff --git a/website/src/content/docs/python/components-reference.mdx b/website/src/content/docs/python/components-reference.mdx index 262a6c8..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="", 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). 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**. | +| `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 87c28a8..9c3d068 100644 --- a/website/src/content/docs/python/components.mdx +++ b/website/src/content/docs/python/components.mdx @@ -207,7 +207,7 @@ app.run() - **`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="", 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 the number key in the item's numpad direction (8 north, 6 east, 2 south, 4 west, and so on). 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. +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. Every value also has a **right-click menu** for free, with Copy, Paste and Reset. From 2c5b5c044ac461b603f920e60e74c7814a27f19f Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 14:08:42 +0530 Subject: [PATCH 12/51] pie: open animation --- bindings/python/tests/test_components.py | 24 +++++++++++++++++++ kit/intern/kit_core.c | 5 ++-- kit/intern/kit_intern.h | 3 +++ kit/intern/kit_menus.c | 21 +++++++++++++--- .../src/content/docs/python/components.mdx | 2 +- 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/bindings/python/tests/test_components.py b/bindings/python/tests/test_components.py index 4cb085f..bc75370 100644 --- a/bindings/python/tests/test_components.py +++ b/bindings/python/tests/test_components.py @@ -587,6 +587,30 @@ def test_pie_accelerator_keys(): 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 = [] diff --git a/kit/intern/kit_core.c b/kit/intern/kit_core.c index e96c263..42a1611 100644 --- a/kit/intern/kit_core.c +++ b/kit/intern/kit_core.c @@ -3685,9 +3685,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; diff --git a/kit/intern/kit_intern.h b/kit/intern/kit_intern.h index 6ad4c86..d13b864 100644 --- a/kit/intern/kit_intern.h +++ b/kit/intern/kit_intern.h @@ -290,6 +290,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); diff --git a/kit/intern/kit_menus.c b/kit/intern/kit_menus.c index e97dda9..1bf0843 100644 --- a/kit/intern/kit_menus.c +++ b/kit/intern/kit_menus.c @@ -874,6 +874,7 @@ static const short g_pie_angle[8] = {90, 45, 0, 315, 270, 225, 180, 135}; * 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 { @@ -926,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); } @@ -1222,7 +1225,16 @@ 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); @@ -1253,6 +1265,7 @@ 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, @@ -1296,7 +1309,9 @@ void BK_open_pie( 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/website/src/content/docs/python/components.mdx b/website/src/content/docs/python/components.mdx index 9c3d068..2e48ee2 100644 --- a/website/src/content/docs/python/components.mdx +++ b/website/src/content/docs/python/components.mdx @@ -207,7 +207,7 @@ app.run() - **`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="", 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 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. +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. From de0ce2e4040284eec0b96795a620fb4968ce6857 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 15:08:44 +0530 Subject: [PATCH 13/51] rust: on_focus in BKWindowCallbacks --- bindings/rust/blenderui-sys/src/lib.rs | 1 + bindings/rust/blenderui/src/window.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) 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/window.rs b/bindings/rust/blenderui/src/window.rs index a59814b..7d019f2 100644 --- a/bindings/rust/blenderui/src/window.rs +++ b/bindings/rust/blenderui/src/window.rs @@ -947,7 +947,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: None }; 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); From ba7c2449a97fb97d7fa1baeede55eb86ff0b5b23 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 15:10:50 +0530 Subject: [PATCH 14/51] wayland: wait for the first configure --- kit/intern/kit_window_wayland.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/kit/intern/kit_window_wayland.c b/kit/intern/kit_window_wayland.c index 5b5f0f0..dd11763 100644 --- a/kit/intern/kit_window_wayland.c +++ b/kit/intern/kit_window_wayland.c @@ -1200,6 +1200,16 @@ 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); @@ -1236,7 +1246,7 @@ BKWindow *bk_wl_window_new(BKContext *ctx, const char *title, int width, int hei 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; } @@ -1292,6 +1302,7 @@ static void set_hidden(BKWindow *win, int hidden) else { role_create(win); wl_surface_commit(win->surface); + wait_configure(win); win->full = 1; } } From 57ad36aef930709eede10f8ede069388f8de72e3 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 15:14:20 +0530 Subject: [PATCH 15/51] imgui: recover the frame after a Python error --- bindings/imgui/c/cimgui_impl_bui.cpp | 1 + bindings/imgui/c/cimgui_impl_bui.h | 1 + bindings/imgui/imgui_impl_bui.cpp | 16 ++++++++++++++++ bindings/imgui/imgui_impl_bui.h | 3 +++ bindings/python/src/buiimgui_module.cpp | 1 + 5 files changed, 22 insertions(+) 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/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; From 634de0c6ec8f0e7e8f940a64242ed915ccae9186 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 15:16:00 +0530 Subject: [PATCH 16/51] tk test: Tk 9's default tearoff --- bindings/python/tests/test_tk_compat.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bindings/python/tests/test_tk_compat.py b/bindings/python/tests/test_tk_compat.py index 2c28362..d62ad1c 100644 --- a/bindings/python/tests/test_tk_compat.py +++ b/bindings/python/tests/test_tk_compat.py @@ -510,6 +510,9 @@ def _normalize(v): _LOOSE = {"prog_canvas": {"canvasx", "hbar", "xview"}, "prog_widgets": {"lbl_font"}, "prog_todo": {"sb"}, "prog_editor": {"scrolled", "sb_follows"}, "prog_scrollable_frame": {"scrolled", "sb", "region_set"}} +# ... and where Tk 9 changed a default that bui.tksyntax (TkVersion 8.6) keeps: a menu's +# default tearoff is 0 on every platform +_LOOSE_TK9 = {"prog_editor": {"view_types"}} # ... and what BUI must observe for them (stdlib Tk's depends on window mapping timing) _BUI_EXPECT = {"prog_editor": {"scrolled": True, "sb_follows": True}, "prog_canvas": {"xview": True, "canvasx": 200.0}, @@ -528,6 +531,8 @@ def test_programs_match_stdlib(): continue want = prog(*real) loose = _LOOSE.get(prog.__name__, set()) + if real[0].TkVersion >= 9: + loose = loose | _LOOSE_TK9.get(prog.__name__, set()) for key, ref in want.items(): if key in loose: continue From 0ca786ae04f88178edb6d8492c5f5dd7aabaed86 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 20:57:48 +0530 Subject: [PATCH 17/51] Revert "tk test: Tk 9's default tearoff" --- bindings/python/tests/test_tk_compat.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bindings/python/tests/test_tk_compat.py b/bindings/python/tests/test_tk_compat.py index d62ad1c..2c28362 100644 --- a/bindings/python/tests/test_tk_compat.py +++ b/bindings/python/tests/test_tk_compat.py @@ -510,9 +510,6 @@ def _normalize(v): _LOOSE = {"prog_canvas": {"canvasx", "hbar", "xview"}, "prog_widgets": {"lbl_font"}, "prog_todo": {"sb"}, "prog_editor": {"scrolled", "sb_follows"}, "prog_scrollable_frame": {"scrolled", "sb", "region_set"}} -# ... and where Tk 9 changed a default that bui.tksyntax (TkVersion 8.6) keeps: a menu's -# default tearoff is 0 on every platform -_LOOSE_TK9 = {"prog_editor": {"view_types"}} # ... and what BUI must observe for them (stdlib Tk's depends on window mapping timing) _BUI_EXPECT = {"prog_editor": {"scrolled": True, "sb_follows": True}, "prog_canvas": {"xview": True, "canvasx": 200.0}, @@ -531,8 +528,6 @@ def test_programs_match_stdlib(): continue want = prog(*real) loose = _LOOSE.get(prog.__name__, set()) - if real[0].TkVersion >= 9: - loose = loose | _LOOSE_TK9.get(prog.__name__, set()) for key, ref in want.items(): if key in loose: continue From d7dde3f20bf85ef0e9a7afe6a37b2ca96558ee3f Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 21:07:54 +0530 Subject: [PATCH 18/51] changelog: Linux windows, hit test, focus, pies --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddd94d4..54bc152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,48 @@ All notable changes to **blenderui** (BUI). The format follows ## [Unreleased] +### Added +- **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. Transparent windows keep the software renderer. +- **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. +- **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). +- **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 +- `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. +- `BKWindowCallbacks` has the new `on_focus` member, and so has the Rust `blenderui-sys` struct. + ### Fixed - **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 From 3a48261ffba3e929c0bdfec613537505cd1228b1 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 17:33:21 +0530 Subject: [PATCH 19/51] docs: window requests, focus, hit test, raster alpha, imgui abandon --- README.md | 2 +- docs/c-api.md | 3 ++- docs/components.md | 4 ++-- docs/imgui.md | 2 +- website/src/content/docs/c/c-api.mdx | 1 + website/src/content/docs/imgui/c.mdx | 2 +- website/src/content/docs/imgui/cpp.mdx | 1 + website/src/content/docs/lua/overview.mdx | 4 ++++ website/src/content/docs/python/components.mdx | 4 ++-- 9 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a8e40c4..a17c138 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,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/docs/c-api.md b/docs/c-api.md index df1db5e..2888543 100644 --- a/docs/c-api.md +++ b/docs/c-api.md @@ -80,10 +80,11 @@ 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, resizable, minsize, state; on Linux also override, transparent, and on Wayland 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. | | **Text** | `BUI_text_measure`, `BUI_raster_text`, `BUI_text_add_font(family, flags, data, len)` (register a font; needed on wasm) | +| **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`). | diff --git a/docs/components.md b/docs/components.md index 157369f..10cc722 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 (on Linux: `"transparent"`, `"override"`, `"topmost"`, 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/website/src/content/docs/c/c-api.mdx b/website/src/content/docs/c/c-api.mdx index 6478901..9f4988a 100644 --- a/website/src/content/docs/c/c-api.mdx +++ b/website/src/content/docs/c/c-api.mdx @@ -262,6 +262,7 @@ 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 | diff --git a/website/src/content/docs/imgui/c.mdx b/website/src/content/docs/imgui/c.mdx index d9ef9db..61ae20c 100644 --- a/website/src/content/docs/imgui/c.mdx +++ b/website/src/content/docs/imgui/c.mdx @@ -47,7 +47,7 @@ add_executable(myapp main.c) target_link_libraries(myapp PRIVATE bui::cimgui) ``` -The backend calls `ImGui_NewFrame()`, your `frame(user)` and `ImGui_Render()` itself, and Blender's style and font are on by default. BUI's own C demo is the `bui_imgui_demo_c` target (`examples/imgui_demo_c.c`). +The backend calls `ImGui_NewFrame()`, your `frame(user)` and `ImGui_Render()` itself, and Blender's style and font are on by default. If `frame` stops early on an error, `cImGui_ImplBUI_AbandonFrame()` ends the frame with Dear ImGui's error recovery. BUI's own C demo is the `bui_imgui_demo_c` target (`examples/imgui_demo_c.c`).
Date: Sun, 8 Nov 2026 17:34:17 +0530 Subject: [PATCH 20/51] docs: drop imgui abandon and lua context from tutorials --- website/src/content/docs/imgui/c.mdx | 2 +- website/src/content/docs/imgui/cpp.mdx | 1 - website/src/content/docs/lua/overview.mdx | 4 ---- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/website/src/content/docs/imgui/c.mdx b/website/src/content/docs/imgui/c.mdx index 61ae20c..d9ef9db 100644 --- a/website/src/content/docs/imgui/c.mdx +++ b/website/src/content/docs/imgui/c.mdx @@ -47,7 +47,7 @@ add_executable(myapp main.c) target_link_libraries(myapp PRIVATE bui::cimgui) ``` -The backend calls `ImGui_NewFrame()`, your `frame(user)` and `ImGui_Render()` itself, and Blender's style and font are on by default. If `frame` stops early on an error, `cImGui_ImplBUI_AbandonFrame()` ends the frame with Dear ImGui's error recovery. BUI's own C demo is the `bui_imgui_demo_c` target (`examples/imgui_demo_c.c`). +The backend calls `ImGui_NewFrame()`, your `frame(user)` and `ImGui_Render()` itself, and Blender's style and font are on by default. BUI's own C demo is the `bui_imgui_demo_c` target (`examples/imgui_demo_c.c`).
Date: Sun, 8 Nov 2026 17:41:11 +0530 Subject: [PATCH 21/51] rust: window activation hook and transparent background --- bindings/rust/blenderui/src/window.rs | 50 ++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/bindings/rust/blenderui/src/window.rs b/bindings/rust/blenderui/src/window.rs index 7d019f2..a795945 100644 --- a/bindings/rust/blenderui/src/window.rs +++ b/bindings/rust/blenderui/src/window.rs @@ -203,6 +203,16 @@ 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. Linux (X11, Wayland) only; the window + /// draws on the software raster. + Transparent, +} + #[derive(Clone, Debug, Default)] pub struct TitlebarOptions { pub title: Option, @@ -218,6 +228,7 @@ 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, } /// The application entry point (gpui's `Application`). @@ -364,6 +375,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 +414,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 +463,9 @@ 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); + } if options.gpu == Some(true) { sys::BK_window_set_gpu(*n, 1); } @@ -502,6 +519,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 +838,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 { @@ -948,7 +982,7 @@ impl Window { } 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_focus: None }; + 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); From 313d8b93cddb79376736cbfbd1fa3d0d68476ec5 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 17:42:25 +0530 Subject: [PATCH 22/51] python: drop X11 autorepeat key releases in the Tk host --- bindings/python/bui/_surface.py | 11 +++++++++++ bindings/python/tests/test_tk_input.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) 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/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_"): From ca2869d088b3dd5646e63eeaebc0890745979349 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 17:42:37 +0530 Subject: [PATCH 23/51] docs: rust window activation and transparent background --- CHANGELOG.md | 4 ++++ docs/rust.md | 2 +- website/src/content/docs/rust/gpui-syntax.mdx | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39516f8..2217e50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ All notable changes to **blenderui** (BUI). The format follows 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. - **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. @@ -43,6 +45,8 @@ All notable changes to **blenderui** (BUI). The format follows - `BKWindowCallbacks` has the new `on_focus` member, and so has the Rust `blenderui-sys` struct. ### Fixed +- **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). - **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 diff --git a/docs/rust.md b/docs/rust.md index 2087173..4fcb9f4 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`, `window_background` (`WindowBackgroundAppearance::Transparent` on Linux); `App::on_window_activation` reports focus | | `cx.set_global`, `cx.global` | the same | | `cx.stop_propagation()` | the same | diff --git a/website/src/content/docs/rust/gpui-syntax.mdx b/website/src/content/docs/rust/gpui-syntax.mdx index 7cb5c82..fa94203 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` and `window_background` (`WindowBackgroundAppearance::Transparent` on Linux) | | `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. From 0141607248ebb9fe818e9aad5681cd603d0a34fe Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 18:40:16 +0530 Subject: [PATCH 24/51] wayland: cursor-shape cursors, as GHOST --- CHANGELOG.md | 3 ++ kit/CMakeLists.txt | 18 ++++++-- kit/intern/kit_window_wayland.c | 77 +++++++++++++++++++++++++++------ 3 files changed, 81 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2217e50..becb4ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,9 @@ All notable changes to **blenderui** (BUI). The format follows 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. +- **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. ### Fixed diff --git a/kit/CMakeLists.txt b/kit/CMakeLists.txt index cf70ca9..1cf9278 100644 --- a/kit/CMakeLists.txt +++ b/kit/CMakeLists.txt @@ -174,9 +174,21 @@ elseif(UNIX AND NOT APPLE) file(MAKE_DIRECTORY ${_wl_gen}) set(_wl_src) # wlr-layer-shell is not in wayland-protocols: kit/extern/wayland_protocols has it - foreach(_x ${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) + 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 diff --git a/kit/intern/kit_window_wayland.c b/kit/intern/kit_window_wayland.c index dd11763..37a1db4 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. @@ -29,6 +30,9 @@ # 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" @@ -153,6 +157,10 @@ static struct { 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; @@ -453,22 +461,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]); @@ -874,6 +924,11 @@ static void registry_global(void *d, struct wl_registry *reg, uint32_t name, con 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); @@ -959,12 +1014,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; From e5aa0e7783a809edb6b98f2ddf819fa2b3c6b17a Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 18:40:27 +0530 Subject: [PATCH 25/51] docs: wayland cursor-shape --- website/src/content/docs/concepts/window-hosts.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 37ab058efeef83772f905f1687653d7f042f03ce Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 19:48:49 +0530 Subject: [PATCH 26/51] wayland: copy only the damage after a full frame --- kit/intern/kit_window_wayland.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/kit/intern/kit_window_wayland.c b/kit/intern/kit_window_wayland.c index 37a1db4..8159a50 100644 --- a/kit/intern/kit_window_wayland.c +++ b/kit/intern/kit_window_wayland.c @@ -325,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++) { @@ -337,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); } From 3ec5a5ec8541711f04c54d4486a2d9c82b9dd978 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 19:48:49 +0530 Subject: [PATCH 27/51] kit: no relayout or redraw for unchanged values and canvas items --- CHANGELOG.md | 7 +++++++ kit/include/BUI_kit.h | 2 +- kit/intern/kit_core.c | 3 +++ kit/intern/kit_frame.c | 37 +++++++++++++++++++++++++++++++++++-- 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index becb4ee..2955d1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,13 @@ All notable changes to **blenderui** (BUI). The format follows - `BKWindowCallbacks` has the new `on_focus` member, and so has the Rust `blenderui-sys` struct. ### Fixed +- **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). - **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. diff --git a/kit/include/BUI_kit.h b/kit/include/BUI_kit.h index 03d48b5..ea7b2de 100644 --- a/kit/include/BUI_kit.h +++ b/kit/include/BUI_kit.h @@ -454,7 +454,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 */ diff --git a/kit/intern/kit_core.c b/kit/intern/kit_core.c index 22a259e..9610682 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; } 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) From 37d5ced5c9e78e4abfadc9b3cc925e7a4c7b992c Mon Sep 17 00:00:00 2001 From: nusaturn Date: Sun, 8 Nov 2026 19:55:36 +0530 Subject: [PATCH 28/51] js: transparent windows, focus and blur events --- CHANGELOG.md | 2 ++ bindings/node/index.d.ts | 5 +++- bindings/node/index.js | 23 +++++++++++++------ bindings/rust/blenderui-jsview/src/lib.rs | 20 ++++++++++++---- bindings/rust/blenderui-node/src/lib.rs | 2 ++ bindings/rust/blenderui-qjs/src/main.rs | 6 +++-- bindings/rust/blenderui-qjs/src/prelude.js | 2 +- .../content/docs/javascript/native-api.mdx | 4 ++-- 8 files changed, 47 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2955d1c..e5b4194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ All notable changes to **blenderui** (BUI). The format follows 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. +- **JavaScript** (Node, Bun and `bui build` apps): `app.window({ transparent: true })` and the window events `focus` and + `blur`. - **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. diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index aa46202..bb18d74 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -18,6 +18,8 @@ export interface WindowOptions { height?: number resizable?: boolean gpu?: boolean + /** Per-pixel alpha: only the elements show (Linux; CPU renderer). */ + transparent?: boolean } export type EventType = 'click' | 'mousedown' | 'mouseup' | 'mousemove' | 'mouseenter' | 'mouseleave' | @@ -199,7 +201,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..8fd9976 100644 --- a/bindings/node/index.js +++ b/bindings/node/index.js @@ -52,7 +52,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 @@ -65,7 +65,7 @@ class Window { 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, + resizable: options.resizable, gpu: options.gpu, transparent: options.transparent, }) } @@ -154,17 +154,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 +273,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/rust/blenderui-jsview/src/lib.rs b/bindings/rust/blenderui-jsview/src/lib.rs index 123a818..bc11ca4 100644 --- a/bindings/rust/blenderui-jsview/src/lib.rs +++ b/bindings/rust/blenderui-jsview/src/lib.rs @@ -51,6 +51,8 @@ pub struct WindowConfig { pub height: Option, pub resizable: Option, pub gpu: Option, + /// Per-pixel alpha, only the elements show (Linux). + pub transparent: Option, } /// A layout box, logical pixels. @@ -415,21 +417,26 @@ 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) { @@ -441,6 +448,11 @@ 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 + }, ..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..f176b1a 100644 --- a/bindings/rust/blenderui-node/src/lib.rs +++ b/bindings/rust/blenderui-node/src/lib.rs @@ -83,6 +83,7 @@ pub struct WindowConfig { pub height: Option, pub resizable: Option, pub gpu: Option, + pub transparent: Option, } #[napi(object)] @@ -222,6 +223,7 @@ 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, }).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..45a37d9 100644 --- a/bindings/rust/blenderui-qjs/src/main.rs +++ b/bindings/rust/blenderui-qjs/src/main.rs @@ -397,8 +397,10 @@ 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, title: Option, width: f64, height: f64, resizable: Option, gpu: Option, + transparent: Option| { + command(Command::Open(id, view::WindowConfig { title, width: Some(width), height: Some(height), resizable, gpu, + transparent })); }); 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..f0b1599 100644 --- a/bindings/rust/blenderui-qjs/src/prelude.js +++ b/bindings/rust/blenderui-qjs/src/prelude.js @@ -112,7 +112,7 @@ 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) + c.gpu ?? this._options.gpu ?? null, c.transparent ?? null) } commit (window, ops, strings, animated) { host.commit(window, ops, strings, !!animated) } diff --git a/website/src/content/docs/javascript/native-api.mdx b/website/src/content/docs/javascript/native-api.mdx index 436be07..50aee0d 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) and `transparent` (per-pixel alpha on Linux, CPU renderer). `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 From b584c6dda92ade2d7bccd4706f768b9d037563a4 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 01:53:20 +0530 Subject: [PATCH 29/51] kit: text shadow switch, off in python --- CHANGELOG.md | 2 ++ bindings/python/src/buipy_kit.c | 13 +++++++++++++ docs/c-api.md | 2 +- kit/include/BUI_kit.h | 3 +++ kit/intern/kit_draw.c | 16 ++++++++++++++-- website/src/content/docs/c/c-api.mdx | 1 + .../src/content/docs/concepts/text-and-fonts.mdx | 2 +- 7 files changed, 35 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b4194..b071063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ All notable changes to **blenderui** (BUI). The format follows 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. diff --git a/bindings/python/src/buipy_kit.c b/bindings/python/src/buipy_kit.c index 5cbcf9e..48c1f8b 100644 --- a/bindings/python/src/buipy_kit.c +++ b/bindings/python/src/buipy_kit.c @@ -1958,6 +1958,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()); @@ -2281,6 +2291,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)."}, @@ -2699,6 +2711,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/docs/c-api.md b/docs/c-api.md index 2888543..d67cf46 100644 --- a/docs/c-api.md +++ b/docs/c-api.md @@ -82,7 +82,7 @@ int main(void) | **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; on Linux also override, transparent, and on Wayland 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) | | **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` | diff --git a/kit/include/BUI_kit.h b/kit/include/BUI_kit.h index ea7b2de..7d7542c 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 diff --git a/kit/intern/kit_draw.c b/kit/intern/kit_draw.c index 8bd98be..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++) { diff --git a/website/src/content/docs/c/c-api.mdx b/website/src/content/docs/c/c-api.mdx index 9f4988a..41af309 100644 --- a/website/src/content/docs/c/c-api.mdx +++ b/website/src/content/docs/c/c-api.mdx @@ -265,6 +265,7 @@ BK_custom_set(node, &cls, my_data); | 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. From 9080f51450618523fb441e51e7c59767715a1460 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 02:05:01 +0530 Subject: [PATCH 30/51] kit: forget a popup's animations when it closes --- CHANGELOG.md | 2 ++ kit/intern/kit_core.c | 24 +----------------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b071063..52a0ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ All notable changes to **blenderui** (BUI). The format follows - `BKWindowCallbacks` has the new `on_focus` member, and so has the Rust `blenderui-sys` struct. ### 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 diff --git a/kit/intern/kit_core.c b/kit/intern/kit_core.c index 9610682..9c267a1 100644 --- a/kit/intern/kit_core.c +++ b/kit/intern/kit_core.c @@ -1764,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]; From feb61c8afc9c8fd4ba3eb844f6e546657e46ca82 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 14:49:02 +0530 Subject: [PATCH 31/51] win32: transparent windows through UpdateLayeredWindow --- gpu/include/GPU_raster.h | 7 +++ gpu/intern/gpu_raster.c | 112 ++++++++++++++++++++++++++++++++++ kit/intern/kit_window_win32.c | 84 ++++++++++++++++++++++--- 3 files changed, 193 insertions(+), 10 deletions(-) diff --git a/gpu/include/GPU_raster.h b/gpu/include/GPU_raster.h index 79d6f9c..300f722 100644 --- a/gpu/include/GPU_raster.h +++ b/gpu/include/GPU_raster.h @@ -344,6 +344,13 @@ const BuiGpuSink *BUI_raster_sink(BuiRaster *r); * window: an HWND on Windows. Returns 0 when unsupported on this platform. */ int BUI_raster_present_native(BuiRaster *r, void *window_handle, int full); +/* Show a frame that keeps alpha (BUI_raster_set_alpha) in a layered HWND with per-pixel alpha + * (UpdateLayeredWindow). `opacity` 0..255 scales the whole window. The window needs + * WS_EX_LAYERED and no frame. Always the whole window, so `full` only refreshes every + * pixel of the copy kept here. Returns 0 on a GPU frame, a frame without alpha, or + * a platform other than Windows. */ +int BUI_raster_present_layered(BuiRaster *r, void *window_handle, int opacity, int full); + #ifdef __cplusplus } #endif diff --git a/gpu/intern/gpu_raster.c b/gpu/intern/gpu_raster.c index 48d99c7..0641ea0 100644 --- a/gpu/intern/gpu_raster.c +++ b/gpu/intern/gpu_raster.c @@ -35,9 +35,12 @@ struct BuiRaster { /* 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; + void *layer; /* BUI_raster_present_layered: the frame as a DIB section (Win32) */ struct BuiRaster *scratch; /* CPU coverage for polygons in GPU mode */ }; +static void layer_free(BuiRaster *r); + /* -------------------------------------------------------------------- */ /** \name Pixel helpers * \{ */ @@ -198,6 +201,7 @@ void BUI_raster_free(BuiRaster *r) return; } BUI_raster_gpu_release(r); + layer_free(r); BUI_raster_free(r->scratch); free(r->pix); free(r->prev); @@ -4336,6 +4340,100 @@ int BUI_raster_present_native(BuiRaster *r, void *window_handle, int full) ReleaseDC(hwnd, hdc); return 1; } + +/* The frame as a top-down 32 bpp DIB section, the surface UpdateLayeredWindow reads. */ +typedef struct Layer { + HDC dc; + HBITMAP bmp, old; + uint32_t *bits; + int w, h; +} Layer; + +static void layer_free(BuiRaster *r) +{ + Layer *l = (Layer *)r->layer; + if (!l) { + return; + } + if (l->old) { + SelectObject(l->dc, l->old); + } + if (l->bmp) { + DeleteObject(l->bmp); + } + if (l->dc) { + DeleteDC(l->dc); + } + free(l); + r->layer = NULL; +} + +/* The layer at the frame's size. `fresh` is set when it was made now (its pixels are empty). */ +static Layer *layer_get(BuiRaster *r, int *fresh) +{ + Layer *l = (Layer *)r->layer; + *fresh = 0; + if (l && l->w == r->w && l->h == r->h) { + return l; + } + layer_free(r); + l = (Layer *)calloc(1, sizeof(Layer)); + if (!l) { + return NULL; + } + r->layer = l; + BITMAPINFO bmi; + memset(&bmi, 0, sizeof(bmi)); + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = r->w; + bmi.bmiHeader.biHeight = -r->h; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + l->dc = CreateCompatibleDC(NULL); + l->bmp = l->dc ? CreateDIBSection(l->dc, &bmi, DIB_RGB_COLORS, (void **)&l->bits, NULL, 0) : NULL; + if (!l->bmp) { + layer_free(r); + return NULL; + } + l->old = (HBITMAP)SelectObject(l->dc, l->bmp); + l->w = r->w; + l->h = r->h; + *fresh = 1; + return l; +} + +int BUI_raster_present_layered(BuiRaster *r, void *window_handle, int opacity, int full) +{ + HWND hwnd = (HWND)window_handle; + if (r->gpu || !r->alpha || r->w <= 0 || r->h <= 0 || !hwnd || !IsWindow(hwnd)) { + return 0; + } + int fresh; + Layer *l = layer_get(r, &fresh); + if (!l) { + return 0; + } + if (fresh || full) { + memcpy(l->bits, r->pix, (size_t)r->w * (size_t)r->h * sizeof(uint32_t)); + } + else { + for (int i = 0; i < r->damage_len; i++) { + const int x = r->damage[i][0], y = r->damage[i][1], w = r->damage[i][2], h = r->damage[i][3]; + for (int row = y; row < y + h; row++) { + memcpy(l->bits + (size_t)row * r->w + x, r->pix + (size_t)row * r->w + x, + (size_t)w * sizeof(uint32_t)); + } + } + } + /* The raster is premultiplied 0xAARRGGBB, as ULW_ALPHA with AC_SRC_ALPHA needs. The size + * is passed each time: a layered window's surface follows it, not the window. */ + const BLENDFUNCTION blend = {AC_SRC_OVER, 0, (BYTE)opacity, AC_SRC_ALPHA}; + const POINT src = {0, 0}; + SIZE size = {r->w, r->h}; + return UpdateLayeredWindow(hwnd, NULL, NULL, &size, l->dc, &src, 0, (BLENDFUNCTION *)&blend, + ULW_ALPHA) != 0; +} #else int BUI_raster_present_native(BuiRaster *r, void *window_handle, int full) { @@ -4349,6 +4447,20 @@ int BUI_raster_present_native(BuiRaster *r, void *window_handle, int full) (void)full; return 0; } + +static void layer_free(BuiRaster *r) +{ + (void)r; +} + +int BUI_raster_present_layered(BuiRaster *r, void *window_handle, int opacity, int full) +{ + (void)r; + (void)window_handle; + (void)opacity; + (void)full; + return 0; +} #endif /** \} */ diff --git a/kit/intern/kit_window_win32.c b/kit/intern/kit_window_win32.c index ad27b8c..5e3c738 100644 --- a/kit/intern/kit_window_win32.c +++ b/kit/intern/kit_window_win32.c @@ -47,6 +47,9 @@ 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": per-pixel alpha (UpdateLayeredWindow) */ + BYTE opacity; /* "alpha" as 0..255 */ BKImeWin32 ime; /* the IME (GHOST_ImeWin32) */ WPARAM last_vk; /* the virtual key of the last WM_KEYDOWN */ }; @@ -364,6 +367,17 @@ 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 present(BKWindow *win, int full) +{ + if (win->layered) { + BUI_raster_present_layered(win->raster, win->hwnd, win->opacity, full); + } + else { + BUI_raster_present_native(win->raster, win->hwnd, full); + } +} + static void do_render(BKWindow *win) { RECT rc; @@ -387,7 +401,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,7 +513,7 @@ 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: @@ -770,6 +784,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 */ @@ -886,7 +901,7 @@ BuiRaster *BK_window_raster(BKWindow *win) void BK_window_set_gpu(BKWindow *win, int on) { - if (win && on) { + if (win && on && !win->layered) { /* per-pixel alpha needs the software raster */ gpu_attach(win); host_request(win); } @@ -946,6 +961,49 @@ 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); +} + +/* "transparent": per-pixel alpha. The raster keeps alpha and UpdateLayeredWindow shows it, so + * the window has no frame and no GPU renderer (Direct3D 11 draws opaque frames). */ +static void set_transparent(BKWindow *win, int on) +{ + if (on == win->layered) { + return; + } + const LONG_PTR ex = GetWindowLongPtrW(win->hwnd, GWL_EXSTYLE) & ~WS_EX_LAYERED; + win->layered = on; + BUI_raster_set_alpha(win->raster, on); + /* UpdateLayeredWindow fails once SetLayeredWindowAttributes ran, until the style is set again */ + SetWindowLongPtrW(win->hwnd, GWL_EXSTYLE, ex); + if (on) { + BUI_raster_gpu_release(win->raster); + SetWindowLongPtrW(win->hwnd, GWL_EXSTYLE, ex | WS_EX_LAYERED); + } + else if (win->opacity != 255) { + set_opacity(win); + } + set_frame(win); + win->full = 1; + host_request(win); +} + void BK_window_wm(BKWindow *win, const char *key, double a, double b) { HWND h = win->hwnd; @@ -976,15 +1034,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) { /* the next present scales the frame */ + win->full = 1; + host_request(win); + } + else { + 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}; From a64f59890f36de2695c615a4d2035665830ad34c Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 14:49:02 +0530 Subject: [PATCH 32/51] cocoa: transparent windows --- kit/intern/kit_window_cocoa.m | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/kit/intern/kit_window_cocoa.m b/kit/intern/kit_window_cocoa.m index e63a71e..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, @@ -1019,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) @@ -1605,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]; From 2fb5168a85503f5160e4df8d2861d50bf347d91d Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 14:49:02 +0530 Subject: [PATCH 33/51] docs: transparent windows on windows and macos --- CHANGELOG.md | 5 +++++ bindings/python/bui/host.py | 2 +- docs/c-api.md | 2 +- docs/components.md | 2 +- docs/rust.md | 2 +- kit/include/BUI_kit.h | 5 +++-- website/src/content/docs/c/c-api.mdx | 2 +- website/src/content/docs/javascript/native-api.mdx | 2 +- website/src/content/docs/python/components.mdx | 2 +- website/src/content/docs/rust/gpui-syntax.mdx | 2 +- 10 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a0ca4..9b6240a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ All notable changes to **blenderui** (BUI). The format follows 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. Transparent windows keep the software renderer. +- **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. + The Direct3D 11 renderer stays opaque, so a transparent window drops it. - **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 diff --git a/bindings/python/bui/host.py b/bindings/python/bui/host.py index 8379e2e..0bf0eea 100644 --- a/bindings/python/bui/host.py +++ b/bindings/python/bui/host.py @@ -277,7 +277,7 @@ 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, transparent (native Linux windows, before show), layer + 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: diff --git a/docs/c-api.md b/docs/c-api.md index d67cf46..f99234c 100644 --- a/docs/c-api.md +++ b/docs/c-api.md @@ -80,7 +80,7 @@ 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; on Linux also override, transparent, and on Wayland layer and anchor for wlr-layer-shell), callbacks `on_timer`, `on_close`, `on_frame`, `on_focus`, `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; `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) | diff --git a/docs/components.md b/docs/components.md index 10cc722..b6341e2 100644 --- a/docs/components.md +++ b/docs/components.md @@ -36,7 +36,7 @@ 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`: `wm(key, a, b)` sends window requests (on Linux: `"transparent"`, `"override"`, `"topmost"`, Wayland `"layer"` and `"anchor"`), `on_focus(active)` reports keyboard focus. | +| `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/rust.md b/docs/rust.md index 4fcb9f4..4967441 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`, `window_background` (`WindowBackgroundAppearance::Transparent` on Linux); `App::on_window_activation` reports focus | +| `WindowOptions { window_bounds, titlebar, .. }`, `WindowBounds::Windowed(..)` | the same, plus `is_resizable`, `window_min_size`, `window_background` (`WindowBackgroundAppearance::Transparent`); `App::on_window_activation` reports focus | | `cx.set_global`, `cx.global` | the same | | `cx.stop_propagation()` | the same | diff --git a/kit/include/BUI_kit.h b/kit/include/BUI_kit.h index 7d7542c..3613bf3 100644 --- a/kit/include/BUI_kit.h +++ b/kit/include/BUI_kit.h @@ -701,8 +701,9 @@ 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". - * "transparent" (a, Linux): per-pixel alpha, no region background; set it before show (X11 - * makes the window again) and keep the software renderer. + * "transparent" (a): per-pixel alpha, no region background, software renderer. On Linux set it + * before show (X11 makes the window again). On Windows the window has no frame (the raster is + * shown with UpdateLayeredWindow); 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, diff --git a/website/src/content/docs/c/c-api.mdx b/website/src/content/docs/c/c-api.mdx index 41af309..8a56892 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`, `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. On Linux, `BK_window_wm(win, "transparent", 1, 0)` before `BK_window_show` 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; software rendering). `"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. +- **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, a non-opaque window on macOS; software rendering, and 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. diff --git a/website/src/content/docs/javascript/native-api.mdx b/website/src/content/docs/javascript/native-api.mdx index 50aee0d..4f5b629 100644 --- a/website/src/content/docs/javascript/native-api.mdx +++ b/website/src/content/docs/javascript/native-api.mdx @@ -61,7 +61,7 @@ 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`, `gpu` (to choose per window) and `transparent` (per-pixel alpha on Linux, CPU renderer). `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) and `transparent` (per-pixel alpha, CPU renderer). `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(event, fn)` for `close`, `focus` and `blur` (the window got or lost the keyboard focus). diff --git a/website/src/content/docs/python/components.mdx b/website/src/content/docs/python/components.mdx index 10ec06e..3f5bc2c 100644 --- a/website/src/content/docs/python/components.mdx +++ b/website/src/content/docs/python/components.mdx @@ -49,7 +49,7 @@ 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. Its `wm(key, a, b)` sends window requests (on Linux: `"transparent"`, `"override"`, `"topmost"`, Wayland `"layer"` and `"anchor"`), and `on_focus(active)` reports keyboard focus. | +| `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"` … | diff --git a/website/src/content/docs/rust/gpui-syntax.mdx b/website/src/content/docs/rust/gpui-syntax.mdx index fa94203..0d20dc6 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`, `gpu` and `window_background` (`WindowBackgroundAppearance::Transparent` on Linux) | +| `WindowOptions { window_bounds, titlebar, .. }`, `WindowBounds::Windowed(..)` | The same, plus `is_resizable`, `window_min_size`, `gpu` and `window_background` (`WindowBackgroundAppearance::Transparent`) | | `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 | From d26fb4922e80c026429a6d71d38ff46c32e21738 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 15:14:25 +0530 Subject: [PATCH 34/51] gpu: transparent frames in the OpenGL renderer --- gpu/include/GPU_raster.h | 6 +++-- gpu/intern/gpu_d3d11.c | 11 +++++++- gpu/intern/gpu_d3d11.h | 6 ++++- gpu/intern/gpu_gl.c | 33 +++++++++++++++++++---- gpu/intern/gpu_raster.c | 10 ++++--- gpu/tests/test_raster_alpha.c | 49 +++++++++++++++++++++++++++++++++++ kit/intern/kit_window_x11.c | 9 +++++-- 7 files changed, 109 insertions(+), 15 deletions(-) diff --git a/gpu/include/GPU_raster.h b/gpu/include/GPU_raster.h index 300f722..849838f 100644 --- a/gpu/include/GPU_raster.h +++ b/gpu/include/GPU_raster.h @@ -55,10 +55,11 @@ int BUI_raster_damage(const BuiRaster *r, const int (**r_rects)[4]); 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. CPU frames only. */ + * 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. CPU frames only. */ +/* 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. */ @@ -316,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..4335b8b 100644 --- a/gpu/intern/gpu_d3d11.c +++ b/gpu/intern/gpu_d3d11.c @@ -1682,9 +1682,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 +1800,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..d0a4811 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,7 @@ 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++) { + for (size_t i = 0; i < n && !g->alpha; 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 0641ea0..96ac197 100644 --- a/gpu/intern/gpu_raster.c +++ b/gpu/intern/gpu_raster.c @@ -276,7 +276,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; } @@ -490,9 +490,11 @@ int BUI_raster_alpha(const BuiRaster *r) void BUI_raster_clear(BuiRaster *r, int x, int y, int w, int h) { int x0, y0, x1, y1; - if (r->gpu || !clip_box(r, (float)x, (float)y, (float)(x + w), (float)(y + h), &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++) { diff --git a/gpu/tests/test_raster_alpha.c b/gpu/tests/test_raster_alpha.c index 7ce2e90..2e980fc 100644 --- a/gpu/tests/test_raster_alpha.c +++ b/gpu/tests/test_raster_alpha.c @@ -19,6 +19,54 @@ static int check(const char *what, int ok) 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}; @@ -48,6 +96,7 @@ int main(void) 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/intern/kit_window_x11.c b/kit/intern/kit_window_x11.c index 46ac2c6..27cb743 100644 --- a/kit/intern/kit_window_x11.c +++ b/kit/intern/kit_window_x11.c @@ -422,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); } } @@ -909,7 +910,7 @@ BuiRaster *BK_window_raster(BKWindow *win) void BK_window_set_gpu(BKWindow *win, int on) { - if (win && on && win->depth != 32) { /* a transparent window keeps the software raster */ + if (win && on) { gpu_attach(win); host_request(win); } @@ -984,11 +985,15 @@ void BK_window_wm(BKWindow *win, const char *key, double a, double b) } 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; } } From f6601800f840ab8146ae90cff2cbcc789b8de92f Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 15:17:38 +0530 Subject: [PATCH 35/51] win32: transparent windows with the Direct3D 11 renderer --- CHANGELOG.md | 8 +- gpu/include/GPU_raster.h | 7 - gpu/intern/gpu_d3d11.c | 98 ++++++++++++-- gpu/intern/gpu_raster.c | 112 ---------------- kit/include/BUI_kit.h | 8 +- kit/intern/kit_window_win32.c | 188 ++++++++++++++++++++++++--- website/src/content/docs/c/c-api.mdx | 2 +- 7 files changed, 266 insertions(+), 157 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b6240a..5bef163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,12 +16,16 @@ All notable changes to **blenderui** (BUI). The format follows - **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. Transparent windows keep the software renderer. + 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. - The Direct3D 11 renderer stays opaque, so a transparent window drops it. + With the Direct3D 11 renderer the window is made with `WS_EX_NOREDIRECTIONBITMAP` and shown by DirectComposition through a + premultiplied composition swap chain (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 `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 diff --git a/gpu/include/GPU_raster.h b/gpu/include/GPU_raster.h index 849838f..393c520 100644 --- a/gpu/include/GPU_raster.h +++ b/gpu/include/GPU_raster.h @@ -346,13 +346,6 @@ const BuiGpuSink *BUI_raster_sink(BuiRaster *r); * window: an HWND on Windows. Returns 0 when unsupported on this platform. */ int BUI_raster_present_native(BuiRaster *r, void *window_handle, int full); -/* Show a frame that keeps alpha (BUI_raster_set_alpha) in a layered HWND with per-pixel alpha - * (UpdateLayeredWindow). `opacity` 0..255 scales the whole window. The window needs - * WS_EX_LAYERED and no frame. Always the whole window, so `full` only refreshes every - * pixel of the copy kept here. Returns 0 on a GPU frame, a frame without alpha, or - * a platform other than Windows. */ -int BUI_raster_present_layered(BuiRaster *r, void *window_handle, int opacity, int full); - #ifdef __cplusplus } #endif diff --git a/gpu/intern/gpu_d3d11.c b/gpu/intern/gpu_d3d11.c index 4335b8b..4a78bb8 100644 --- a/gpu/intern/gpu_d3d11.c +++ b/gpu/intern/gpu_d3d11.c @@ -23,6 +23,8 @@ # define COBJMACROS # include # include +# include +# include # include # include @@ -39,6 +41,10 @@ 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}}; +static const GUID k_IID_IDCompositionDevice = { + 0xc37ea93a, 0xe7aa, 0x450d, {0xb1, 0x6f, 0x97, 0x46, 0xcb, 0x04, 0x07, 0xf3}}; # define REL(p) \ do { \ @@ -137,6 +143,14 @@ struct BuiGpu { IDXGIFactory2 *factory; IDXGISwapChain1 *swap; int sw, sh; /* swap chain size */ + /* alpha: frames keep premultiplied alpha. The swap chain then goes to the window through + * DirectComposition (no redirection surface: the window has WS_EX_NOREDIRECTIONBITMAP) and + * ClearView clears the rects of a redrawn region. */ + int alpha; + ID3D11DeviceContext1 *ctx1; + IDCompositionDevice *dcomp; + IDCompositionTarget *dtarget; + IDCompositionVisual *dvisual; int swap_fresh; ID3D11VertexShader *vs_quad, *vs_tri, *vs_mesh; @@ -304,6 +318,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, DirectComposition */ + } 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 +450,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); @@ -521,8 +541,12 @@ void bui_gpu_free(BuiGpu *g) REL(g->vs_tri); REL(g->ps_quad); REL(g->ps_tri); + REL(g->dvisual); + REL(g->dtarget); + REL(g->dcomp); REL(g->swap); REL(g->factory); + REL(g->ctx1); REL(g->ctx); REL(g->dev); free(g->inst); @@ -1393,8 +1417,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; } @@ -1545,6 +1569,29 @@ static void flush(BuiGpu *g) g->n_b = g->n_inst = g->n_tv = g->n_mv = g->n_mi = 0; } +/* Shows the swap chain through DirectComposition: the window has no redirection surface, so + * the compositor takes the premultiplied buffers directly (no copy, GPU blending). */ +static int dcomp_attach(BuiGpu *g) +{ + typedef HRESULT(WINAPI * CreateDeviceFn)(IDXGIDevice *, REFIID, void **); + HMODULE dll = LoadLibraryW(L"dcomp.dll"); + CreateDeviceFn create = dll ? (CreateDeviceFn)(void *)GetProcAddress(dll, + "DCompositionCreateDevice") : + NULL; + IDXGIDevice *xd = NULL; + const int ok = create && + SUCCEEDED(ID3D11Device_QueryInterface(g->dev, &k_IID_IDXGIDevice, (void **)&xd)) && + SUCCEEDED(create(xd, &k_IID_IDCompositionDevice, (void **)&g->dcomp)) && + SUCCEEDED(IDCompositionDevice_CreateTargetForHwnd(g->dcomp, g->hwnd, TRUE, + &g->dtarget)) && + SUCCEEDED(IDCompositionDevice_CreateVisual(g->dcomp, &g->dvisual)) && + SUCCEEDED(IDCompositionVisual_SetContent(g->dvisual, (IUnknown *)g->swap)) && + SUCCEEDED(IDCompositionTarget_SetRoot(g->dtarget, (IDCompositionVisual *)g->dvisual)) && + SUCCEEDED(IDCompositionDevice_Commit(g->dcomp)); /* once: Present needs no commit */ + REL(xd); + return ok; +} + static int ensure_swap(BuiGpu *g) { if (g->swap && g->sw == g->w && g->sh == g->h) { @@ -1566,15 +1613,30 @@ 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 (FAILED(IDXGIFactory2_CreateSwapChainForHwnd(g->factory, (IUnknown *)g->dev, g->hwnd, &sd, - NULL, NULL, &g->swap))) - { - return 0; + /* only a transparent window pays for premultiplied alpha: the compositor blends it */ + sd.Scaling = g->alpha ? DXGI_SCALING_STRETCH : DXGI_SCALING_NONE; + sd.AlphaMode = g->alpha ? DXGI_ALPHA_MODE_PREMULTIPLIED : DXGI_ALPHA_MODE_IGNORE; + if (g->alpha) { + if (FAILED(IDXGIFactory2_CreateSwapChainForComposition(g->factory, (IUnknown *)g->dev, &sd, + NULL, &g->swap)) || + !dcomp_attach(g)) + { + REL(g->dvisual); + REL(g->dtarget); + REL(g->dcomp); + REL(g->swap); + return 0; + } + } + else { + if (FAILED(IDXGIFactory2_CreateSwapChainForHwnd(g->factory, (IUnknown *)g->dev, g->hwnd, &sd, + NULL, NULL, &g->swap))) + { + return 0; + } + IDXGIFactory2_MakeWindowAssociation(g->factory, g->hwnd, DXGI_MWA_NO_ALT_ENTER); } - IDXGIFactory2_MakeWindowAssociation(g->factory, g->hwnd, DXGI_MWA_NO_ALT_ENTER); } g->sw = g->w; g->sh = g->h; @@ -1582,12 +1644,23 @@ 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))) { @@ -1643,7 +1716,8 @@ int bui_gpu_read(BuiGpu *g, uint32_t *dst, int w, int h) 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 */ + /* like the raster: 0x00RRGGBB, or premultiplied 0xAARRGGBB with alpha */ + dst[(size_t)y * w + x] = g->alpha ? src[x] : src[x] & 0xFFFFFFu; } } ID3D11DeviceContext_Unmap(g->ctx, (ID3D11Resource *)st, 0); diff --git a/gpu/intern/gpu_raster.c b/gpu/intern/gpu_raster.c index 96ac197..68e1775 100644 --- a/gpu/intern/gpu_raster.c +++ b/gpu/intern/gpu_raster.c @@ -35,12 +35,9 @@ struct BuiRaster { /* 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; - void *layer; /* BUI_raster_present_layered: the frame as a DIB section (Win32) */ struct BuiRaster *scratch; /* CPU coverage for polygons in GPU mode */ }; -static void layer_free(BuiRaster *r); - /* -------------------------------------------------------------------- */ /** \name Pixel helpers * \{ */ @@ -201,7 +198,6 @@ void BUI_raster_free(BuiRaster *r) return; } BUI_raster_gpu_release(r); - layer_free(r); BUI_raster_free(r->scratch); free(r->pix); free(r->prev); @@ -4342,100 +4338,6 @@ int BUI_raster_present_native(BuiRaster *r, void *window_handle, int full) ReleaseDC(hwnd, hdc); return 1; } - -/* The frame as a top-down 32 bpp DIB section, the surface UpdateLayeredWindow reads. */ -typedef struct Layer { - HDC dc; - HBITMAP bmp, old; - uint32_t *bits; - int w, h; -} Layer; - -static void layer_free(BuiRaster *r) -{ - Layer *l = (Layer *)r->layer; - if (!l) { - return; - } - if (l->old) { - SelectObject(l->dc, l->old); - } - if (l->bmp) { - DeleteObject(l->bmp); - } - if (l->dc) { - DeleteDC(l->dc); - } - free(l); - r->layer = NULL; -} - -/* The layer at the frame's size. `fresh` is set when it was made now (its pixels are empty). */ -static Layer *layer_get(BuiRaster *r, int *fresh) -{ - Layer *l = (Layer *)r->layer; - *fresh = 0; - if (l && l->w == r->w && l->h == r->h) { - return l; - } - layer_free(r); - l = (Layer *)calloc(1, sizeof(Layer)); - if (!l) { - return NULL; - } - r->layer = l; - BITMAPINFO bmi; - memset(&bmi, 0, sizeof(bmi)); - bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - bmi.bmiHeader.biWidth = r->w; - bmi.bmiHeader.biHeight = -r->h; - bmi.bmiHeader.biPlanes = 1; - bmi.bmiHeader.biBitCount = 32; - bmi.bmiHeader.biCompression = BI_RGB; - l->dc = CreateCompatibleDC(NULL); - l->bmp = l->dc ? CreateDIBSection(l->dc, &bmi, DIB_RGB_COLORS, (void **)&l->bits, NULL, 0) : NULL; - if (!l->bmp) { - layer_free(r); - return NULL; - } - l->old = (HBITMAP)SelectObject(l->dc, l->bmp); - l->w = r->w; - l->h = r->h; - *fresh = 1; - return l; -} - -int BUI_raster_present_layered(BuiRaster *r, void *window_handle, int opacity, int full) -{ - HWND hwnd = (HWND)window_handle; - if (r->gpu || !r->alpha || r->w <= 0 || r->h <= 0 || !hwnd || !IsWindow(hwnd)) { - return 0; - } - int fresh; - Layer *l = layer_get(r, &fresh); - if (!l) { - return 0; - } - if (fresh || full) { - memcpy(l->bits, r->pix, (size_t)r->w * (size_t)r->h * sizeof(uint32_t)); - } - else { - for (int i = 0; i < r->damage_len; i++) { - const int x = r->damage[i][0], y = r->damage[i][1], w = r->damage[i][2], h = r->damage[i][3]; - for (int row = y; row < y + h; row++) { - memcpy(l->bits + (size_t)row * r->w + x, r->pix + (size_t)row * r->w + x, - (size_t)w * sizeof(uint32_t)); - } - } - } - /* The raster is premultiplied 0xAARRGGBB, as ULW_ALPHA with AC_SRC_ALPHA needs. The size - * is passed each time: a layered window's surface follows it, not the window. */ - const BLENDFUNCTION blend = {AC_SRC_OVER, 0, (BYTE)opacity, AC_SRC_ALPHA}; - const POINT src = {0, 0}; - SIZE size = {r->w, r->h}; - return UpdateLayeredWindow(hwnd, NULL, NULL, &size, l->dc, &src, 0, (BLENDFUNCTION *)&blend, - ULW_ALPHA) != 0; -} #else int BUI_raster_present_native(BuiRaster *r, void *window_handle, int full) { @@ -4449,20 +4351,6 @@ int BUI_raster_present_native(BuiRaster *r, void *window_handle, int full) (void)full; return 0; } - -static void layer_free(BuiRaster *r) -{ - (void)r; -} - -int BUI_raster_present_layered(BuiRaster *r, void *window_handle, int opacity, int full) -{ - (void)r; - (void)window_handle; - (void)opacity; - (void)full; - return 0; -} #endif /** \} */ diff --git a/kit/include/BUI_kit.h b/kit/include/BUI_kit.h index 3613bf3..3e6c4b6 100644 --- a/kit/include/BUI_kit.h +++ b/kit/include/BUI_kit.h @@ -701,9 +701,11 @@ 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". - * "transparent" (a): per-pixel alpha, no region background, software renderer. On Linux set it - * before show (X11 makes the window again). On Windows the window has no frame (the raster is - * shown with UpdateLayeredWindow); on macOS it is a non-opaque window with a clear background. + * "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 and is made again: with the Direct3D 11 renderer it goes through + * DirectComposition, else the raster is shown with UpdateLayeredWindow. 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, diff --git a/kit/intern/kit_window_win32.c b/kit/intern/kit_window_win32.c index 5e3c738..b58f9b9 100644 --- a/kit/intern/kit_window_win32.c +++ b/kit/intern/kit_window_win32.c @@ -32,6 +32,14 @@ # define ID_WAKE 3 /* BK_TIMER_CUSTOM: custom nodes' frames */ # define ID_GPU 4 /* checks the GPU device until it is ready */ # define ID_USER_BASE 1000 +# ifndef WS_EX_NOREDIRECTIONBITMAP +# define WS_EX_NOREDIRECTIONBITMAP 0x00200000L /* Windows 8 */ +# endif + +/* How a transparent window shows its frame: UpdateLayeredWindow copies the software raster + * (every frame, on the CPU); LAYER_GPU hands the Direct3D 11 frames to DirectComposition + * (WS_EX_NOREDIRECTIONBITMAP, no copy), and falls back to LAYER_ULW without the device. */ +enum { LAYER_NONE = 0, LAYER_ULW = 1, LAYER_GPU = 2 }; struct BKWindow { HWND hwnd; @@ -48,8 +56,14 @@ struct BKWindow { 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": per-pixel alpha (UpdateLayeredWindow) */ + 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 */ }; @@ -368,10 +382,87 @@ static void ime_sync(BKWindow *win) } /* 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) { - BUI_raster_present_layered(win->raster, win->hwnd, win->opacity, full); + if (win->layered == LAYER_ULW) { + present_layered(win, full); } else { BUI_raster_present_native(win->raster, win->hwnd, full); @@ -380,6 +471,7 @@ static void present(BKWindow *win, int 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); @@ -734,6 +826,7 @@ static LRESULT CALLBACK wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) win->full = 1; host_request(win); } + gpu_window_check(win); } else if (wp >= ID_USER_BASE) { KillTimer(hwnd, wp); @@ -834,6 +927,7 @@ 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); @@ -901,7 +995,7 @@ BuiRaster *BK_window_raster(BKWindow *win) void BK_window_set_gpu(BKWindow *win, int on) { - if (win && on && !win->layered) { /* per-pixel alpha needs the software raster */ + if (win && on && win->layered != LAYER_ULW) { /* that way of showing alpha is software */ gpu_attach(win); host_request(win); } @@ -980,30 +1074,84 @@ static void set_opacity(BKWindow *win) SetLayeredWindowAttributes(win->hwnd, 0, win->opacity, LWA_ALPHA); } -/* "transparent": per-pixel alpha. The raster keeps alpha and UpdateLayeredWindow shows it, so - * the window has no frame and no GPU renderer (Direct3D 11 draws opaque frames). */ -static void set_transparent(BKWindow *win, int on) +/* The same window made again with other extended styles (WS_EX_NOREDIRECTIONBITMAP and + * WS_EX_LAYERED cannot be changed on a window that exists). */ +static int reopen(BKWindow *win, LONG_PTR ex) { - if (on == win->layered) { + 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; + DestroyWindow(old); + if (style & WS_VISIBLE) { + ShowWindow(h, SW_SHOW); + } + return 1; +} + +/* "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 LONG_PTR ex = GetWindowLongPtrW(win->hwnd, GWL_EXSTYLE) & ~WS_EX_LAYERED; - win->layered = on; - BUI_raster_set_alpha(win->raster, on); - /* UpdateLayeredWindow fails once SetLayeredWindowAttributes ran, until the style is set again */ - SetWindowLongPtrW(win->hwnd, GWL_EXSTYLE, ex); - if (on) { - BUI_raster_gpu_release(win->raster); - SetWindowLongPtrW(win->hwnd, GWL_EXSTYLE, ex | WS_EX_LAYERED); + const int gpu = BUI_raster_is_gpu(win->raster) || BUI_raster_gpu_pending(win->raster); + const LONG_PTR style_bit = mode == LAYER_GPU ? WS_EX_NOREDIRECTIONBITMAP : + mode == LAYER_ULW ? WS_EX_LAYERED : 0; + const LONG_PTR ex = (GetWindowLongPtrW(win->hwnd, GWL_EXSTYLE) & + ~(LONG_PTR)(WS_EX_LAYERED | WS_EX_NOREDIRECTIONBITMAP)) | style_bit; + BUI_raster_gpu_release(win->raster); /* the device holds the swap chain of the old window */ + if (!reopen(win, ex)) { + if (gpu) { + gpu_attach(win); + } + return; } - else if (win->opacity != 255) { - set_opacity(win); + if (win->layered == LAYER_ULW) { + layer_free(win); } + win->layered = mode; + BUI_raster_set_alpha(win->raster, mode != LAYER_NONE); set_frame(win); + 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; @@ -1035,11 +1183,11 @@ void BK_window_wm(BKWindow *win, const char *key, double a, double b) } else if (!strcmp(key, "alpha")) { win->opacity = (BYTE)(BK_CLAMP(a, 0.0, 1.0) * 255.0 + 0.5); - if (win->layered) { /* the next present scales the frame */ + if (win->layered == LAYER_ULW) { /* the next present scales the frame */ win->full = 1; host_request(win); } - else { + else if (win->layered == LAYER_NONE) { /* DirectComposition frames are not scaled */ set_opacity(win); } } diff --git a/website/src/content/docs/c/c-api.mdx b/website/src/content/docs/c/c-api.mdx index 8a56892..f1febce 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`, `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, a non-opaque window on macOS; software rendering, and 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. +- **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, DirectComposition 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. From d4789767f282d1e113daac1b4baad05ad96c4e9c Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 15:26:23 +0530 Subject: [PATCH 36/51] docs: transparent window options on every platform --- bindings/node/index.d.ts | 2 +- bindings/rust/blenderui/src/window.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index bb18d74..e5446d4 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -18,7 +18,7 @@ export interface WindowOptions { height?: number resizable?: boolean gpu?: boolean - /** Per-pixel alpha: only the elements show (Linux; CPU renderer). */ + /** Per-pixel alpha: only the elements show (no frame on Windows). */ transparent?: boolean } diff --git a/bindings/rust/blenderui/src/window.rs b/bindings/rust/blenderui/src/window.rs index a795945..b75d5e7 100644 --- a/bindings/rust/blenderui/src/window.rs +++ b/bindings/rust/blenderui/src/window.rs @@ -208,8 +208,7 @@ pub enum WindowBounds { pub enum WindowBackgroundAppearance { #[default] Opaque, - /// Per-pixel alpha: only the elements show. Linux (X11, Wayland) only; the window - /// draws on the software raster. + /// Per-pixel alpha: only the elements show. On Windows the window has no frame. Transparent, } From 1b84b7c147e583a79b26503ada952030c60993a0 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 15:37:06 +0530 Subject: [PATCH 37/51] d3d11: declare the DirectComposition interfaces for C --- gpu/intern/gpu_d3d11.c | 51 +++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/gpu/intern/gpu_d3d11.c b/gpu/intern/gpu_d3d11.c index 4a78bb8..10d81c8 100644 --- a/gpu/intern/gpu_d3d11.c +++ b/gpu/intern/gpu_d3d11.c @@ -24,7 +24,6 @@ # include # include # include -# include # include # include @@ -46,6 +45,38 @@ static const GUID k_IID_ID3D11DeviceContext1 = { static const GUID k_IID_IDCompositionDevice = { 0xc37ea93a, 0xe7aa, 0x450d, {0xb1, 0x6f, 0x97, 0x46, 0xcb, 0x04, 0x07, 0xf3}}; +/* dcomp.h declares overloaded methods, so C cannot parse it. The three interfaces used here + * are declared by their vtable order (dcomp.idl in Wine); `void *` fills the unused slots. */ +typedef struct DCompVisual DCompVisual; +typedef struct DCompTarget DCompTarget; +typedef struct DCompDevice DCompDevice; +# define DCOMP_COM(self) \ + HRESULT(STDMETHODCALLTYPE *QueryInterface)(self *, REFIID, void **); \ + ULONG(STDMETHODCALLTYPE *AddRef)(self *); \ + ULONG(STDMETHODCALLTYPE *Release)(self *) +struct DCompVisual { + const struct { + DCOMP_COM(DCompVisual); + void *offset_clip[12]; /* SetOffsetX (animation) ... SetClip */ + HRESULT(STDMETHODCALLTYPE *SetContent)(DCompVisual *, IUnknown *); + } *lpVtbl; +}; +struct DCompTarget { + const struct { + DCOMP_COM(DCompTarget); + HRESULT(STDMETHODCALLTYPE *SetRoot)(DCompTarget *, DCompVisual *); + } *lpVtbl; +}; +struct DCompDevice { + const struct { + DCOMP_COM(DCompDevice); + HRESULT(STDMETHODCALLTYPE *Commit)(DCompDevice *); + void *wait_and_statistics[2]; + HRESULT(STDMETHODCALLTYPE *CreateTargetForHwnd)(DCompDevice *, HWND, BOOL, DCompTarget **); + HRESULT(STDMETHODCALLTYPE *CreateVisual)(DCompDevice *, DCompVisual **); + } *lpVtbl; +}; + # define REL(p) \ do { \ if (p) { \ @@ -148,9 +179,9 @@ struct BuiGpu { * ClearView clears the rects of a redrawn region. */ int alpha; ID3D11DeviceContext1 *ctx1; - IDCompositionDevice *dcomp; - IDCompositionTarget *dtarget; - IDCompositionVisual *dvisual; + DCompDevice *dcomp; + DCompTarget *dtarget; + DCompVisual *dvisual; int swap_fresh; ID3D11VertexShader *vs_quad, *vs_tri, *vs_mesh; @@ -1582,12 +1613,12 @@ static int dcomp_attach(BuiGpu *g) const int ok = create && SUCCEEDED(ID3D11Device_QueryInterface(g->dev, &k_IID_IDXGIDevice, (void **)&xd)) && SUCCEEDED(create(xd, &k_IID_IDCompositionDevice, (void **)&g->dcomp)) && - SUCCEEDED(IDCompositionDevice_CreateTargetForHwnd(g->dcomp, g->hwnd, TRUE, - &g->dtarget)) && - SUCCEEDED(IDCompositionDevice_CreateVisual(g->dcomp, &g->dvisual)) && - SUCCEEDED(IDCompositionVisual_SetContent(g->dvisual, (IUnknown *)g->swap)) && - SUCCEEDED(IDCompositionTarget_SetRoot(g->dtarget, (IDCompositionVisual *)g->dvisual)) && - SUCCEEDED(IDCompositionDevice_Commit(g->dcomp)); /* once: Present needs no commit */ + SUCCEEDED(g->dcomp->lpVtbl->CreateTargetForHwnd(g->dcomp, g->hwnd, TRUE, + &g->dtarget)) && + SUCCEEDED(g->dcomp->lpVtbl->CreateVisual(g->dcomp, &g->dvisual)) && + SUCCEEDED(g->dvisual->lpVtbl->SetContent(g->dvisual, (IUnknown *)g->swap)) && + SUCCEEDED(g->dtarget->lpVtbl->SetRoot(g->dtarget, g->dvisual)) && + SUCCEEDED(g->dcomp->lpVtbl->Commit(g->dcomp)); /* once: Present needs no commit */ REL(xd); return ok; } From 5cea7a6182bfe4eb592e8157edd7c15bbf9e9932 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 15:43:35 +0530 Subject: [PATCH 38/51] win32: transparent GPU windows through DWM blur-behind --- CHANGELOG.md | 7 +- gpu/intern/gpu_d3d11.c | 105 +++++---------------------- kit/include/BUI_kit.h | 4 +- kit/intern/kit_window_win32.c | 62 ++++++++++++---- website/src/content/docs/c/c-api.mdx | 2 +- 5 files changed, 71 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f08d8b5..19c4d00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,10 @@ All notable changes to **blenderui** (BUI). The format follows 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 is made with `WS_EX_NOREDIRECTIONBITMAP` and shown by DirectComposition through a - premultiplied composition swap chain (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 `DXGI_ALPHA_MODE_IGNORE`. + 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 diff --git a/gpu/intern/gpu_d3d11.c b/gpu/intern/gpu_d3d11.c index 10d81c8..a933678 100644 --- a/gpu/intern/gpu_d3d11.c +++ b/gpu/intern/gpu_d3d11.c @@ -42,40 +42,6 @@ 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}}; -static const GUID k_IID_IDCompositionDevice = { - 0xc37ea93a, 0xe7aa, 0x450d, {0xb1, 0x6f, 0x97, 0x46, 0xcb, 0x04, 0x07, 0xf3}}; - -/* dcomp.h declares overloaded methods, so C cannot parse it. The three interfaces used here - * are declared by their vtable order (dcomp.idl in Wine); `void *` fills the unused slots. */ -typedef struct DCompVisual DCompVisual; -typedef struct DCompTarget DCompTarget; -typedef struct DCompDevice DCompDevice; -# define DCOMP_COM(self) \ - HRESULT(STDMETHODCALLTYPE *QueryInterface)(self *, REFIID, void **); \ - ULONG(STDMETHODCALLTYPE *AddRef)(self *); \ - ULONG(STDMETHODCALLTYPE *Release)(self *) -struct DCompVisual { - const struct { - DCOMP_COM(DCompVisual); - void *offset_clip[12]; /* SetOffsetX (animation) ... SetClip */ - HRESULT(STDMETHODCALLTYPE *SetContent)(DCompVisual *, IUnknown *); - } *lpVtbl; -}; -struct DCompTarget { - const struct { - DCOMP_COM(DCompTarget); - HRESULT(STDMETHODCALLTYPE *SetRoot)(DCompTarget *, DCompVisual *); - } *lpVtbl; -}; -struct DCompDevice { - const struct { - DCOMP_COM(DCompDevice); - HRESULT(STDMETHODCALLTYPE *Commit)(DCompDevice *); - void *wait_and_statistics[2]; - HRESULT(STDMETHODCALLTYPE *CreateTargetForHwnd)(DCompDevice *, HWND, BOOL, DCompTarget **); - HRESULT(STDMETHODCALLTYPE *CreateVisual)(DCompDevice *, DCompVisual **); - } *lpVtbl; -}; # define REL(p) \ do { \ @@ -174,14 +140,10 @@ struct BuiGpu { IDXGIFactory2 *factory; IDXGISwapChain1 *swap; int sw, sh; /* swap chain size */ - /* alpha: frames keep premultiplied alpha. The swap chain then goes to the window through - * DirectComposition (no redirection surface: the window has WS_EX_NOREDIRECTIONBITMAP) and - * ClearView clears the rects of a redrawn region. */ + /* 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; - DCompDevice *dcomp; - DCompTarget *dtarget; - DCompVisual *dvisual; int swap_fresh; ID3D11VertexShader *vs_quad, *vs_tri, *vs_mesh; @@ -352,7 +314,7 @@ static int device_init(BuiGpu *g) if (g->alpha && FAILED(hr = ID3D11DeviceContext_QueryInterface(g->ctx, &k_IID_ID3D11DeviceContext1, (void **)&g->ctx1))) { - return fail("ID3D11DeviceContext1", hr); /* Windows 8: ClearView, DirectComposition */ + 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 */ @@ -572,9 +534,6 @@ void bui_gpu_free(BuiGpu *g) REL(g->vs_tri); REL(g->ps_quad); REL(g->ps_tri); - REL(g->dvisual); - REL(g->dtarget); - REL(g->dcomp); REL(g->swap); REL(g->factory); REL(g->ctx1); @@ -1600,29 +1559,6 @@ static void flush(BuiGpu *g) g->n_b = g->n_inst = g->n_tv = g->n_mv = g->n_mi = 0; } -/* Shows the swap chain through DirectComposition: the window has no redirection surface, so - * the compositor takes the premultiplied buffers directly (no copy, GPU blending). */ -static int dcomp_attach(BuiGpu *g) -{ - typedef HRESULT(WINAPI * CreateDeviceFn)(IDXGIDevice *, REFIID, void **); - HMODULE dll = LoadLibraryW(L"dcomp.dll"); - CreateDeviceFn create = dll ? (CreateDeviceFn)(void *)GetProcAddress(dll, - "DCompositionCreateDevice") : - NULL; - IDXGIDevice *xd = NULL; - const int ok = create && - SUCCEEDED(ID3D11Device_QueryInterface(g->dev, &k_IID_IDXGIDevice, (void **)&xd)) && - SUCCEEDED(create(xd, &k_IID_IDCompositionDevice, (void **)&g->dcomp)) && - SUCCEEDED(g->dcomp->lpVtbl->CreateTargetForHwnd(g->dcomp, g->hwnd, TRUE, - &g->dtarget)) && - SUCCEEDED(g->dcomp->lpVtbl->CreateVisual(g->dcomp, &g->dvisual)) && - SUCCEEDED(g->dvisual->lpVtbl->SetContent(g->dvisual, (IUnknown *)g->swap)) && - SUCCEEDED(g->dtarget->lpVtbl->SetRoot(g->dtarget, g->dvisual)) && - SUCCEEDED(g->dcomp->lpVtbl->Commit(g->dcomp)); /* once: Present needs no commit */ - REL(xd); - return ok; -} - static int ensure_swap(BuiGpu *g) { if (g->swap && g->sw == g->w && g->sh == g->h) { @@ -1644,30 +1580,23 @@ static int ensure_swap(BuiGpu *g) sd.SampleDesc.Count = 1; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.BufferCount = 2; - sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; - /* only a transparent window pays for premultiplied alpha: the compositor blends it */ - sd.Scaling = g->alpha ? DXGI_SCALING_STRETCH : DXGI_SCALING_NONE; - sd.AlphaMode = g->alpha ? DXGI_ALPHA_MODE_PREMULTIPLIED : DXGI_ALPHA_MODE_IGNORE; if (g->alpha) { - if (FAILED(IDXGIFactory2_CreateSwapChainForComposition(g->factory, (IUnknown *)g->dev, &sd, - NULL, &g->swap)) || - !dcomp_attach(g)) - { - REL(g->dvisual); - REL(g->dtarget); - REL(g->dcomp); - REL(g->swap); - return 0; - } + /* 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 { - if (FAILED(IDXGIFactory2_CreateSwapChainForHwnd(g->factory, (IUnknown *)g->dev, g->hwnd, &sd, - NULL, NULL, &g->swap))) - { - return 0; - } - IDXGIFactory2_MakeWindowAssociation(g->factory, g->hwnd, DXGI_MWA_NO_ALT_ENTER); + 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))) + { + return 0; } + IDXGIFactory2_MakeWindowAssociation(g->factory, g->hwnd, DXGI_MWA_NO_ALT_ENTER); } g->sw = g->w; g->sh = g->h; @@ -1697,7 +1626,7 @@ int bui_gpu_present(BuiGpu *g, int full) 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}; diff --git a/kit/include/BUI_kit.h b/kit/include/BUI_kit.h index 3e6c4b6..c55c77f 100644 --- a/kit/include/BUI_kit.h +++ b/kit/include/BUI_kit.h @@ -703,8 +703,8 @@ void *BK_window_handle(BKWindow *win); /* HWND / X11 Window / NSWindow * * * (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 and is made again: with the Direct3D 11 renderer it goes through - * DirectComposition, else the raster is shown with UpdateLayeredWindow. On macOS it is a + * 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, diff --git a/kit/intern/kit_window_win32.c b/kit/intern/kit_window_win32.c index b58f9b9..e6b1347 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" @@ -32,13 +33,10 @@ # define ID_WAKE 3 /* BK_TIMER_CUSTOM: custom nodes' frames */ # define ID_GPU 4 /* checks the GPU device until it is ready */ # define ID_USER_BASE 1000 -# ifndef WS_EX_NOREDIRECTIONBITMAP -# define WS_EX_NOREDIRECTIONBITMAP 0x00200000L /* Windows 8 */ -# endif /* How a transparent window shows its frame: UpdateLayeredWindow copies the software raster - * (every frame, on the CPU); LAYER_GPU hands the Direct3D 11 frames to DirectComposition - * (WS_EX_NOREDIRECTIONBITMAP, no copy), and falls back to LAYER_ULW without the device. */ + * (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 { @@ -312,6 +310,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) @@ -320,8 +329,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(); @@ -609,6 +617,11 @@ static LRESULT CALLBACK wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) 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); @@ -1074,8 +1087,8 @@ static void set_opacity(BKWindow *win) SetLayeredWindowAttributes(win->hwnd, 0, win->opacity, LWA_ALPHA); } -/* The same window made again with other extended styles (WS_EX_NOREDIRECTIONBITMAP and - * WS_EX_LAYERED cannot be changed on a window that exists). */ +/* 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; @@ -1100,6 +1113,20 @@ static int reopen(BKWindow *win, LONG_PTR ex) 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) @@ -1108,12 +1135,11 @@ static void set_layering(BKWindow *win, int mode) return; } const int gpu = BUI_raster_is_gpu(win->raster) || BUI_raster_gpu_pending(win->raster); - const LONG_PTR style_bit = mode == LAYER_GPU ? WS_EX_NOREDIRECTIONBITMAP : - mode == LAYER_ULW ? WS_EX_LAYERED : 0; - const LONG_PTR ex = (GetWindowLongPtrW(win->hwnd, GWL_EXSTYLE) & - ~(LONG_PTR)(WS_EX_LAYERED | WS_EX_NOREDIRECTIONBITMAP)) | style_bit; + 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 (!reopen(win, ex)) { + if (ex != old_ex && !reopen(win, ex)) { if (gpu) { gpu_attach(win); } @@ -1122,9 +1148,15 @@ static void set_layering(BKWindow *win, int mode) 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); } @@ -1187,7 +1219,7 @@ void BK_window_wm(BKWindow *win, const char *key, double a, double b) win->full = 1; host_request(win); } - else if (win->layered == LAYER_NONE) { /* DirectComposition frames are not scaled */ + else if (win->layered == LAYER_NONE) { /* GPU frames with alpha are not scaled */ set_opacity(win); } } diff --git a/website/src/content/docs/c/c-api.mdx b/website/src/content/docs/c/c-api.mdx index f1febce..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`, `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, DirectComposition 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. +- **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. From 84170aff716214b7df6e6faa909a15ab4c2721cd Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 15:49:43 +0530 Subject: [PATCH 39/51] gpu: test for alpha once per readback --- gpu/intern/gpu_d3d11.c | 5 +++-- gpu/intern/gpu_gl.c | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gpu/intern/gpu_d3d11.c b/gpu/intern/gpu_d3d11.c index a933678..c4c47ef 100644 --- a/gpu/intern/gpu_d3d11.c +++ b/gpu/intern/gpu_d3d11.c @@ -1673,11 +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++) { - /* like the raster: 0x00RRGGBB, or premultiplied 0xAARRGGBB with alpha */ - dst[(size_t)y * w + x] = g->alpha ? src[x] : src[x] & 0xFFFFFFu; + dst[(size_t)y * w + x] = src[x] & mask; } } ID3D11DeviceContext_Unmap(g->ctx, (ID3D11Resource *)st, 0); diff --git a/gpu/intern/gpu_gl.c b/gpu/intern/gpu_gl.c index d0a4811..09cb821 100644 --- a/gpu/intern/gpu_gl.c +++ b/gpu/intern/gpu_gl.c @@ -2614,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 && !g->alpha; 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; From 7af1a95fc9936f577e1246b0188cdd699a9552be Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 17:29:08 +0530 Subject: [PATCH 40/51] rust: window kind, decorations and layer shell options --- bindings/rust/blenderui/src/lib.rs | 3 + bindings/rust/blenderui/src/window.rs | 7 ++ bindings/rust/blenderui/src/window_kind.rs | 98 ++++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 bindings/rust/blenderui/src/window_kind.rs 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 b75d5e7..0c3179f 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; @@ -228,6 +229,8 @@ pub struct WindowOptions { /// 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`). @@ -465,6 +468,10 @@ impl App { 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) { + let key = CString::new(key).unwrap_or_default(); + sys::BK_window_wm(*n, key.as_ptr(), a, b); + } if options.gpu == Some(true) { sys::BK_window_set_gpu(*n, 1); } diff --git a/bindings/rust/blenderui/src/window_kind.rs b/bindings/rust/blenderui/src/window_kind.rs new file mode 100644 index 0000000..f30eeb0 --- /dev/null +++ b/bindings/rust/blenderui/src/window_kind.rs @@ -0,0 +1,98 @@ +//! Window kinds, decorations and layer-shell options (gpui's `WindowKind`, +//! `WindowDecorations` and `layer_shell`), sent to the host as window requests. + +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(pub 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 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, +} + +type Request = (&'static str, f64, f64); + +impl WindowKind { + /// The window requests (key, a, b) 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(("topmost", 1.0, 0.0)), None), + WindowKind::LayerShell(o) => ( + Some(("layer", f64::from(o.layer as u8), f64::from(o.keyboard_interactivity as u8))), + Some(("anchor", f64::from(o.anchor.0), 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(("override", 1.0, 0.0))].into_iter().flatten() + } +} From 586dd187e1d90c9265e8249cb8b5987bb2ffac0e Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 17:31:12 +0530 Subject: [PATCH 41/51] js: always on top, frame and layer shell window options --- bindings/node/index.d.ts | 18 +++++++ bindings/node/index.js | 29 +++++++++-- bindings/node/test/native.test.js | 9 ++++ bindings/rust/blenderui-jsview/src/lib.rs | 58 +++++++++++++++++++++- bindings/rust/blenderui-node/src/lib.rs | 17 ++++++- bindings/rust/blenderui-qjs/src/main.rs | 33 ++++++++++-- bindings/rust/blenderui-qjs/src/prelude.js | 4 +- 7 files changed, 156 insertions(+), 12 deletions(-) diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index e5446d4..0f61457 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -20,6 +20,24 @@ export interface WindowOptions { 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' | diff --git a/bindings/node/index.js b/bindings/node/index.js index 8fd9976..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 @@ -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, transparent: options.transparent, - }) + app.native.openWindow(id, windowConfig(options)) } /** An element. Capitalized tags are Blender's widgets (`Button`, `NumberSlider` ...). */ 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/rust/blenderui-jsview/src/lib.rs b/bindings/rust/blenderui-jsview/src/lib.rs index bc11ca4..bc1c779 100644 --- a/bindings/rust/blenderui-jsview/src/lib.rs +++ b/bindings/rust/blenderui-jsview/src/lib.rs @@ -51,8 +51,58 @@ pub struct WindowConfig { pub height: Option, pub resizable: Option, pub gpu: Option, - /// Per-pixel alpha, only the elements show (Linux). + /// 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 LayerShellConfig { + fn options(&self) -> LayerShellOptions { + let edge = |name: &str| match name { + "top" => Anchor::TOP, + "bottom" => Anchor::BOTTOM, + "left" => Anchor::LEFT, + "right" => Anchor::RIGHT, + _ => Anchor::default(), + }; + LayerShellOptions { + layer: match self.layer.as_deref() { + Some("background") => Layer::Background, + Some("bottom") => Layer::Bottom, + Some("top") => Layer::Top, + _ => Layer::Overlay, + }, + anchor: self.anchor.iter().flatten().fold(Anchor::default(), |a, e| a | edge(e)), + exclusive_zone: self.exclusive_zone.map(|z| px(z as f32)), + keyboard_interactivity: match self.keyboard.as_deref() { + Some("exclusive") => KeyboardInteractivity::Exclusive, + Some("on-demand") => KeyboardInteractivity::OnDemand, + _ => KeyboardInteractivity::None, + }, + } + } + + /// The window kind: a layer surface wins over `alwaysOnTop`. + fn kind(layer_shell: Option<&LayerShellConfig>, always_on_top: Option) -> WindowKind { + match (layer_shell, always_on_top) { + (Some(l), _) => WindowKind::LayerShell(l.options()), + (None, Some(true)) => WindowKind::Floating, + (None, _) => WindowKind::Normal, + } + } } /// A layout box, logical pixels. @@ -453,6 +503,12 @@ pub fn open_window(cx: &mut App, id: u32, config: WindowConfig, sink: Sink) { } else { WindowBackgroundAppearance::Opaque }, + kind: LayerShellConfig::kind(config.layer_shell.as_ref(), config.always_on_top), + 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 f176b1a..ee93c78 100644 --- a/bindings/rust/blenderui-node/src/lib.rs +++ b/bindings/rust/blenderui-node/src/lib.rs @@ -84,6 +84,18 @@ pub struct WindowConfig { 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)] @@ -223,7 +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, + 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 45a37d9..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,10 +423,9 @@ fn host_object<'js>(ctx: &Ctx<'js>, argv: Vec) -> rquickjs::Result, width: f64, height: f64, resizable: Option, gpu: Option, - transparent: Option| { - command(Command::Open(id, view::WindowConfig { title, width: Some(width), height: Some(height), resizable, gpu, - transparent })); + 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 f0b1599..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, c.transparent ?? 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) } From cc111ce97ac26576232acdecdd88808ff1a00d87 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 17:31:12 +0530 Subject: [PATCH 42/51] rust: test window kind requests --- bindings/rust/blenderui/src/window_kind.rs | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/bindings/rust/blenderui/src/window_kind.rs b/bindings/rust/blenderui/src/window_kind.rs index f30eeb0..0314458 100644 --- a/bindings/rust/blenderui/src/window_kind.rs +++ b/bindings/rust/blenderui/src/window_kind.rs @@ -96,3 +96,42 @@ impl WindowKind { [first, second, frameless.then_some(("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), [("topmost", 1.0, 0.0)]); + assert_eq!( + requests(WindowKind::PopUp, WindowDecorations::Server), + [("topmost", 1.0, 0.0), ("override", 1.0, 0.0)] + ); + assert_eq!(requests(WindowKind::Normal, WindowDecorations::Client), [("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), + [("layer", 2.0, 2.0), ("anchor", 5.0, 24.0)] + ); + } +} From 0a1c91ea2dbd2907244d04ee5aa8c38962a88ee0 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 17:31:50 +0530 Subject: [PATCH 43/51] docs: window kind options in rust and javascript --- CHANGELOG.md | 5 +++++ docs/rust.md | 2 +- website/src/content/docs/javascript/native-api.mdx | 2 +- website/src/content/docs/rust/gpui-syntax.mdx | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c4d00..83bac11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,11 @@ All notable changes to **blenderui** (BUI). The format follows WindowBackgroundAppearance::Transparent` opens a transparent window on Linux. - **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. diff --git a/docs/rust.md b/docs/rust.md index 4967441..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`, `window_background` (`WindowBackgroundAppearance::Transparent`); `App::on_window_activation` reports focus | +| `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/website/src/content/docs/javascript/native-api.mdx b/website/src/content/docs/javascript/native-api.mdx index 4f5b629..e52ddc5 100644 --- a/website/src/content/docs/javascript/native-api.mdx +++ b/website/src/content/docs/javascript/native-api.mdx @@ -61,7 +61,7 @@ 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`, `gpu` (to choose per window) and `transparent` (per-pixel alpha, CPU renderer). `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(event, fn)` for `close`, `focus` and `blur` (the window got or lost the keyboard focus). diff --git a/website/src/content/docs/rust/gpui-syntax.mdx b/website/src/content/docs/rust/gpui-syntax.mdx index 0d20dc6..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`, `gpu` and `window_background` (`WindowBackgroundAppearance::Transparent`) | +| `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 | From 7d32cc37c19ec8f5a474b151d90ff4b7e7824587 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 17:34:28 +0530 Subject: [PATCH 44/51] rust: window kind cleanups from review --- bindings/rust/blenderui-jsview/src/lib.rs | 29 ++++++------- bindings/rust/blenderui/src/window.rs | 1 - bindings/rust/blenderui/src/window_kind.rs | 47 +++++++++++++++++----- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/bindings/rust/blenderui-jsview/src/lib.rs b/bindings/rust/blenderui-jsview/src/lib.rs index bc1c779..2f15924 100644 --- a/bindings/rust/blenderui-jsview/src/lib.rs +++ b/bindings/rust/blenderui-jsview/src/lib.rs @@ -69,8 +69,8 @@ pub struct LayerShellConfig { pub keyboard: Option, } -impl LayerShellConfig { - fn options(&self) -> LayerShellOptions { +impl From<&LayerShellConfig> for LayerShellOptions { + fn from(c: &LayerShellConfig) -> Self { let edge = |name: &str| match name { "top" => Anchor::TOP, "bottom" => Anchor::BOTTOM, @@ -79,29 +79,29 @@ impl LayerShellConfig { _ => Anchor::default(), }; LayerShellOptions { - layer: match self.layer.as_deref() { + layer: match c.layer.as_deref() { Some("background") => Layer::Background, Some("bottom") => Layer::Bottom, Some("top") => Layer::Top, _ => Layer::Overlay, }, - anchor: self.anchor.iter().flatten().fold(Anchor::default(), |a, e| a | edge(e)), - exclusive_zone: self.exclusive_zone.map(|z| px(z as f32)), - keyboard_interactivity: match self.keyboard.as_deref() { + 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 kind(layer_shell: Option<&LayerShellConfig>, always_on_top: Option) -> WindowKind { - match (layer_shell, always_on_top) { - (Some(l), _) => WindowKind::LayerShell(l.options()), - (None, Some(true)) => WindowKind::Floating, - (None, _) => WindowKind::Normal, - } +/// 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, } } @@ -490,6 +490,7 @@ 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)), @@ -503,7 +504,7 @@ pub fn open_window(cx: &mut App, id: u32, config: WindowConfig, sink: Sink) { } else { WindowBackgroundAppearance::Opaque }, - kind: LayerShellConfig::kind(config.layer_shell.as_ref(), config.always_on_top), + kind, window_decorations: if config.frame == Some(false) { WindowDecorations::Client } else { diff --git a/bindings/rust/blenderui/src/window.rs b/bindings/rust/blenderui/src/window.rs index 0c3179f..0795289 100644 --- a/bindings/rust/blenderui/src/window.rs +++ b/bindings/rust/blenderui/src/window.rs @@ -469,7 +469,6 @@ impl App { sys::BK_window_wm(*n, c"transparent".as_ptr(), 1.0, 0.0); } for (key, a, b) in options.kind.requests(options.window_decorations) { - let key = CString::new(key).unwrap_or_default(); sys::BK_window_wm(*n, key.as_ptr(), a, b); } if options.gpu == Some(true) { diff --git a/bindings/rust/blenderui/src/window_kind.rs b/bindings/rust/blenderui/src/window_kind.rs index 0314458..3e8d425 100644 --- a/bindings/rust/blenderui/src/window_kind.rs +++ b/bindings/rust/blenderui/src/window_kind.rs @@ -1,6 +1,7 @@ //! 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; @@ -52,7 +53,7 @@ pub enum KeyboardInteractivity { /// 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(pub u8); +pub struct Anchor(u8); impl Anchor { pub const TOP: Anchor = Anchor(1); @@ -61,6 +62,13 @@ impl Anchor { 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; @@ -79,21 +87,32 @@ pub struct LayerShellOptions { pub keyboard_interactivity: KeyboardInteractivity, } -type Request = (&'static str, f64, f64); +/// 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 (key, a, b) that make a new window of this kind and decoration. + /// 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(("topmost", 1.0, 0.0)), None), + WindowKind::PopUp | WindowKind::Floating => (Some((c"topmost", 1.0, 0.0)), None), WindowKind::LayerShell(o) => ( - Some(("layer", f64::from(o.layer as u8), f64::from(o.keyboard_interactivity as u8))), - Some(("anchor", f64::from(o.anchor.0), f64::from(o.exclusive_zone.map_or(0.0, |z| z.0)))), + 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(("override", 1.0, 0.0))].into_iter().flatten() + [first, second, frameless.then_some((c"override", 1.0, 0.0))] + .into_iter() + .flatten() } } @@ -113,12 +132,18 @@ mod tests { #[test] fn floating_is_topmost_and_pop_up_has_no_frame() { - assert_eq!(requests(WindowKind::Floating, WindowDecorations::Server), [("topmost", 1.0, 0.0)]); + assert_eq!( + requests(WindowKind::Floating, WindowDecorations::Server), + [(c"topmost", 1.0, 0.0)] + ); assert_eq!( requests(WindowKind::PopUp, WindowDecorations::Server), - [("topmost", 1.0, 0.0), ("override", 1.0, 0.0)] + [(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)] ); - assert_eq!(requests(WindowKind::Normal, WindowDecorations::Client), [("override", 1.0, 0.0)]); } #[test] @@ -131,7 +156,7 @@ mod tests { }); assert_eq!( requests(kind, WindowDecorations::Server), - [("layer", 2.0, 2.0), ("anchor", 5.0, 24.0)] + [(c"layer", 2.0, 2.0), (c"anchor", 5.0, 24.0)] ); } } From 5cc2167e43811468d4deb5272de483b342242324 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Mon, 9 Nov 2026 17:43:19 +0530 Subject: [PATCH 45/51] changelog: gpu header changes, transparent windows on every platform --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83bac11..ec8c0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ All notable changes to **blenderui** (BUI). The format follows (`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. + 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 @@ -65,6 +65,9 @@ All notable changes to **blenderui** (BUI). The format follows 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 From 718472bbc11347926670f04b161c0f1d1bfb0467 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Tue, 10 Nov 2026 14:32:32 +0530 Subject: [PATCH 46/51] tests: send tk quit from the run loop --- bindings/python/tests/test_tk_compat.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bindings/python/tests/test_tk_compat.py b/bindings/python/tests/test_tk_compat.py index 2c28362..ccd7c55 100644 --- a/bindings/python/tests/test_tk_compat.py +++ b/bindings/python/tests/test_tk_compat.py @@ -629,7 +629,7 @@ def test_quit_asks_windows_to_close(): objc.objc_getClass.restype = objc.sel_registerName.restype = ctypes.c_void_p def msg(obj, sel, *args): - # PYFUNCTYPE keeps the GIL: Quit runs the Python close handler inside the call + # PYFUNCTYPE keeps the GIL: the Python close handler may run inside the call call = ctypes.PYFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, *[ctypes.c_long] * len(args))(("objc_msgSend", objc)) return call(obj, objc.sel_registerName(sel), *args) @@ -643,7 +643,9 @@ def choose_quit(): for i in range(msg(menu, b"numberOfItems") or 0): key = msg(msg(menu, b"itemAtIndex:", i), b"keyEquivalent") if ctypes.cast(msg(key, b"UTF8String"), ctypes.c_char_p).value == b"q": - msg(menu, b"performActionForItemAtIndex:", i) + # queued on the run loop: BUI refuses a close that arrives inside a Python callback + msg(menu, b"performSelectorOnMainThread:withObject:waitUntilDone:", + objc.sel_registerName(b"performActionForItemAtIndex:"), i, 0) return raise AssertionError("no Quit item in Tk's application menu") root.after(50, choose_quit) From c8dc4e8c8c7a5a2531ec4d3736507f13e5477faa Mon Sep 17 00:00:00 2001 From: nusaturn Date: Tue, 10 Nov 2026 14:50:19 +0530 Subject: [PATCH 47/51] python: answer a close that arrives mid-callback right after it --- bindings/python/src/buipy_kit.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/bindings/python/src/buipy_kit.c b/bindings/python/src/buipy_kit.c index 48e2d76..5f2d82b 100644 --- a/bindings/python/src/buipy_kit.c +++ b/bindings/python/src/buipy_kit.c @@ -2496,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; } From 15c4198732785dd955064c1f2360f9c9c2fac263 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Tue, 10 Nov 2026 14:50:19 +0530 Subject: [PATCH 48/51] tests: quit from the timer callback again, python defers the close --- bindings/python/tests/test_tk_compat.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bindings/python/tests/test_tk_compat.py b/bindings/python/tests/test_tk_compat.py index ccd7c55..2c28362 100644 --- a/bindings/python/tests/test_tk_compat.py +++ b/bindings/python/tests/test_tk_compat.py @@ -629,7 +629,7 @@ def test_quit_asks_windows_to_close(): objc.objc_getClass.restype = objc.sel_registerName.restype = ctypes.c_void_p def msg(obj, sel, *args): - # PYFUNCTYPE keeps the GIL: the Python close handler may run inside the call + # PYFUNCTYPE keeps the GIL: Quit runs the Python close handler inside the call call = ctypes.PYFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, *[ctypes.c_long] * len(args))(("objc_msgSend", objc)) return call(obj, objc.sel_registerName(sel), *args) @@ -643,9 +643,7 @@ def choose_quit(): for i in range(msg(menu, b"numberOfItems") or 0): key = msg(msg(menu, b"itemAtIndex:", i), b"keyEquivalent") if ctypes.cast(msg(key, b"UTF8String"), ctypes.c_char_p).value == b"q": - # queued on the run loop: BUI refuses a close that arrives inside a Python callback - msg(menu, b"performSelectorOnMainThread:withObject:waitUntilDone:", - objc.sel_registerName(b"performActionForItemAtIndex:"), i, 0) + msg(menu, b"performActionForItemAtIndex:", i) return raise AssertionError("no Quit item in Tk's application menu") root.after(50, choose_quit) From 5a4c004471456bc7a7bb346a896f200aa6f876ba Mon Sep 17 00:00:00 2001 From: nusaturn Date: Tue, 10 Nov 2026 20:42:38 +0530 Subject: [PATCH 49/51] gpu: blend skips the alpha byte in opaque frames --- gpu/intern/gpu_raster.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gpu/intern/gpu_raster.c b/gpu/intern/gpu_raster.c index 68e1775..89ec659 100644 --- a/gpu/intern/gpu_raster.c +++ b/gpu/intern/gpu_raster.c @@ -68,13 +68,16 @@ static inline Paint paint_from(const BuiRaster *rs, const float c[4]) return p; } -/* Blend s over d with factor f in 0..256, all four bytes (alpha 0 stays 0). */ +/* 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 ag = (((s >> 8) & 0xFF00FFu) * f + ((d >> 8) & 0xFF00FFu) * nf) & 0xFF00FF00u; - return rb | ag; + 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. */ From c28ac4ef8fc913b22ec891549a6155b21e4d3e33 Mon Sep 17 00:00:00 2001 From: nusaturn Date: Tue, 10 Nov 2026 20:42:38 +0530 Subject: [PATCH 50/51] win32: timers follow the window when it is made again --- kit/intern/kit_window_win32.c | 71 ++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/kit/intern/kit_window_win32.c b/kit/intern/kit_window_win32.c index e6b1347..a6cac6d 100644 --- a/kit/intern/kit_window_win32.c +++ b/kit/intern/kit_window_win32.c @@ -64,10 +64,59 @@ struct BKWindow { } 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) @@ -293,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) @@ -357,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()); } } @@ -822,27 +871,27 @@ 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)); } @@ -943,6 +992,7 @@ void BK_window_free(BKWindow *win) layer_free(win); BUI_raster_free(win->raster); bk_ime_win32_free(&win->ime); + free(win->timers); free(win); } @@ -1036,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; } @@ -1051,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); } } @@ -1106,6 +1156,7 @@ static int reopen(BKWindow *win, LONG_PTR ex) 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); From 7e698bf927d612347b93d2fc6af08eeb8b7ee79e Mon Sep 17 00:00:00 2001 From: nusaturn Date: Tue, 10 Nov 2026 20:52:44 +0530 Subject: [PATCH 51/51] gpu: opaque image and paint paths skip the alpha byte --- gpu/intern/gpu_raster.c | 88 ++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/gpu/intern/gpu_raster.c b/gpu/intern/gpu_raster.c index 89ec659..3234e7b 100644 --- a/gpu/intern/gpu_raster.c +++ b/gpu/intern/gpu_raster.c @@ -57,13 +57,13 @@ static inline float clampf(float v, float lo, float hi) return v < lo ? lo : (v > hi ? hi : v); } -static inline Paint paint_from(const BuiRaster *rs, 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 | rs->alpha; + 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; } @@ -547,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(r, rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -636,7 +636,7 @@ void BUI_raster_round_rect( } return; } - const Paint p = paint_from(r, rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -726,7 +726,7 @@ void BUI_raster_circle( } return; } - const Paint p = paint_from(r, rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -785,7 +785,7 @@ void BUI_raster_line( } return; } - const Paint p = paint_from(r, rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -837,7 +837,7 @@ void BUI_raster_ellipse(BuiRaster *r, } return; } - const Paint p = paint_from(r, rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -934,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(r, top) : (Paint){0, 0}; - const Paint p_out = outline ? paint_from(r, outline) : (Paint){0, 0}; - const Paint p_emb = has_emboss ? paint_from(r, 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; @@ -950,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(r, 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. */ @@ -1015,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; @@ -1055,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(r, p->outline); - const Paint p_emb = paint_from(r, p->emboss); - Paint p_in = paint_from(r, 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; @@ -1073,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(r, c); + p_in = paint_from(ralpha, c); } /* -- middle columns: row-constant coverage -- */ int span_a = ma, span_b = mb; @@ -1141,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(r, gc), ci); + put_cov(row + px, paint_from(ralpha, gc), ci); } Paint pi = p_in; if (gradient && p->shade_horizontal) { @@ -1150,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(r, c); + pi = paint_from(ralpha, c); } put_cov(row + px, pi, ci); } @@ -1161,13 +1162,19 @@ void BUI_raster_widget_base(BuiRaster *r, const BuiWidgetBase *p) } } -/* Premultiplied source over the frame, all four bytes. */ -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 ag = ((d >> 8) & 0xFF00FFu) * ia + 0x800080u; rb = ((rb + ((rb >> 8) & 0xFF00FFu)) >> 8) & 0xFF00FFu; /* /255, rounded */ + 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; } @@ -1190,7 +1197,7 @@ static inline void put_image_px(uint32_t *d, uint32_t c, uint32_t ga, uint32_t a *d = c & (0xFFFFFFu | alpha); } else if (c) { - *d = premul_over(*d, c) & (0xFFFFFFu | alpha); + *d = premul_over(*d, c, alpha); } } @@ -1375,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; @@ -1400,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) | r->alpha; + (uint32_t)clampi((l + bu) >> 16, 0, 255) | ralpha; } px--; } @@ -1409,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, r->alpha); + put_image_px(row + px, yuv_argb(k, yr[tx], c[0], c[1]), ga, ralpha); } } return; @@ -1446,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, r->alpha); + put_image_px(row + px, yuv_argb(k, Y, U, V), ga, ralpha); } } } @@ -1461,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)) @@ -1518,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, r->alpha); + put_image_px(row + px, s[px], ga, ralpha); } } return; @@ -1547,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, r->alpha); + put_image_px(row + px, c, ga, ralpha); } } } @@ -1715,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(r, rgba); + const Paint p = paint_from(r->alpha, rgba); if (p.a <= 0) { return; } @@ -1794,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); @@ -1826,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(r, 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]); @@ -1853,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(r, rgba), 1.0f); + put_cov(row + px, paint_from(ralpha, rgba), 1.0f); } } } @@ -2093,13 +2103,13 @@ 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 frame pixel. */ -static inline void put_straight(const BuiRaster *r, uint32_t *px, const float c[4]) +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(r, c); + Paint p = paint_from(alpha, c); if (p.a >= 256) { *px = p.rgb; } @@ -2138,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}; @@ -2217,7 +2228,7 @@ static void mesh_triangle(BuiRaster *r, continue; } if (flat_all) { - put_straight(r, row + px, flat); + put_straight(ralpha, row + px, flat); continue; } /* barycentric weights of vertices 0, 1, 2 */ @@ -2245,7 +2256,7 @@ static void mesh_triangle(BuiRaster *r, c[k] *= tx[k]; } } - put_straight(r, row + px, c); + put_straight(ralpha, row + px, c); } } } @@ -2315,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, @@ -2345,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(r, rgba), cov); + put_cov(row + px, paint_from(ralpha, rgba), cov); } } } @@ -2360,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; @@ -2389,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(r, c), 1.0f); + put_cov(row + px, paint_from(ralpha, c), 1.0f); } } } @@ -2397,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, @@ -2453,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(r, c), cov); + put_cov(row + px, paint_from(ralpha, c), cov); } } } @@ -2743,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(r, rgba); + const Paint p = paint_from(r->alpha, rgba); if (!e || !e->mask || p.a <= 0) { return; }