Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions backend/aether_ui_backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
41 changes: 39 additions & 2 deletions backend/aether_ui_gtk4.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
100 changes: 93 additions & 7 deletions backend/aether_ui_macos.m
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
41 changes: 39 additions & 2 deletions backend/aether_ui_win32.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
35 changes: 34 additions & 1 deletion ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)" \
Expand Down
Loading
Loading