const std = @import("std"); pub const ActivityPlacement = union(enum) { const Transient = struct { row: u16, row_count: u16 = 1, gap_above_rows: u16, footer_clearance_rows: u16 = 0, tool_row: ?u16 = null, }; none, transient_row: Transient, overlay_entry: u16, pub fn reservedFooterRows(self: ActivityPlacement) u16 { return switch (self) { .transient_row => |activity| activity.footer_clearance_rows, .overlay_entry, .none => 0, }; } pub fn reservesFooterRow(self: ActivityPlacement) bool { return self.reservedFooterRows() > 0; } pub fn reservedTopRow(self: ActivityPlacement) ?u16 { return switch (self) { .transient_row => |activity| { const first_row = activity.tool_row orelse activity.row; if (first_row <= activity.gap_above_rows) return 1; return first_row - activity.gap_above_rows; }, .overlay_entry, .none => null, }; } pub fn transcriptReservationRows(self: ActivityPlacement) u16 { return switch (self) { .transient_row => |activity| activity.gap_above_rows +| (activity.row -| (activity.tool_row orelse activity.row)) +| activity.row_count +| activity.footer_clearance_rows, .overlay_entry, .none => 0, }; } pub fn requiredFooterTop(self: ActivityPlacement) ?u16 { return switch (self) { .transient_row => |activity| activity.row +| activity.row_count +| activity.footer_clearance_rows, .overlay_entry, .none => null, }; } pub fn row(self: ActivityPlacement) ?u16 { return switch (self) { .transient_row => |activity| activity.row, .overlay_entry => |r| r, .none => null, }; } pub fn primaryRowCount(self: ActivityPlacement) u16 { return switch (self) { .transient_row => |activity| activity.row_count, .overlay_entry => 1, .none => 0, }; } pub fn toolRow(self: ActivityPlacement) ?u16 { return switch (self) { .transient_row => |activity| activity.tool_row, .overlay_entry, .none => null, }; }}; pub const Input = struct { active_label_present: bool, activity_rows: u16 = 1, tool_before_activity: bool = false, cursor_row: u16, cursor_col: u16, transient_gap_rows: u16, place_mid_line_active: bool, entry_bound_active: bool, stream_active: bool, footer_top_for_extra: u16, footer_clearance_rows: u16 = 0,}; pub fn resolve(input: Input) ActivityPlacement { const activity_rows = @max(input.activity_rows, 1); if (input.tool_before_activity) { const tool_row = if (input.cursor_col == 1) input.cursor_row +| input.transient_gap_rows else if (input.place_mid_line_active) input.cursor_row +| input.transient_gap_rows +| 1 else return .none; return .{ .transient_row = .{ .row = tool_row +| 2, .row_count = activity_rows, .gap_above_rows = input.transient_gap_rows, .footer_clearance_rows = input.footer_clearance_rows, .tool_row = tool_row, } }; } if (input.active_label_present) { const gap_rows = input.transient_gap_rows; if (input.cursor_col == 1) { return .{ .transient_row = .{ .row = input.cursor_row +| gap_rows, .row_count = activity_rows, .gap_above_rows = gap_rows, .footer_clearance_rows = input.footer_clearance_rows, } }; } if (input.place_mid_line_active) { return .{ .transient_row = .{ .row = input.cursor_row +| gap_rows +| 1, .row_count = activity_rows, .gap_above_rows = gap_rows, .footer_clearance_rows = input.footer_clearance_rows, } }; } } if (!input.active_label_present and input.entry_bound_active and input.stream_active) { if (input.cursor_col == 1) { return .{ .transient_row = .{ .row = input.cursor_row, .gap_above_rows = 0, .footer_clearance_rows = input.footer_clearance_rows } }; } const sticky_row = if (input.footer_top_for_extra > 1) input.footer_top_for_extra - 1 else input.footer_top_for_extra; return .{ .transient_row = .{ .row = sticky_row, .gap_above_rows = 0, .footer_clearance_rows = input.footer_clearance_rows } }; } return .none;} pub fn suppressEntryOverlayForBanner(activity: *ActivityPlacement, banner_active: bool) void { if (!banner_active) return; switch (activity.*) { .overlay_entry => { activity.* = .none; }, .none, .transient_row => {}, }} test "activity placement reserves row only for thinking shimmer" { const thinking = resolve(.{ .active_label_present = true, .cursor_row = 5, .cursor_col = 1, .transient_gap_rows = 0, .place_mid_line_active = false, .entry_bound_active = false, .stream_active = false, .footer_top_for_extra = 9, }); try std.testing.expect(!thinking.reservesFooterRow()); try std.testing.expectEqual(@as(u16, 0), thinking.reservedFooterRows()); try std.testing.expectEqual(@as(u16, 1), thinking.transcriptReservationRows()); try std.testing.expectEqual(@as(u16, 5), thinking.row().?); const none = resolve(.{ .active_label_present = true, .cursor_row = 5, .cursor_col = 3, .transient_gap_rows = 0, .place_mid_line_active = false, .entry_bound_active = false, .stream_active = false, .footer_top_for_extra = 9, }); try std.testing.expect(!none.reservesFooterRow()); try std.testing.expectEqual(@as(u16, 0), none.reservedFooterRows()); try std.testing.expectEqual(@as(u16, 0), none.transcriptReservationRows()); try std.testing.expectEqual(@as(?u16, null), none.row());} test "transient activity tracks leading gap separately from footer clearance" { const activity = resolve(.{ .active_label_present = true, .cursor_row = 3, .cursor_col = 1, .transient_gap_rows = 1, .place_mid_line_active = false, .entry_bound_active = false, .stream_active = false, .footer_top_for_extra = 6, }); try std.testing.expect(!activity.reservesFooterRow()); try std.testing.expectEqual(@as(u16, 4), activity.row().?); try std.testing.expectEqual(@as(?u16, 3), activity.reservedTopRow()); try std.testing.expectEqual(@as(u16, 2), activity.transcriptReservationRows()); try std.testing.expect(activity.row().? < 6);} test "transient activity does not count its leading gap as footer clearance" { const activity = resolve(.{ .active_label_present = true, .cursor_row = 10, .cursor_col = 1, .transient_gap_rows = 1, .place_mid_line_active = false, .entry_bound_active = false, .stream_active = false, .footer_top_for_extra = 12, }); try std.testing.expectEqual(@as(u16, 11), activity.row().?); try std.testing.expectEqual(@as(u16, 0), activity.reservedFooterRows()); try std.testing.expectEqual(@as(?u16, 10), activity.reservedTopRow());} test "mid-line transient activity reserves gap plus status row only" { const activity = resolve(.{ .active_label_present = true, .cursor_row = 5, .cursor_col = 12, .transient_gap_rows = 1, .place_mid_line_active = true, .entry_bound_active = false, .stream_active = false, .footer_top_for_extra = 6, }); switch (activity) { .transient_row => |transient| { try std.testing.expectEqual(@as(u16, 7), transient.row); try std.testing.expectEqual(@as(u16, 1), transient.gap_above_rows); try std.testing.expectEqual(@as(?u16, 6), activity.reservedTopRow()); try std.testing.expectEqual(@as(u16, 2), activity.transcriptReservationRows()); }, .none, .overlay_entry => return error.TestUnexpectedResult, }} test "active entry remains in the transient activity band" { const activity = resolve(.{ .active_label_present = false, .cursor_row = 9, .cursor_col = 1, .transient_gap_rows = 0, .place_mid_line_active = false, .entry_bound_active = true, .stream_active = true, .footer_top_for_extra = 10, }); try std.testing.expectEqual(@as(u16, 9), activity.row().?); try std.testing.expectEqual(@as(u16, 0), activity.reservedFooterRows()); switch (activity) { .transient_row => |transient| { try std.testing.expectEqual(@as(u16, 9), transient.row); try std.testing.expectEqual(@as(u16, 0), transient.gap_above_rows); }, .none, .overlay_entry => return error.TestUnexpectedResult, }} test "banner suppression converts entry overlay to none" { var activity = ActivityPlacement{ .overlay_entry = 7 }; suppressEntryOverlayForBanner(&activity, true); switch (activity) { .none => {}, .overlay_entry, .transient_row => return error.TestUnexpectedResult, }}
const std = @import("std");
pub const ActivityPlacement = union(enum) {
const Transient = struct {
row: u16,
row_count: u16 = 1,
gap_above_rows: u16,
footer_clearance_rows: u16 = 0,
tool_row: ?u16 = null,
};
none,
transient_row: Transient,
overlay_entry: u16,
pub fn reservedFooterRows(self: ActivityPlacement) u16 {
return switch (self) {
.transient_row => |activity| activity.footer_clearance_rows,
.overlay_entry, .none => 0,
};
}
pub fn reservesFooterRow(self: ActivityPlacement) bool {
return self.reservedFooterRows() > 0;
}
pub fn reservedTopRow(self: ActivityPlacement) ?u16 {
return switch (self) {
.transient_row => |activity| {
const first_row = activity.tool_row orelse activity.row;
if (first_row <= activity.gap_above_rows) return 1;
return first_row - activity.gap_above_rows;
},
.overlay_entry, .none => null,
};
}
pub fn transcriptReservationRows(self: ActivityPlacement) u16 {
return switch (self) {
.transient_row => |activity| activity.gap_above_rows +| (activity.row -| (activity.tool_row orelse activity.row)) +| activity.row_count +| activity.footer_clearance_rows,
.overlay_entry, .none => 0,
};
}
pub fn requiredFooterTop(self: ActivityPlacement) ?u16 {
return switch (self) {
.transient_row => |activity| activity.row +| activity.row_count +| activity.footer_clearance_rows,
.overlay_entry, .none => null,
};
}
pub fn row(self: ActivityPlacement) ?u16 {
return switch (self) {
.transient_row => |activity| activity.row,
.overlay_entry => |r| r,
.none => null,
};
}
pub fn primaryRowCount(self: ActivityPlacement) u16 {
return switch (self) {
.transient_row => |activity| activity.row_count,
.overlay_entry => 1,
.none => 0,
};
}
pub fn toolRow(self: ActivityPlacement) ?u16 {
return switch (self) {
.transient_row => |activity| activity.tool_row,
.overlay_entry, .none => null,
};
}
};
pub const Input = struct {
active_label_present: bool,
activity_rows: u16 = 1,
tool_before_activity: bool = false,
cursor_row: u16,
cursor_col: u16,
transient_gap_rows: u16,
place_mid_line_active: bool,
entry_bound_active: bool,
stream_active: bool,
footer_top_for_extra: u16,
footer_clearance_rows: u16 = 0,
};
pub fn resolve(input: Input) ActivityPlacement {
const activity_rows = @max(input.activity_rows, 1);
if (input.tool_before_activity) {
const tool_row = if (input.cursor_col == 1)
input.cursor_row +| input.transient_gap_rows
else if (input.place_mid_line_active)
input.cursor_row +| input.transient_gap_rows +| 1
else
return .none;
return .{ .transient_row = .{
.row = tool_row +| 2,
.row_count = activity_rows,
.gap_above_rows = input.transient_gap_rows,
.footer_clearance_rows = input.footer_clearance_rows,
.tool_row = tool_row,
} };
}
if (input.active_label_present) {
const gap_rows = input.transient_gap_rows;
if (input.cursor_col == 1) {
return .{ .transient_row = .{
.row = input.cursor_row +| gap_rows,
.row_count = activity_rows,
.gap_above_rows = gap_rows,
.footer_clearance_rows = input.footer_clearance_rows,
} };
}
if (input.place_mid_line_active) {
return .{ .transient_row = .{
.row = input.cursor_row +| gap_rows +| 1,
.row_count = activity_rows,
.gap_above_rows = gap_rows,
.footer_clearance_rows = input.footer_clearance_rows,
} };
}
}
if (!input.active_label_present and input.entry_bound_active and input.stream_active) {
if (input.cursor_col == 1) {
return .{ .transient_row = .{ .row = input.cursor_row, .gap_above_rows = 0, .footer_clearance_rows = input.footer_clearance_rows } };
}
const sticky_row = if (input.footer_top_for_extra > 1) input.footer_top_for_extra - 1 else input.footer_top_for_extra;
return .{ .transient_row = .{ .row = sticky_row, .gap_above_rows = 0, .footer_clearance_rows = input.footer_clearance_rows } };
}
return .none;
}
pub fn suppressEntryOverlayForBanner(activity: *ActivityPlacement, banner_active: bool) void {
if (!banner_active) return;
switch (activity.*) {
.overlay_entry => {
activity.* = .none;
},
.none, .transient_row => {},
}
}
test "activity placement reserves row only for thinking shimmer" {
const thinking = resolve(.{
.active_label_present = true,
.cursor_row = 5,
.cursor_col = 1,
.transient_gap_rows = 0,
.place_mid_line_active = false,
.entry_bound_active = false,
.stream_active = false,
.footer_top_for_extra = 9,
});
try std.testing.expect(!thinking.reservesFooterRow());
try std.testing.expectEqual(@as(u16, 0), thinking.reservedFooterRows());
try std.testing.expectEqual(@as(u16, 1), thinking.transcriptReservationRows());
try std.testing.expectEqual(@as(u16, 5), thinking.row().?);
const none = resolve(.{
.active_label_present = true,
.cursor_row = 5,
.cursor_col = 3,
.transient_gap_rows = 0,
.place_mid_line_active = false,
.entry_bound_active = false,
.stream_active = false,
.footer_top_for_extra = 9,
});
try std.testing.expect(!none.reservesFooterRow());
try std.testing.expectEqual(@as(u16, 0), none.reservedFooterRows());
try std.testing.expectEqual(@as(u16, 0), none.transcriptReservationRows());
try std.testing.expectEqual(@as(?u16, null), none.row());
}
test "transient activity tracks leading gap separately from footer clearance" {
const activity = resolve(.{
.active_label_present = true,
.cursor_row = 3,
.cursor_col = 1,
.transient_gap_rows = 1,
.place_mid_line_active = false,
.entry_bound_active = false,
.stream_active = false,
.footer_top_for_extra = 6,
});
try std.testing.expect(!activity.reservesFooterRow());
try std.testing.expectEqual(@as(u16, 4), activity.row().?);
try std.testing.expectEqual(@as(?u16, 3), activity.reservedTopRow());
try std.testing.expectEqual(@as(u16, 2), activity.transcriptReservationRows());
try std.testing.expect(activity.row().? < 6);
}
test "transient activity does not count its leading gap as footer clearance" {
const activity = resolve(.{
.active_label_present = true,
.cursor_row = 10,
.cursor_col = 1,
.transient_gap_rows = 1,
.place_mid_line_active = false,
.entry_bound_active = false,
.stream_active = false,
.footer_top_for_extra = 12,
});
try std.testing.expectEqual(@as(u16, 11), activity.row().?);
try std.testing.expectEqual(@as(u16, 0), activity.reservedFooterRows());
try std.testing.expectEqual(@as(?u16, 10), activity.reservedTopRow());
}
test "mid-line transient activity reserves gap plus status row only" {
const activity = resolve(.{
.active_label_present = true,
.cursor_row = 5,
.cursor_col = 12,
.transient_gap_rows = 1,
.place_mid_line_active = true,
.entry_bound_active = false,
.stream_active = false,
.footer_top_for_extra = 6,
});
switch (activity) {
.transient_row => |transient| {
try std.testing.expectEqual(@as(u16, 7), transient.row);
try std.testing.expectEqual(@as(u16, 1), transient.gap_above_rows);
try std.testing.expectEqual(@as(?u16, 6), activity.reservedTopRow());
try std.testing.expectEqual(@as(u16, 2), activity.transcriptReservationRows());
},
.none, .overlay_entry => return error.TestUnexpectedResult,
}
}
test "active entry remains in the transient activity band" {
const activity = resolve(.{
.active_label_present = false,
.cursor_row = 9,
.cursor_col = 1,
.transient_gap_rows = 0,
.place_mid_line_active = false,
.entry_bound_active = true,
.stream_active = true,
.footer_top_for_extra = 10,
});
try std.testing.expectEqual(@as(u16, 9), activity.row().?);
try std.testing.expectEqual(@as(u16, 0), activity.reservedFooterRows());
switch (activity) {
.transient_row => |transient| {
try std.testing.expectEqual(@as(u16, 9), transient.row);
try std.testing.expectEqual(@as(u16, 0), transient.gap_above_rows);
},
.none, .overlay_entry => return error.TestUnexpectedResult,
}
}
test "banner suppression converts entry overlay to none" {
var activity = ActivityPlacement{ .overlay_entry = 7 };
suppressEntryOverlayForBanner(&activity, true);
switch (activity) {
.none => {},
.overlay_entry, .transient_row => return error.TestUnexpectedResult,
}
}