diff --git a/backend/aether_ui_backend.h b/backend/aether_ui_backend.h index 3b05a349..2d85c5fe 100644 --- a/backend/aether_ui_backend.h +++ b/backend/aether_ui_backend.h @@ -275,6 +275,17 @@ void aether_ui_window_close_impl(int win_handle); /* #95: state a widget's width/height, and read back what it actually got. The setter is what holds a panel at a size across a layout pass; the getter answers from the real allocation, not from the request. */ +/* #102: blit WITHOUT copying — the pixels stay the caller's and must stay + valid until the next canvas_clear. For a surface redrawn every frame the + owning variant's malloc+memcpy of the whole framebuffer dominated the + frame; a caller that cannot promise the lifetime uses the owning one. */ +void aether_ui_canvas_draw_image_borrowed_impl(int canvas_id, double x, double y, + int iw, int ih, + const unsigned char* rgba, int byte_len); +void aether_ui_canvas_draw_image_scaled_borrowed_impl(int canvas_id, double x, double y, + double dw, double dh, int iw, int ih, + const unsigned char* rgba, int byte_len); + void aether_ui_set_width_impl(int handle, int px); void aether_ui_set_height_impl(int handle, int px); int aether_ui_get_width_impl(int handle); diff --git a/backend/aether_ui_gtk4.c b/backend/aether_ui_gtk4.c index 5b76f644..b8739a2f 100644 --- a/backend/aether_ui_gtk4.c +++ b/backend/aether_ui_gtk4.c @@ -4359,7 +4359,10 @@ typedef struct { // DRAW_IMAGE pixel dims double a0, a1; // ARC start/end angle (radians) char* text; // FILL_TEXT string (owned, freed on clear/destroy) - unsigned char* pixels; // DRAW_IMAGE RGBA8888 buffer (owned), iw*ih*4 bytes + unsigned char* pixels; // DRAW_IMAGE RGBA8888 buffer, iw*ih*4 bytes + /* #102: 0 = owned and freed with the command, 1 = borrowed from the + caller until the next canvas_clear. */ + int pixels_borrowed; int iw, ih; // DRAW_IMAGE pixel width/height // Gradient (FILL_LINEAR / FILL_RADIAL): geometry in x,y..(see impls), // plus owned stop arrays. FILL_LINEAR uses (gx1,gy1)→(gx2,gy2); @@ -5621,6 +5624,38 @@ void aether_ui_canvas_draw_image_impl(int canvas_id, double x, double y, // As draw_image, but scale the iw×ih source to a dw×dh destination rect at // (x,y) — for a video/raster frame whose pixel resolution differs from the // region's canvas-px extent. dw/dh <= 0 falls back to a 1:1 native blit. +/* #102: draw WITHOUT copying. The pixels stay the caller's and must remain + * valid until the next canvas_clear on this canvas — the lifetime the + * retained command list already has. That is what a per-frame surface (a 3D + * viewport, a video frame, a game framebuffer) already guarantees: one buffer, + * overwritten in place, canvas cleared and redrawn each frame. The owning + * variant copies the whole framebuffer per call, which measured 61% of the + * frame at 918x659. A caller that cannot promise the lifetime keeps using the + * owning variant. */ +void aether_ui_canvas_draw_image_borrowed_impl(int canvas_id, double x, double y, + int iw, int ih, + const unsigned char* rgba, int byte_len) { + if (iw <= 0 || ih <= 0 || !rgba) return; + if (byte_len < iw * ih * 4) return; + canvas_add_cmd(canvas_id, (CanvasCmd){ + .type = CANVAS_DRAW_IMAGE, .x = x, .y = y, + .pixels = (unsigned char*)rgba, .pixels_borrowed = 1, + .iw = iw, .ih = ih + }); +} + +void aether_ui_canvas_draw_image_scaled_borrowed_impl(int canvas_id, double x, double y, + double dw, double dh, int iw, int ih, + const unsigned char* rgba, int byte_len) { + if (iw <= 0 || ih <= 0 || !rgba) return; + if (byte_len < iw * ih * 4) return; + canvas_add_cmd(canvas_id, (CanvasCmd){ + .type = CANVAS_DRAW_IMAGE, .x = x, .y = y, .w = dw, .h = dh, + .pixels = (unsigned char*)rgba, .pixels_borrowed = 1, + .iw = iw, .ih = ih + }); +} + void aether_ui_canvas_draw_image_scaled_impl(int canvas_id, double x, double y, double dw, double dh, int iw, int ih, const unsigned char* rgba, int byte_len) { @@ -5692,7 +5727,9 @@ void aether_ui_canvas_clear_impl(int canvas_id) { free(c->text); c->text = NULL; } if (c->type == CANVAS_DRAW_IMAGE && c->pixels) { - free(c->pixels); c->pixels = NULL; + /* #102: a borrowed buffer belongs to the caller. */ + if (!c->pixels_borrowed) free(c->pixels); + c->pixels = NULL; } if (c->type == CANVAS_FILL_LINEAR || c->type == CANVAS_FILL_RADIAL) { free(c->stop_off); c->stop_off = NULL; diff --git a/backend/aether_ui_macos.m b/backend/aether_ui_macos.m index 6ef6be44..b8bb6315 100644 --- a/backend/aether_ui_macos.m +++ b/backend/aether_ui_macos.m @@ -2859,9 +2859,28 @@ int aether_ui_state_style_impl(int handle, int state) { void aether_ui_set_edge_insets(int handle, double top, double right, double bottom, double left) { NSView* v = (__bridge NSView*)aether_ui_get_widget(handle); - if (v && [v isKindOfClass:[NSStackView class]]) { - [(NSStackView*)v setEdgeInsets:NSEdgeInsetsMake(top, left, bottom, right)]; + if (!v || ![v isKindOfClass:[NSStackView class]]) return; + NSStackView* sv = (NSStackView*)v; + [sv setEdgeInsets:NSEdgeInsetsMake(top, left, bottom, right)]; + + /* #96: container children are pinned to this stack's own edges rather + * than laid out as ordinary arranged subviews, so NSStackView's insets do + * not reach them. Those pins carry the inset as their constant, and this + * is where they learn a NEW one: styles are normally applied after the + * tree is built, so the constraints already exist when an inset arrives. + * Without this, an inset set through a stylesheet would move the leaf + * children and leave every nested row behind. */ + for (NSLayoutConstraint* c in [sv constraints]) { + NSString* id_ = [c identifier]; + if (!id_) continue; + if ([id_ isEqualToString:@"aeui-inset-lead"]) { + c.constant = left; + } else if ([id_ isEqualToString:@"aeui-inset-trail"] || + [id_ isEqualToString:@"aeui-inset-trail-max"]) { + c.constant = -right; + } } + [sv setNeedsLayout:YES]; } // Does this view carry its own width-to-constant constraint? @@ -4505,7 +4524,13 @@ void aether_ui_image_set_size(int handle, int width, int height) { double w, h; double a0, a1; // ARC start/end angle char* text; // FILL_TEXT string (owned) - unsigned char* pixels; // DRAW_IMAGE RGBA8888 buffer (owned) + unsigned char* pixels; // DRAW_IMAGE RGBA8888 buffer + /* #102: 0 = this command owns `pixels` and frees them with the command; + 1 = they belong to the caller and are only borrowed until the next + canvas_clear. A per-frame viewport hands over the same stable buffer + every frame, and copying 2.4 MB sixty times a second cost more than + reading the frame off the GPU did. */ + int pixels_borrowed; int iw, ih; // DRAW_IMAGE pixel dims double gx1, gy1, gx2, gy2, gr, gfx, gfy; // gradient geometry double grad_line_width; // 0 → fill; >0 → stroke at this width @@ -5617,6 +5642,43 @@ void aether_ui_canvas_draw_image_impl(int canvas_id, double x, double y, // backend only). The command carries the dest extent in w/h and the // executor hands CGContextDrawImage a dest rect of that size; CG scales // natively, same as GTK4's cairo path and win32's StretchBlt. +/* #102: draw WITHOUT copying. The pixels stay the caller's, and must remain + * valid and unchanged until the next canvas_clear on this canvas — the same + * lifetime the retained command list already has. + * + * That is exactly the contract a per-frame surface already satisfies: a 3D + * viewport, a video frame or a game framebuffer owns one buffer, overwrites + * it in place, and clears and redraws the canvas each frame. The owning + * variant above allocated and copied the whole framebuffer on every call, + * which for a 918x659 viewport measured 61% of the frame — more than reading + * the frame back off the GPU, and six times more than rendering it. + * + * A caller that cannot promise that lifetime should keep using the owning + * variant; this is a sharper tool on purpose. */ +void aether_ui_canvas_draw_image_borrowed_impl(int canvas_id, double x, double y, + int iw, int ih, + const unsigned char* rgba, int byte_len) { + if (iw <= 0 || ih <= 0 || !rgba) return; + if (byte_len < iw * ih * 4) return; + canvas_add_cmd(canvas_id, (CanvasCmd){ + .type = CANVAS_DRAW_IMAGE, .x = x, .y = y, + .pixels = (unsigned char*)rgba, .pixels_borrowed = 1, + .iw = iw, .ih = ih + }); +} + +void aether_ui_canvas_draw_image_scaled_borrowed_impl(int canvas_id, double x, double y, + double dw, double dh, int iw, int ih, + const unsigned char* rgba, int byte_len) { + if (iw <= 0 || ih <= 0 || !rgba) return; + if (byte_len < iw * ih * 4) return; + canvas_add_cmd(canvas_id, (CanvasCmd){ + .type = CANVAS_DRAW_IMAGE, .x = x, .y = y, .w = dw, .h = dh, + .pixels = (unsigned char*)rgba, .pixels_borrowed = 1, + .iw = iw, .ih = ih + }); +} + void aether_ui_canvas_draw_image_scaled_impl(int canvas_id, double x, double y, double dw, double dh, int iw, int ih, const unsigned char* rgba, int byte_len) { @@ -5682,7 +5744,9 @@ void aether_ui_canvas_clear_impl(int canvas_id) { free(c->text); c->text = NULL; } if (c->type == CANVAS_DRAW_IMAGE && c->pixels) { - free(c->pixels); c->pixels = NULL; + /* #102: a borrowed buffer belongs to the caller. */ + if (!c->pixels_borrowed) free(c->pixels); + c->pixels = NULL; } if (c->type == CANVAS_FILL_LINEAR || c->type == CANVAS_FILL_RADIAL) { free(c->stop_off); c->stop_off = NULL; @@ -6351,10 +6415,32 @@ void aether_ui_widget_add_child_ctx(void* parent_ctx, int child_handle) { // narrower than the parent; trailing == at high-but-not-required // priority makes it stretch whenever nothing forbids it (which is // what gives the calculator its full-width button rows). - [child.leadingAnchor constraintEqualToAnchor:sv.leadingAnchor].active = YES; - [child.trailingAnchor constraintLessThanOrEqualToAnchor:sv.trailingAnchor].active = YES; + // + // #96: the constants are the parent's edge INSETS. Pinning to + // the bare anchors is what made a container child ignore the + // padding a leaf child gets for free from NSStackView's own + // arranged-subview layout, so a heading sat 12px in and the + // row under it did not — every inspector panel misaligned by + // exactly the padding. Tagged so set_edge_insets can update + // them when styles are applied AFTER the tree is built, which + // is the usual order (`apply_styles` at the end of a block). + NSEdgeInsets pins = [sv edgeInsets]; + NSLayoutConstraint* lead = + [child.leadingAnchor constraintEqualToAnchor:sv.leadingAnchor + constant:pins.left]; + [lead setIdentifier:@"aeui-inset-lead"]; + lead.active = YES; + + NSLayoutConstraint* cap = + [child.trailingAnchor constraintLessThanOrEqualToAnchor:sv.trailingAnchor + constant:-pins.right]; + [cap setIdentifier:@"aeui-inset-trail-max"]; + cap.active = YES; + NSLayoutConstraint* stretch = - [child.trailingAnchor constraintEqualToAnchor:sv.trailingAnchor]; + [child.trailingAnchor constraintEqualToAnchor:sv.trailingAnchor + constant:-pins.right]; + [stretch setIdentifier:@"aeui-inset-trail"]; stretch.priority = NSLayoutPriorityDefaultHigh; stretch.active = YES; } diff --git a/backend/aether_ui_win32.c b/backend/aether_ui_win32.c index a769e379..535f15e5 100644 --- a/backend/aether_ui_win32.c +++ b/backend/aether_ui_win32.c @@ -5854,7 +5854,10 @@ typedef struct { float cr, cg, cb, calpha; int cap, join; // STROKE and gradient STROKE: 0=butt/miter 1=round 2=square/bevel char* text; // FILL_TEXT string (owned) - unsigned char* pixels; // DRAW_IMAGE RGBA8888 buffer (owned) + unsigned char* pixels; // DRAW_IMAGE RGBA8888 buffer + /* #102: 0 = owned and freed with the command, 1 = borrowed from the + caller until the next canvas_clear. */ + int pixels_borrowed; int iw, ih; // DRAW_IMAGE pixel dims // Gradient: linear (gx1,gy1)→(gx2,gy2); radial center (gx1,gy1) r gr. float gx1, gy1, gx2, gy2, gr, gfx, gfy; @@ -6303,6 +6306,38 @@ void aether_ui_canvas_draw_image_impl(int canvas_id, double x, double y, // source-pixel size). Dest extent rides p2/p3 and the executor hands // StretchDIBits a dest rect of that size; GDI scales natively, matching // GTK4's cairo path. +/* #102: draw WITHOUT copying. The pixels stay the caller's and must remain + * valid until the next canvas_clear on this canvas — the lifetime the + * retained command list already has. That is what a per-frame surface (a 3D + * viewport, a video frame, a game framebuffer) already guarantees: one buffer, + * overwritten in place, canvas cleared and redrawn each frame. The owning + * variant copies the whole framebuffer per call, which measured 61% of the + * frame at 918x659. A caller that cannot promise the lifetime keeps using the + * owning variant. */ +void aether_ui_canvas_draw_image_borrowed_impl(int canvas_id, double x, double y, + int iw, int ih, + const unsigned char* rgba, int byte_len) { + if (iw <= 0 || ih <= 0 || !rgba) return; + if (byte_len < iw * ih * 4) return; + CanvasCmd c = {0}; + c.k = CV_DRAW_IMAGE; c.p0 = x; c.p1 = y; + c.pixels = (unsigned char*)rgba; c.pixels_borrowed = 1; + c.iw = iw; c.ih = ih; + canvas_add_cmd(canvas_id, c); +} + +void aether_ui_canvas_draw_image_scaled_borrowed_impl(int canvas_id, double x, double y, + double dw, double dh, int iw, int ih, + const unsigned char* rgba, int byte_len) { + if (iw <= 0 || ih <= 0 || !rgba) return; + if (byte_len < iw * ih * 4) return; + CanvasCmd c = {0}; + c.k = CV_DRAW_IMAGE; c.p0 = x; c.p1 = y; c.p2 = dw; c.p3 = dh; + c.pixels = (unsigned char*)rgba; c.pixels_borrowed = 1; + c.iw = iw; c.ih = ih; + canvas_add_cmd(canvas_id, c); +} + void aether_ui_canvas_draw_image_scaled_impl(int canvas_id, double x, double y, double dw, double dh, int iw, int ih, const unsigned char* rgba, int byte_len) { @@ -6370,7 +6405,9 @@ static void canvas_free_text(int canvas_id) { } if (c->font_family) { free(c->font_family); c->font_family = NULL; } if (c->k == CV_DRAW_IMAGE && c->pixels) { - free(c->pixels); c->pixels = NULL; + /* #102: a borrowed buffer belongs to the caller. */ + if (!c->pixels_borrowed) free(c->pixels); + c->pixels = NULL; } if (c->k == CV_FILL_LINEAR || c->k == CV_FILL_RADIAL) { free(c->stop_off); c->stop_off = NULL; diff --git a/ci.sh b/ci.sh index d00319d9..b47a0f2a 100755 --- a/ci.sh +++ b/ci.sh @@ -65,7 +65,7 @@ fi # ------------------------------------------------------------------------- # All examples that must compile in Phase 1. -EXAMPLES=(disclosure_demo icons_demo pills_demo textpath_demo counter form picker styled system canvas testable calculator context_menu overlay_demo vg_tooltip each_demo rebuild_demo fileicon_demo scrollbg_demo keyhandler_demo imagefill_demo filedrop_demo barfill_demo listbox_demo table_demo transitions_demo split_demo bindings_demo tabs_demo menu rbind_demo typo_demo multiselect_demo dblclick_demo tree_demo tabledeleg_demo weightclamp_demo shortcut_demo polish_demo vlist_demo wshortcut_demo multiwindow_demo timer_demo canvasscroll_demo canvasclip_demo canvasresetclip_demo resizecb_demo quit_demo panelsize_demo groupalpha_demo hoverpaint_demo gradspread_demo placeholder_demo multikey_demo sheet_demo winmenu_demo reorder_demo overlaytr_demo a11y_demo material_demo themes_demo csssem_demo zen_demo states_demo undo_demo roles_demo command_demo clipboard window_title) +EXAMPLES=(disclosure_demo icons_demo pills_demo textpath_demo counter form picker styled system canvas testable calculator context_menu overlay_demo vg_tooltip each_demo rebuild_demo fileicon_demo scrollbg_demo keyhandler_demo imagefill_demo filedrop_demo barfill_demo listbox_demo table_demo transitions_demo split_demo bindings_demo tabs_demo menu rbind_demo typo_demo multiselect_demo dblclick_demo tree_demo tabledeleg_demo weightclamp_demo shortcut_demo polish_demo vlist_demo wshortcut_demo multiwindow_demo timer_demo canvasscroll_demo canvasclip_demo canvasresetclip_demo resizecb_demo quit_demo panelsize_demo insets_demo blitborrow_demo groupalpha_demo hoverpaint_demo gradspread_demo placeholder_demo multikey_demo sheet_demo winmenu_demo reorder_demo overlaytr_demo a11y_demo material_demo themes_demo csssem_demo zen_demo states_demo undo_demo roles_demo command_demo clipboard window_title) # Examples without a test server — Phase 2 smoke-launches each. # calculator and testable are exercised through their HTTP drivers in # Phases 3-4, so they are not smoke-tested here. @@ -976,6 +976,39 @@ if [ "$SPEC_OK" -eq 1 ]; then FAIL=$((FAIL + 1)) fi + # A container child must get the same content box as a leaf child. Both + # read back through get_width, so no driver is needed. The panel is 400 + # wide with 12px side insets: the row must measure 376. Before the fix it + # measured the full 400, hanging 12px outside the padding every leaf + # child respected. + echo "-- Phase 5e23: a container child sits inside the parent's insets --" + run_self_quitting "$(EX_BIN insets_demo)" insets_demo 30 + ins_rc=$? + ins_out=$(cat /tmp/ci_insets_demo.selfquit.log 2>/dev/null) + if [ "$ins_rc" -eq 0 ] && printf '%s' "$ins_out" | grep -q "row_w=376"; then + echo " OK insets_demo: container child inset like a leaf (376)" + else + echo " FAIL insets_demo: rc=$ins_rc" + printf '%s\n' "$ins_out" | grep -a "row_w" | sed 's/^/ /' \ + || echo " (no width line printed)" + FAIL=$((FAIL + 1)) + fi + + # The borrowed blit skips the per-frame copy; it is only worth having if + # it draws the SAME pixels. The demo blits one image both ways and reads + # back two of them, so this asserts the picture, not the speed. + echo "-- Phase 5e24: a borrowed blit draws what the copying blit draws --" + run_self_quitting "$(EX_BIN blitborrow_demo)" blitborrow_demo 30 + blit_rc=$? + blit_out=$(cat /tmp/ci_blitborrow_demo.selfquit.log 2>/dev/null) + if [ "$blit_rc" -eq 0 ] && printf '%s' "$blit_out" | grep -q "^MATCH$"; then + echo " OK blitborrow_demo: borrowed and copied blits agree" + else + echo " FAIL blitborrow_demo: rc=$blit_rc" + printf '%s\n' "$blit_out" | grep -aE "owned|borrow" | sed 's/^/ /' + FAIL=$((FAIL + 1)) + fi + echo "-- Phase 5e19: canvas_reset_clip widens the clip back --" UI_SPEC=canvasresetclip_demo/spec_canvasresetclip_demo \ run_server_test "$(EX_BIN canvasresetclip_demo)" \ diff --git a/examples/blitborrow_demo/.build.ae b/examples/blitborrow_demo/.build.ae new file mode 100644 index 00000000..5f10dba8 --- /dev/null +++ b/examples/blitborrow_demo/.build.ae @@ -0,0 +1,18 @@ +// blitborrow_demo — aether-ui toolkit example, built as its own cached aeb node. +import bldr +import aether +import aether (source, output, no_closure_regen) +import build_support.aetherui (ui_backend) + +main() { + bldr.build() { + root = _get("root") + aether.program() { + source("blitborrow_demo.ae") + output("blitborrow_demo") + no_closure_regen() + ui_backend(root) + } + return 0 + } +} diff --git a/examples/blitborrow_demo/blitborrow_demo.ae b/examples/blitborrow_demo/blitborrow_demo.ae new file mode 100644 index 00000000..455f7dd9 --- /dev/null +++ b/examples/blitborrow_demo/blitborrow_demo.ae @@ -0,0 +1,74 @@ +// #102 — a borrowed blit must draw exactly what the copying blit draws. +// +// canvas_draw_image_ptr allocates and copies the whole image on every call. +// For a surface redrawn every frame that measured 61% of the frame, more than +// reading the frame off the GPU. The borrowed variant keeps the caller's +// buffer instead of copying it. +// +// Speed is worthless if the pixels are wrong, so this asserts the PIXELS: the +// same buffer drawn both ways must read back identically. + +import ui +import ui (root_vstack, text, canvas_create, canvas_clear, canvas_draw_image_ptr, + canvas_draw_image_borrowed_ptr, canvas_read_pixel, app_quit, timer, + timer_cancel, enable_test_server_root) +import std.bytes +import std.os (os_getenv) + +// A 4x4 RGBA image: red, with one green pixel at (1,1). +build_image() -> ptr { + b = bytes.new(4 * 4 * 4) + bytes.set_length(b, 4 * 4 * 4) + i = 0 + while i < 16 { + bytes.set(b, i * 4 + 0, 255) + bytes.set(b, i * 4 + 1, 0) + bytes.set(b, i * 4 + 2, 0) + bytes.set(b, i * 4 + 3, 255) + i = i + 1 + } + // (1,1) is index 5: make it green. + bytes.set(b, 5 * 4 + 0, 0) + bytes.set(b, 5 * 4 + 1, 255) + bytes.set(b, 5 * 4 + 2, 0) + return b +} + +main() { + cv = 0 + main_root = root_vstack(0) { + _t = text("blit") + cv = canvas_create(4, 4) + } + img_b = build_image() + // The _ptr blits take a RAW buffer; a bytes object carries a header, so + // hand over its data pointer, not the object. + img = bytes.data(img_b) + + app = ui.app_create("blit borrow", 120, 120) + if os_getenv("AETHER_UI_TEST_PORT") != null { + enable_test_server_root(9222, main_root) + } + + tid = 0 + tid = timer(120) callback { + timer_cancel(tid) + // Copying blit first. + canvas_clear(cv) + canvas_draw_image_ptr(cv, 0.0, 0.0, 4, 4, img, 64) + owned_00 = canvas_read_pixel(cv, 0, 0, 4, 4) + owned_11 = canvas_read_pixel(cv, 1, 1, 4, 4) + + // Same buffer, borrowed. + canvas_clear(cv) + canvas_draw_image_borrowed_ptr(cv, 0.0, 0.0, 4, 4, img, 64) + borrow_00 = canvas_read_pixel(cv, 0, 0, 4, 4) + borrow_11 = canvas_read_pixel(cv, 1, 1, 4, 4) + + println("owned 00=${owned_00} 11=${owned_11}") + println("borrow 00=${borrow_00} 11=${borrow_11}") + if owned_00 == borrow_00 { if owned_11 == borrow_11 { println("MATCH") } } + app_quit() + } + ui.app_start(app, main_root) +} diff --git a/examples/insets_demo/.build.ae b/examples/insets_demo/.build.ae new file mode 100644 index 00000000..471db153 --- /dev/null +++ b/examples/insets_demo/.build.ae @@ -0,0 +1,18 @@ +// insets_demo — aether-ui toolkit example, built as its own cached aeb node. +import bldr +import aether +import aether (source, output, no_closure_regen) +import build_support.aetherui (ui_backend) + +main() { + bldr.build() { + root = _get("root") + aether.program() { + source("insets_demo.ae") + output("insets_demo") + no_closure_regen() + ui_backend(root) + } + return 0 + } +} diff --git a/examples/insets_demo/insets_demo.ae b/examples/insets_demo/insets_demo.ae new file mode 100644 index 00000000..43129e62 --- /dev/null +++ b/examples/insets_demo/insets_demo.ae @@ -0,0 +1,57 @@ +// #96 — a container child must sit inside the parent's insets, like a leaf does. +// +// A leaf child (text) was laid out inside st_insets; a container child +// (hstack, divider) was pinned to the stack's raw edges and ignored them. A +// heading sat 12px in and the row under it did not, which is every inspector +// panel in the toolkit misaligned by exactly the padding. +// +// The app reports the x of a leaf and of a container child, so the assertion +// is the two numbers agreeing. + +import ui +import ui (root_vstack, vstack, hstack, text, divider, add_css_class, + create_styles, st_insets, st_bg, apply_styles, get_width, app_quit, + timer, timer_cancel, set_text, enable_test_server_root) +import std.os (os_getenv) + +main() { + panel = 0 + leaf = 0 + row = 0 + report = 0 + + main_root = root_vstack(0) { + report = text("insets: pending") + panel = vstack(0) { + leaf = text("leaf child") + row = hstack(8) { + _t = text("inside a container child") + } + _d = divider() + } + } + + sheet = create_styles() + st_insets(sheet, "panel", 10.0, 12.0, 10.0, 12.0) + st_bg(sheet, "panel", 0x21252B) + add_css_class(panel, "panel") + // Applied AFTER the tree is built, which is the usual order and the case + // that needs the constraint constants updated. + apply_styles(0, sheet) + + app = ui.app_create("insets", 400, 300) + if os_getenv("AETHER_UI_TEST_PORT") != null { + enable_test_server_root(9222, main_root) + } + tid = 0 + tid = timer(150) callback { + timer_cancel(tid) + lw = get_width(leaf) + rw = get_width(row) + // The row must get the same content box as the leaf: parent width + // minus both insets. Report both widths; equal means aligned. + println("leaf_w=${lw} row_w=${rw}") + app_quit() + } + ui.app_start(app, main_root) +} diff --git a/ui/module.ae b/ui/module.ae index 9b78a934..3a6e23cb 100644 --- a/ui/module.ae +++ b/ui/module.ae @@ -103,6 +103,7 @@ exports ( canvas_set_clip_rects, canvas_reset_clip, canvas_clear, canvas_redraw, canvas_write_png, canvas_cmd_count, canvas_render_range_rgba, canvas_draw_image_ptr, + canvas_draw_image_borrowed_ptr, canvas_draw_image_scaled_borrowed_ptr, canvas_draw_image_scaled_ptr, canvas_arc, canvas_close_path, canvas_fill, canvas_fill_text, canvas_stroke_text, canvas_group_begin, canvas_group_end, canvas_read_pixel, @@ -485,6 +486,10 @@ extern aether_ui_canvas_draw_image_scaled_impl_ptr(canvas_id: int, x: float, y: dw: float, dh: float, iw: int, ih: int, rgba: ptr, byte_len: int) extern aether_ui_canvas_draw_image_impl_ptr(canvas_id: int, x: float, y: float, iw: int, ih: int, rgba: ptr, byte_len: int) +extern aether_ui_canvas_draw_image_borrowed_impl(canvas_id: int, x: float, y: float, + iw: int, ih: int, rgba: ptr, byte_len: int) +extern aether_ui_canvas_draw_image_scaled_borrowed_impl(canvas_id: int, x: float, y: float, + dw: float, dh: float, iw: int, ih: int, rgba: ptr, byte_len: int) extern aether_ui_canvas_render_range_rgba_impl(canvas_id: int, start: int, end: int, ox: float, oy: float, width: int, height: int, @@ -4749,6 +4754,33 @@ canvas_draw_image_scaled_ptr(cid: int, x: float, y: float, dw: float, dh: float, aether_ui_canvas_draw_image_scaled_impl_ptr(cid, x, y, dw, dh, iw, ih, rgba, byte_len) } +// canvas_draw_image_borrowed_ptr — blit WITHOUT copying (#102). +// +// The owning blits above allocate and copy the whole image on every call. +// For a one-off image that is right and cheap; for a surface redrawn every +// frame it is a malloc plus a full-framebuffer memcpy sixty times a second. +// Measured on a 918x659 viewport it was 61% of the frame, more than reading +// the frame back off the GPU and roughly six times the cost of rendering it. +// +// CONTRACT, and it is a sharp one: the pixels stay YOURS. They must remain +// valid and unchanged until the next canvas_clear on this canvas, because the +// canvas retains the command and reads the buffer when it paints. That is +// exactly what a per-frame surface already does — one buffer, overwritten in +// place, canvas cleared and redrawn each frame. If your buffer can be freed +// or resized before the next clear, use canvas_draw_image_ptr and take the +// copy. +canvas_draw_image_borrowed_ptr(cid: int, x: float, y: float, iw: int, ih: int, + rgba: ptr, byte_len: int) { + aether_ui_canvas_draw_image_borrowed_impl(cid, x, y, iw, ih, rgba, byte_len) +} + +// Scaled twin of canvas_draw_image_borrowed_ptr. Same lifetime contract. +canvas_draw_image_scaled_borrowed_ptr(cid: int, x: float, y: float, + dw: float, dh: float, iw: int, ih: int, + rgba: ptr, byte_len: int) { + aether_ui_canvas_draw_image_scaled_borrowed_impl(cid, x, y, dw, dh, iw, ih, rgba, byte_len) +} + canvas_cmd_count(cid: int) -> int { return aether_ui_canvas_cmd_count_impl(cid) }