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
2 changes: 1 addition & 1 deletion packages/native-sdk/native-sdk.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export type NativeSdkGpuSurfacePresentMode = "none" | "timer";
export type NativeSdkGpuSurfaceAlphaMode = "none" | "opaque" | "premultiplied";
export type NativeSdkGpuSurfaceColorSpace = "none" | "srgb" | "display_p3";
export type NativeSdkGpuSurfaceStatus = "unavailable" | "initializing" | "ready" | "lost";
export type NativeSdkCursor = "arrow" | "pointing_hand" | "text" | "resize_horizontal";
export type NativeSdkCursor = "arrow" | "pointing_hand" | "text" | "resize_horizontal" | "resize_vertical";
export type NativeSdkCanvasFrameProfileRisk = "idle" | "low" | "moderate" | "high";

export interface NativeSdkViewInfo {
Expand Down
1 change: 1 addition & 0 deletions src/platform/macos/appkit_host.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ typedef enum {
NATIVE_SDK_APPKIT_CURSOR_POINTING_HAND = 1,
NATIVE_SDK_APPKIT_CURSOR_TEXT = 2,
NATIVE_SDK_APPKIT_CURSOR_RESIZE_HORIZONTAL = 3,
NATIVE_SDK_APPKIT_CURSOR_RESIZE_VERTICAL = 4,
} native_sdk_appkit_cursor_t;

typedef enum {
Expand Down
1 change: 1 addition & 0 deletions src/platform/macos/appkit_host.m
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ static NSAccessibilityRole NativeSdkAccessibilityRoleForWidgetRole(NSInteger rol
case NATIVE_SDK_APPKIT_CURSOR_POINTING_HAND: return [NSCursor pointingHandCursor];
case NATIVE_SDK_APPKIT_CURSOR_TEXT: return [NSCursor IBeamCursor];
case NATIVE_SDK_APPKIT_CURSOR_RESIZE_HORIZONTAL: return [NSCursor resizeLeftRightCursor];
case NATIVE_SDK_APPKIT_CURSOR_RESIZE_VERTICAL: return [NSCursor resizeUpDownCursor];
case NATIVE_SDK_APPKIT_CURSOR_ARROW:
default:
return [NSCursor arrowCursor];
Expand Down
1 change: 1 addition & 0 deletions src/platform/macos/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2231,6 +2231,7 @@ fn appKitCursor(cursor: platform_mod.Cursor) c_int {
.pointing_hand => 1,
.text => 2,
.resize_horizontal => 3,
.resize_vertical => 4,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/platform/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,7 @@ pub const Cursor = enum {
pointing_hand,
text,
resize_horizontal,
resize_vertical,
};

pub const ViewInfo = struct {
Expand Down
35 changes: 24 additions & 11 deletions src/primitives/canvas/events.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub const WidgetHit = struct {
depth: usize,
index: usize,
state: WidgetState,
/// Relevant for divider cursor/input semantics; horizontal elsewhere.
split_axis: canvas.SplitAxis = .horizontal,
/// Semantic role of the hit widget (kind alone cannot distinguish a
/// link hotspot from plain text, and links want a pointer cursor).
role: WidgetRole = .none,
Expand Down Expand Up @@ -671,11 +673,10 @@ pub fn widgetKeyboardControlIntent(widget: Widget, keyboard: WidgetKeyboardEvent
}
else
null,
// The split divider is the ARIA separator: horizontal arrows
// adjust the parent split's fraction, Home/End jump to the
// clamp edges (the runtime clamps against the panes' min
// widths when it applies the value).
.split_divider => if (widgetSplitDividerKeyboardValue(widget.value, keyboard)) |next_value|
// The split divider is the ARIA separator: arrows along its axis
// adjust the parent split's fraction, Home/End jump to the clamp
// edges (the runtime clamps against the panes' main-axis minimums).
.split_divider => if (widgetSplitDividerKeyboardValueForAxis(widget.value, widget.runtime_flags.split_axis, keyboard)) |next_value|
.{
.kind = .set_value,
.actions = .{
Expand Down Expand Up @@ -825,19 +826,31 @@ pub fn widgetSliderKeyboardValue(current: f32, keyboard: WidgetKeyboardEvent) ?f
return null;
}

/// Fraction steps for the split divider: the slider's step sizes, on the
/// horizontal axis only (the vertical arrows stay free for tree/list
/// focus travel around the divider).
pub fn widgetSplitDividerKeyboardValue(current: f32, keyboard: WidgetKeyboardEvent) ?f32 {
/// Fraction steps for a split divider: the slider's step sizes along the
/// split's axis. Cross-axis arrows stay free for surrounding focus travel.
pub fn widgetSplitDividerKeyboardValueForAxis(current: f32, axis: canvas.SplitAxis, keyboard: WidgetKeyboardEvent) ?f32 {
if (keyboard.phase != .key_down or keyboard.modifiers.hasNavigationModifier()) return null;
const step: f32 = if (keyboard.modifiers.shift) 0.1 else 0.05;
if (std.ascii.eqlIgnoreCase(keyboard.key, "arrowleft")) return current - step;
if (std.ascii.eqlIgnoreCase(keyboard.key, "arrowright")) return current + step;
switch (axis) {
.horizontal => {
if (std.ascii.eqlIgnoreCase(keyboard.key, "arrowleft")) return current - step;
if (std.ascii.eqlIgnoreCase(keyboard.key, "arrowright")) return current + step;
},
.vertical => {
if (std.ascii.eqlIgnoreCase(keyboard.key, "arrowup")) return current - step;
if (std.ascii.eqlIgnoreCase(keyboard.key, "arrowdown")) return current + step;
},
}
if (std.ascii.eqlIgnoreCase(keyboard.key, "home")) return 0;
if (std.ascii.eqlIgnoreCase(keyboard.key, "end")) return 1;
return null;
}

/// Original horizontal helper retained for source compatibility.
pub fn widgetSplitDividerKeyboardValue(current: f32, keyboard: WidgetKeyboardEvent) ?f32 {
return widgetSplitDividerKeyboardValueForAxis(current, .horizontal, keyboard);
}

/// The ARIA tree-row keymap, resolved on the routed keyboard target:
/// - Enter/Space activate (select, plus press when a command is bound).
/// - A key that MOVED focus onto this row (`focus_moved`) selects it —
Expand Down
2 changes: 2 additions & 0 deletions src/primitives/canvas/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ pub const DesignTokens = token_model.DesignTokens;

// Canvas widget model and built-in factories live in `widgets.zig`; root keeps the public API stable.
pub const WidgetKind = widget_model.WidgetKind;
pub const SplitAxis = widget_model.SplitAxis;
pub const WidgetCursor = widget_model.WidgetCursor;
pub const WidgetState = widget_model.WidgetState;
pub const WidgetRuntimeFlags = widget_model.WidgetRuntimeFlags;
Expand Down Expand Up @@ -754,6 +755,7 @@ pub const textInputCaretVisibleScrollOffsetForWidget = widget_runtime.textInputC
pub const intrinsicWidgetSize = widget_runtime.intrinsicWidgetSize;
pub const cursorForWidgetHit = widget_runtime.cursorForWidgetHit;
pub const cursorForWidgetTarget = widget_runtime.cursorForWidgetTarget;
pub const cursorForWidgetTargetOnAxis = widget_runtime.cursorForWidgetTargetOnAxis;
/// Whether the engine hit-tests widgets of this kind (widget_access.zig —
/// the single source of truth the runtime, both markup engines, and the
/// markup validator's element list all derive from). Kind-level only: the
Expand Down
21 changes: 17 additions & 4 deletions src/primitives/canvas/ui.zig
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,10 @@ pub fn Ui(comptime Msg: type) type {
text: []const u8 = "",
placeholder: []const u8 = "",
value: f32 = 0,
/// Axis for a `split`: `.horizontal` preserves the original
/// left/right behavior; `.vertical` stacks top/bottom. The
/// synthesized divider inherits it for input and semantics.
split_axis: canvas.SplitAxis = .horizontal,
/// HORIZONTAL scroll offset for a horizontal-capable
/// `scroll` container (markup `value-x`) — the sideways
/// counterpart of `value`. Follows the same source-wins
Expand Down Expand Up @@ -629,10 +633,14 @@ pub fn Ui(comptime Msg: type) type {
width: f32 = 0,
/// Definite height; same contract as `width`.
height: f32 = 0,
/// Height floor WITHOUT the definite-max side of `height`.
/// Vertical split panes use it to constrain divider travel.
min_height: f32 = 0,
/// Height ceiling WITHOUT the definite-min side of `height`.
max_height: f32 = 0,
/// Width floor WITHOUT the definite-max side of `width`:
/// the widget may grow past it but never shrink below.
/// Split panes use it to constrain the divider drag (the
/// clamp band derives from both panes' floors).
/// Horizontal split panes use it to constrain divider travel.
min_width: f32 = 0,
/// Width ceiling WITHOUT the definite-min side of `width`:
/// the widget fills or hugs normally until this bound, then
Expand Down Expand Up @@ -3776,6 +3784,7 @@ pub fn Ui(comptime Msg: type) type {
.image_id = options.image,
.image_src = options.image_src,
.value = options.value,
.runtime_flags = .{ .split_axis = options.split_axis },
.value_x = options.value_x,
.tree_level = options.tree_level,
.terminal = .{ .pty = options.pty, .scrollback = options.scrollback },
Expand Down Expand Up @@ -3807,14 +3816,17 @@ pub fn Ui(comptime Msg: type) type {
.virtual_anchor_index = options.virtual_anchor_index,
.virtual_anchor_extent = options.virtual_anchor_extent,
.virtual_total_extent = options.virtual_total_extent,
.min_size = .{ .width = @max(options.width, options.min_width), .height = options.height },
.min_size = .{
.width = @max(options.width, options.min_width),
.height = @max(options.height, options.min_height),
},
// Explicit sizes are definite (min AND max). Resizable
// is the exception: width documents the initial width
// and the engine's drag handle keeps writing larger
// frames past it.
.max_size = if (kind == .resizable) .{} else .{
.width = if (options.width > 0) options.width else options.max_width,
.height = options.height,
.height = if (options.height > 0) options.height else options.max_height,
},
},
.style = options.style,
Expand Down Expand Up @@ -3844,6 +3856,7 @@ pub fn Ui(comptime Msg: type) type {
.kind = .split_divider,
.id = structuralId(split_widget.id, .split_divider, UiKey{ .str = "divider" }),
.value = split_widget.value,
.runtime_flags = .{ .split_axis = split_widget.runtime_flags.split_axis },
.state = .{ .disabled = split_widget.state.disabled },
.semantics = .{ .label = "Split divider" },
};
Expand Down
14 changes: 12 additions & 2 deletions src/primitives/canvas/widget_access.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const snapTextRange = text_model.snapTextRange;
pub fn cursorForWidgetHit(hit: ?WidgetHit) WidgetCursor {
const target = hit orelse return .arrow;
if (target.role == .link and !target.state.disabled) return .pointing_hand;
return cursorForWidgetTarget(target.kind, target.state);
return cursorForWidgetTargetOnAxis(target.kind, target.state, target.split_axis);
}

/// The kind-level half of the register: I-beam over editable text (a
Expand All @@ -31,14 +31,24 @@ pub fn cursorForWidgetHit(hit: ?WidgetHit) WidgetCursor {
/// arrow over everything else — including sliders, which keep the arrow
/// at rest AND during a drag on every native platform. The pointing hand
/// never comes from a kind; it is role-driven (`cursorForWidgetHit`).
///
/// This original entry point preserves horizontal divider behavior.
pub fn cursorForWidgetTarget(kind: WidgetKind, state: WidgetState) WidgetCursor {
return cursorForWidgetTargetOnAxis(kind, state, .horizontal);
}

pub fn cursorForWidgetTargetOnAxis(kind: WidgetKind, state: WidgetState, split_axis: widget_model.SplitAxis) WidgetCursor {
if (state.disabled) return .arrow;
return switch (kind) {
// The terminal joins the editable-text register: a click
// focuses the session's input, and the I-beam advertises it —
// the platform terminals' own convention.
.input, .text_field, .search_field, .combobox, .textarea, .terminal => .text,
.resizable, .split_divider => .resize_horizontal,
.resizable => .resize_horizontal,
.split_divider => switch (split_axis) {
.horizontal => .resize_horizontal,
.vertical => .resize_vertical,
},
else => .arrow,
};
}
Expand Down
1 change: 1 addition & 0 deletions src/primitives/canvas/widget_invalidation.zig
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ fn widgetChange(
const next_diff_lines = next.widget.codeDiffLines();
const layout_dirty =
previous.widget.kind != next.widget.kind or
previous.widget.runtime_flags.split_axis != next.widget.runtime_flags.split_axis or
previous.depth != next.depth or
previous.parent_index != next.parent_index or
root_bounds_dirty or
Expand Down
Loading
Loading