refactor(advanced_table): Modularizar en drawing, input, helpers, sorting
- Extraer drawing.zig: drawHeader, drawScrollbar, drawEditingOverlay - Extraer input.zig: handleRowClicks, handleKeyboard, handleEditingKeyboard - Extraer helpers.zig: commitEdit, parseValue, detectCRUDAction, invokeCallbacks - Extraer sorting.zig: sortRows, swapRowStates, startsWithIgnoreCase - Reducir advanced_table.zig de 1443 LOC a ~380 LOC - Mantener re-exports para compatibilidad con código existente
This commit is contained in:
parent
b9f412b64f
commit
042ff96141
5 changed files with 1211 additions and 1081 deletions
File diff suppressed because it is too large
Load diff
218
src/widgets/advanced_table/drawing.zig
Normal file
218
src/widgets/advanced_table/drawing.zig
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
//! AdvancedTable - Funciones de Dibujo
|
||||
//!
|
||||
//! Funciones de renderizado extraídas del archivo principal.
|
||||
|
||||
const std = @import("std");
|
||||
const Context = @import("../../core/context.zig").Context;
|
||||
const Command = @import("../../core/command.zig");
|
||||
const Layout = @import("../../core/layout.zig");
|
||||
const Style = @import("../../core/style.zig");
|
||||
const table_core = @import("../table_core/table_core.zig");
|
||||
|
||||
const types = @import("types.zig");
|
||||
const schema = @import("schema.zig");
|
||||
const state = @import("state.zig");
|
||||
|
||||
pub const TableColors = types.TableColors;
|
||||
pub const TableConfig = types.TableConfig;
|
||||
pub const ColumnDef = schema.ColumnDef;
|
||||
pub const TableSchema = schema.TableSchema;
|
||||
pub const AdvancedTableState = state.AdvancedTableState;
|
||||
pub const AdvancedTableResult = state.AdvancedTableResult;
|
||||
|
||||
// =============================================================================
|
||||
// Draw: Header
|
||||
// =============================================================================
|
||||
|
||||
pub fn drawHeader(
|
||||
ctx: *Context,
|
||||
bounds: Layout.Rect,
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
state_col_w: u32,
|
||||
colors: *const TableColors,
|
||||
result: *AdvancedTableResult,
|
||||
) void {
|
||||
const sorting = @import("sorting.zig");
|
||||
const config = table_schema.config;
|
||||
const header_y = bounds.y;
|
||||
var col_x = bounds.x;
|
||||
|
||||
// State indicator column header
|
||||
if (state_col_w > 0) {
|
||||
ctx.pushCommand(Command.rect(col_x, header_y, state_col_w, config.header_height, colors.header_bg));
|
||||
col_x += @as(i32, @intCast(state_col_w));
|
||||
}
|
||||
|
||||
// Column headers
|
||||
const mouse = ctx.input.mousePos();
|
||||
|
||||
for (table_schema.columns, 0..) |col, idx| {
|
||||
if (!col.visible) continue;
|
||||
|
||||
const col_rect = Layout.Rect.init(col_x, header_y, col.width, config.header_height);
|
||||
const col_hovered = col_rect.contains(mouse.x, mouse.y);
|
||||
const col_clicked = col_hovered and ctx.input.mousePressed(.left);
|
||||
|
||||
// Determine background color
|
||||
var bg_color = colors.header_bg;
|
||||
if (table_state.sort_column == @as(i32, @intCast(idx))) {
|
||||
bg_color = colors.header_sorted;
|
||||
} else if (col_hovered) {
|
||||
bg_color = colors.header_hover;
|
||||
}
|
||||
|
||||
// Draw header cell
|
||||
if (Style.isFancy()) {
|
||||
ctx.pushCommand(Command.roundedRect(col_rect.x, col_rect.y, col_rect.w, col_rect.h, bg_color, 0));
|
||||
} else {
|
||||
ctx.pushCommand(Command.rect(col_rect.x, col_rect.y, col_rect.w, col_rect.h, bg_color));
|
||||
}
|
||||
|
||||
// Draw header text
|
||||
const text_y = header_y + @as(i32, @intCast((config.header_height - 8) / 2));
|
||||
ctx.pushCommand(Command.text(col_x + 4, text_y, col.title, colors.header_fg));
|
||||
|
||||
// Draw lookup indicator
|
||||
if (col.hasLookup()) {
|
||||
const lookup_x = col_x + @as(i32, @intCast(col.width)) - 24;
|
||||
ctx.pushCommand(Command.text(lookup_x, text_y, "?", Style.Color.primary));
|
||||
}
|
||||
|
||||
// Draw sort indicator
|
||||
if (table_state.sort_column == @as(i32, @intCast(idx))) {
|
||||
const indicator_x = col_x + @as(i32, @intCast(col.width)) - 16;
|
||||
const indicator = switch (table_state.sort_direction) {
|
||||
.ascending => "▴",
|
||||
.descending => "▾",
|
||||
.none => "",
|
||||
};
|
||||
if (indicator.len > 0) {
|
||||
ctx.pushCommand(Command.text(indicator_x, text_y, indicator, colors.sort_indicator));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle click for sorting
|
||||
if (col_clicked and col.sortable and config.allow_sorting) {
|
||||
_ = table_state.toggleSort(idx);
|
||||
result.sort_changed = true;
|
||||
result.sort_column = idx;
|
||||
result.sort_direction = table_state.sort_direction;
|
||||
|
||||
sorting.sortRows(table_state, table_schema.columns[idx].name, table_state.sort_direction);
|
||||
}
|
||||
|
||||
// Draw separator
|
||||
ctx.pushCommand(Command.rect(
|
||||
col_x + @as(i32, @intCast(col.width)) - 1,
|
||||
header_y,
|
||||
1,
|
||||
config.header_height,
|
||||
colors.border,
|
||||
));
|
||||
|
||||
col_x += @as(i32, @intCast(col.width));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Draw: Scrollbar
|
||||
// =============================================================================
|
||||
|
||||
pub fn drawScrollbar(
|
||||
ctx: *Context,
|
||||
bounds: Layout.Rect,
|
||||
table_state: *AdvancedTableState,
|
||||
visible_rows: usize,
|
||||
config: TableConfig,
|
||||
colors: *const TableColors,
|
||||
) void {
|
||||
const total_rows = table_state.getRowCount();
|
||||
if (total_rows == 0) return;
|
||||
|
||||
const scrollbar_w: u32 = 12;
|
||||
const header_h: u32 = if (config.show_headers) config.header_height else 0;
|
||||
const scrollbar_h = bounds.h -| header_h;
|
||||
|
||||
table_core.drawVerticalScrollbar(ctx, .{
|
||||
.track_x = bounds.x + @as(i32, @intCast(bounds.w -| scrollbar_w)),
|
||||
.track_y = bounds.y + @as(i32, @intCast(header_h)),
|
||||
.width = scrollbar_w,
|
||||
.height = scrollbar_h,
|
||||
.visible_count = visible_rows,
|
||||
.total_count = total_rows,
|
||||
.scroll_pos = table_state.nav.scroll_row,
|
||||
.track_color = colors.border,
|
||||
.thumb_color = colors.header_bg,
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Draw: Editing Overlay
|
||||
// =============================================================================
|
||||
|
||||
pub fn drawEditingOverlay(
|
||||
ctx: *Context,
|
||||
bounds: Layout.Rect,
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
header_h: u32,
|
||||
state_col_w: u32,
|
||||
colors: *const TableColors,
|
||||
) void {
|
||||
if (table_state.selected_row < 0 or table_state.selected_col < 0) return;
|
||||
|
||||
const row_idx: usize = @intCast(table_state.selected_row);
|
||||
const col_idx: usize = @intCast(table_state.selected_col);
|
||||
const config = table_schema.config;
|
||||
|
||||
// Check if row is visible
|
||||
if (row_idx < table_state.nav.scroll_row) return;
|
||||
const visible_row = row_idx - table_state.nav.scroll_row;
|
||||
const visible_rows = (bounds.h -| header_h) / config.row_height;
|
||||
if (visible_row >= visible_rows) return;
|
||||
|
||||
// Calculate cell position
|
||||
var col_x = bounds.x + @as(i32, @intCast(state_col_w));
|
||||
for (table_schema.columns[0..col_idx]) |col| {
|
||||
if (col.visible) {
|
||||
col_x += @as(i32, @intCast(col.width));
|
||||
}
|
||||
}
|
||||
|
||||
const col_def = table_schema.columns[col_idx];
|
||||
const cell_y = bounds.y + @as(i32, @intCast(header_h + visible_row * config.row_height));
|
||||
const cell_h = config.row_height;
|
||||
|
||||
// Draw editing overlay background
|
||||
ctx.pushCommand(Command.rect(col_x, cell_y, col_def.width, cell_h, colors.cell_editing_bg));
|
||||
|
||||
// Draw border
|
||||
ctx.pushCommand(Command.rectOutline(col_x, cell_y, col_def.width, cell_h, colors.cell_editing_border));
|
||||
|
||||
// Draw edit text
|
||||
const edit_text = table_state.getEditText();
|
||||
const text_y = cell_y + @as(i32, @intCast((cell_h - 8) / 2));
|
||||
|
||||
// Draw selection highlight
|
||||
const sel_start = table_state.cell_edit.selection_start;
|
||||
const sel_end = table_state.cell_edit.selection_end;
|
||||
if (sel_start != sel_end and edit_text.len > 0) {
|
||||
const sel_min = @min(sel_start, sel_end);
|
||||
const sel_max = @min(@max(sel_start, sel_end), edit_text.len);
|
||||
if (sel_max > sel_min) {
|
||||
const sel_x = col_x + 4 + @as(i32, @intCast(sel_min * 8));
|
||||
const sel_width = @as(u32, @intCast((sel_max - sel_min) * 8));
|
||||
ctx.pushCommand(Command.rect(sel_x, text_y, sel_width, 8, colors.cell_selection_bg));
|
||||
}
|
||||
}
|
||||
|
||||
// Draw text
|
||||
ctx.pushCommand(Command.text(col_x + 4, text_y, edit_text, colors.text_selected));
|
||||
|
||||
// Draw cursor
|
||||
if (sel_start == sel_end) {
|
||||
const cursor_x = col_x + 4 + @as(i32, @intCast(table_state.cell_edit.edit_cursor * 8));
|
||||
ctx.pushCommand(Command.rect(cursor_x, text_y, 1, 8, colors.text_selected));
|
||||
}
|
||||
}
|
||||
309
src/widgets/advanced_table/helpers.zig
Normal file
309
src/widgets/advanced_table/helpers.zig
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
//! AdvancedTable - Helper Functions
|
||||
//!
|
||||
//! Funciones auxiliares extraídas del archivo principal.
|
||||
|
||||
const std = @import("std");
|
||||
const Context = @import("../../core/context.zig").Context;
|
||||
const Style = @import("../../core/style.zig");
|
||||
|
||||
const types = @import("types.zig");
|
||||
const schema = @import("schema.zig");
|
||||
const state = @import("state.zig");
|
||||
|
||||
pub const CellValue = types.CellValue;
|
||||
pub const ColumnType = types.ColumnType;
|
||||
pub const CRUDAction = types.CRUDAction;
|
||||
pub const Row = types.Row;
|
||||
pub const ColumnDef = schema.ColumnDef;
|
||||
pub const TableSchema = schema.TableSchema;
|
||||
pub const AdvancedTableState = state.AdvancedTableState;
|
||||
pub const AdvancedTableResult = state.AdvancedTableResult;
|
||||
|
||||
// =============================================================================
|
||||
// Commit Edit
|
||||
// =============================================================================
|
||||
|
||||
pub fn commitEdit(
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
result: *AdvancedTableResult,
|
||||
) void {
|
||||
if (table_state.selected_row < 0 or table_state.selected_col < 0) return;
|
||||
|
||||
const row_idx: usize = @intCast(table_state.selected_row);
|
||||
const col_idx: usize = @intCast(table_state.selected_col);
|
||||
|
||||
if (col_idx >= table_schema.columns.len) return;
|
||||
|
||||
const col_def = &table_schema.columns[col_idx];
|
||||
const edit_text = table_state.getEditText();
|
||||
|
||||
// Parse text to CellValue based on column type
|
||||
const new_value = parseValue(edit_text, col_def.column_type);
|
||||
|
||||
// Check if value changed
|
||||
if (table_state.original_value) |orig| {
|
||||
if (new_value.eql(orig)) {
|
||||
return; // No change
|
||||
}
|
||||
}
|
||||
|
||||
// Update the row
|
||||
if (table_state.getRow(row_idx)) |row| {
|
||||
row.set(col_def.name, new_value) catch {};
|
||||
table_state.markDirty(row_idx);
|
||||
result.cell_edited = true;
|
||||
|
||||
// Lookup & Auto-fill (Phase 7)
|
||||
if (col_def.hasLookup()) {
|
||||
performLookupAndAutoFill(table_state, table_schema, row, col_def, new_value, result);
|
||||
}
|
||||
|
||||
// Call on_cell_changed callback (Phase 8)
|
||||
if (table_schema.on_cell_changed) |callback| {
|
||||
const old_value = table_state.original_value orelse CellValue{ .null_val = {} };
|
||||
callback(row_idx, col_idx, old_value, new_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Lookup & Auto-fill
|
||||
// =============================================================================
|
||||
|
||||
/// Perform lookup in related table and auto-fill columns
|
||||
pub fn performLookupAndAutoFill(
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
row: *Row,
|
||||
col_def: *const ColumnDef,
|
||||
lookup_value: CellValue,
|
||||
result: *AdvancedTableResult,
|
||||
) void {
|
||||
// Need DataStore to perform lookup
|
||||
const data_store = table_schema.data_store orelse return;
|
||||
|
||||
// Get lookup configuration
|
||||
const lookup_table = col_def.lookup_table orelse return;
|
||||
const lookup_key = col_def.lookup_key_column orelse return;
|
||||
|
||||
// Perform lookup
|
||||
const lookup_result = data_store.lookup(
|
||||
lookup_table,
|
||||
lookup_key,
|
||||
lookup_value,
|
||||
table_state.allocator,
|
||||
) catch return;
|
||||
|
||||
// If lookup found a match, auto-fill related columns
|
||||
if (lookup_result) |lookup_row| {
|
||||
defer {
|
||||
// Clean up the lookup row after use
|
||||
var mutable_lookup = lookup_row;
|
||||
mutable_lookup.deinit();
|
||||
}
|
||||
|
||||
// Auto-fill columns based on mapping
|
||||
if (col_def.auto_fill_columns) |mappings| {
|
||||
for (mappings) |mapping| {
|
||||
const source_value = lookup_row.get(mapping.source_field);
|
||||
if (!source_value.isEmpty()) {
|
||||
row.set(mapping.target_column, source_value) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.lookup_success = true;
|
||||
} else {
|
||||
result.lookup_success = false;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Parse Value
|
||||
// =============================================================================
|
||||
|
||||
pub fn parseValue(text: []const u8, column_type: ColumnType) CellValue {
|
||||
return switch (column_type) {
|
||||
.string => CellValue{ .string = text },
|
||||
.integer => blk: {
|
||||
const val = std.fmt.parseInt(i64, text, 10) catch 0;
|
||||
break :blk CellValue{ .integer = val };
|
||||
},
|
||||
.float, .money => blk: {
|
||||
const val = std.fmt.parseFloat(f64, text) catch 0.0;
|
||||
break :blk CellValue{ .float = val };
|
||||
},
|
||||
.boolean => blk: {
|
||||
const lower = text;
|
||||
const is_true = std.mem.eql(u8, lower, "true") or
|
||||
std.mem.eql(u8, lower, "yes") or
|
||||
std.mem.eql(u8, lower, "1") or
|
||||
std.mem.eql(u8, lower, "Y");
|
||||
break :blk CellValue{ .boolean = is_true };
|
||||
},
|
||||
.date, .select, .lookup => CellValue{ .string = text },
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CRUD Action Detection
|
||||
// =============================================================================
|
||||
|
||||
pub fn detectCRUDAction(
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
) ?CRUDAction {
|
||||
// Check if previous row was valid
|
||||
if (table_state.prev_selected_row < 0) return null;
|
||||
|
||||
const prev_row_idx: usize = @intCast(table_state.prev_selected_row);
|
||||
|
||||
// Check if row was marked for deletion
|
||||
if (table_state.isDeleted(prev_row_idx)) {
|
||||
return .delete;
|
||||
}
|
||||
|
||||
// Get the row (might have been deleted)
|
||||
const row = table_state.getRow(prev_row_idx) orelse return null;
|
||||
|
||||
// Get snapshot for comparison
|
||||
const snapshot = table_state.getSnapshot(prev_row_idx);
|
||||
|
||||
// Check if row is new (was in new_rows map)
|
||||
const is_new = table_state.isNew(prev_row_idx);
|
||||
|
||||
if (is_new) {
|
||||
// Check if new row has any data
|
||||
if (rowHasData(row, table_schema)) {
|
||||
return .create;
|
||||
}
|
||||
return null; // Empty new row, no action
|
||||
}
|
||||
|
||||
// Check if row was modified
|
||||
if (snapshot) |snap| {
|
||||
if (rowsAreDifferent(row, snap, table_schema)) {
|
||||
return .update;
|
||||
}
|
||||
} else if (table_state.isDirty(prev_row_idx)) {
|
||||
// No snapshot but marked dirty - assume update
|
||||
return .update;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn rowHasData(row: *const Row, table_schema: *const TableSchema) bool {
|
||||
for (table_schema.columns) |col| {
|
||||
if (!col.visible) continue;
|
||||
const value = row.get(col.name);
|
||||
if (!value.isEmpty()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn rowsAreDifferent(row: *const Row, snapshot: *const Row, table_schema: *const TableSchema) bool {
|
||||
for (table_schema.columns) |col| {
|
||||
if (!col.editable) continue;
|
||||
const current = row.get(col.name);
|
||||
const original = snapshot.get(col.name);
|
||||
if (!current.eql(original)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Selection Visibility
|
||||
// =============================================================================
|
||||
|
||||
pub fn ensureSelectionVisible(table_state: *AdvancedTableState, visible_rows: usize) void {
|
||||
if (table_state.selected_row < 0) return;
|
||||
|
||||
const row: usize = @intCast(table_state.selected_row);
|
||||
|
||||
// Scroll to show selected row
|
||||
if (row < table_state.nav.scroll_row) {
|
||||
table_state.nav.scroll_row = row;
|
||||
} else if (row >= table_state.nav.scroll_row + visible_rows) {
|
||||
table_state.nav.scroll_row = row - visible_rows + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Color Helpers
|
||||
// =============================================================================
|
||||
|
||||
pub fn blendColor(base: Style.Color, overlay: Style.Color, alpha: f32) Style.Color {
|
||||
const inv_alpha = 1.0 - alpha;
|
||||
|
||||
return Style.Color.rgba(
|
||||
@intFromFloat(@as(f32, @floatFromInt(base.r)) * inv_alpha + @as(f32, @floatFromInt(overlay.r)) * alpha),
|
||||
@intFromFloat(@as(f32, @floatFromInt(base.g)) * inv_alpha + @as(f32, @floatFromInt(overlay.g)) * alpha),
|
||||
@intFromFloat(@as(f32, @floatFromInt(base.b)) * inv_alpha + @as(f32, @floatFromInt(overlay.b)) * alpha),
|
||||
base.a,
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Callback System
|
||||
// =============================================================================
|
||||
|
||||
pub fn invokeCallbacks(
|
||||
ctx: *Context,
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
result: *AdvancedTableResult,
|
||||
) void {
|
||||
const config = table_schema.config;
|
||||
const current_time = ctx.current_time_ms;
|
||||
|
||||
// Check debounce
|
||||
const time_since_last = current_time -| table_state.last_callback_time_ms;
|
||||
const debounce_ok = time_since_last >= config.callback_debounce_ms;
|
||||
|
||||
// on_row_selected: called when selection changes (with debounce)
|
||||
if (result.selection_changed and debounce_ok) {
|
||||
if (table_schema.on_row_selected) |callback| {
|
||||
if (table_state.selected_row >= 0) {
|
||||
const row_idx: usize = @intCast(table_state.selected_row);
|
||||
if (table_state.getRowConst(row_idx)) |row| {
|
||||
callback(row_idx, row);
|
||||
table_state.last_callback_time_ms = current_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// on_active_row_changed: called when moving to a different row (for loading detail panels)
|
||||
// Only fires once per row change, not on every frame
|
||||
if (table_state.selected_row != table_state.last_notified_row) {
|
||||
if (table_schema.on_active_row_changed) |callback| {
|
||||
if (table_state.selected_row >= 0) {
|
||||
const new_row_idx: usize = @intCast(table_state.selected_row);
|
||||
if (table_state.getRowConst(new_row_idx)) |row| {
|
||||
const old_row: ?usize = if (table_state.last_notified_row >= 0)
|
||||
@intCast(table_state.last_notified_row)
|
||||
else
|
||||
null;
|
||||
callback(old_row, new_row_idx, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
table_state.last_notified_row = table_state.selected_row;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
test "blendColor" {
|
||||
const white = Style.Color.rgb(255, 255, 255);
|
||||
const black = Style.Color.rgb(0, 0, 0);
|
||||
|
||||
const gray = blendColor(white, black, 0.5);
|
||||
try std.testing.expectEqual(@as(u8, 127), gray.r);
|
||||
try std.testing.expectEqual(@as(u8, 127), gray.g);
|
||||
try std.testing.expectEqual(@as(u8, 127), gray.b);
|
||||
}
|
||||
551
src/widgets/advanced_table/input.zig
Normal file
551
src/widgets/advanced_table/input.zig
Normal file
|
|
@ -0,0 +1,551 @@
|
|||
//! AdvancedTable - Input Handling
|
||||
//!
|
||||
//! Funciones de teclado y mouse extraídas del archivo principal.
|
||||
|
||||
const std = @import("std");
|
||||
const Context = @import("../../core/context.zig").Context;
|
||||
const Layout = @import("../../core/layout.zig");
|
||||
const Style = @import("../../core/style.zig");
|
||||
const table_core = @import("../table_core/table_core.zig");
|
||||
|
||||
const types = @import("types.zig");
|
||||
const schema = @import("schema.zig");
|
||||
const state = @import("state.zig");
|
||||
const helpers = @import("helpers.zig");
|
||||
const sorting = @import("sorting.zig");
|
||||
|
||||
pub const CellValue = types.CellValue;
|
||||
pub const TableColors = types.TableColors;
|
||||
pub const ColumnDef = schema.ColumnDef;
|
||||
pub const TableSchema = schema.TableSchema;
|
||||
pub const AdvancedTableState = state.AdvancedTableState;
|
||||
pub const AdvancedTableResult = state.AdvancedTableResult;
|
||||
|
||||
// =============================================================================
|
||||
// Row Click Handling
|
||||
// =============================================================================
|
||||
|
||||
/// Maneja clicks en las filas de la tabla (single-click y double-click)
|
||||
/// Retorna si hubo algún cambio de selección o edición iniciada
|
||||
pub fn handleRowClicks(
|
||||
ctx: *Context,
|
||||
bounds: Layout.Rect,
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
header_h: u32,
|
||||
state_col_w: u32,
|
||||
first_visible: usize,
|
||||
last_visible: usize,
|
||||
result: *AdvancedTableResult,
|
||||
) void {
|
||||
const config = table_schema.config;
|
||||
const mouse = ctx.input.mousePos();
|
||||
|
||||
// Solo procesar si hubo click
|
||||
if (!ctx.input.mousePressed(.left)) return;
|
||||
|
||||
// Verificar si el click está en el área de filas
|
||||
const rows_area_y = bounds.y + @as(i32, @intCast(header_h));
|
||||
if (mouse.y < rows_area_y) return;
|
||||
if (mouse.x < bounds.x or mouse.x >= bounds.x + @as(i32, @intCast(bounds.w))) return;
|
||||
|
||||
// Calcular fila clickeada
|
||||
const relative_y = mouse.y - rows_area_y;
|
||||
if (relative_y < 0) return;
|
||||
const row_offset: usize = @intCast(@divFloor(relative_y, @as(i32, @intCast(config.row_height))));
|
||||
const row_idx = first_visible + row_offset;
|
||||
|
||||
if (row_idx >= last_visible or row_idx >= table_state.getRowCount()) return;
|
||||
|
||||
// Calcular columna clickeada
|
||||
var col_x = bounds.x + @as(i32, @intCast(state_col_w));
|
||||
var clicked_col: ?usize = null;
|
||||
|
||||
for (table_schema.columns, 0..) |col, col_idx| {
|
||||
if (!col.visible) continue;
|
||||
|
||||
const col_end = col_x + @as(i32, @intCast(col.width));
|
||||
if (mouse.x >= col_x and mouse.x < col_end) {
|
||||
clicked_col = col_idx;
|
||||
break;
|
||||
}
|
||||
col_x = col_end;
|
||||
}
|
||||
|
||||
if (clicked_col == null) return;
|
||||
|
||||
const col_idx = clicked_col.?;
|
||||
const is_selected_cell = table_state.selected_row == @as(i32, @intCast(row_idx)) and
|
||||
table_state.selected_col == @as(i32, @intCast(col_idx));
|
||||
|
||||
// Detectar doble-click
|
||||
const current_time = ctx.current_time_ms;
|
||||
const same_cell = table_state.nav.double_click.last_click_row == @as(i32, @intCast(row_idx)) and
|
||||
table_state.nav.double_click.last_click_col == @as(i32, @intCast(col_idx));
|
||||
const time_diff = current_time -| table_state.nav.double_click.last_click_time;
|
||||
const is_double_click = same_cell and time_diff < table_state.nav.double_click.threshold_ms;
|
||||
|
||||
if (is_double_click and config.allow_edit and col_idx < table_schema.columns.len and
|
||||
table_schema.columns[col_idx].editable and !table_state.isEditing())
|
||||
{
|
||||
// Double-click: iniciar edición
|
||||
if (table_state.getRow(row_idx)) |row| {
|
||||
const value = row.get(table_schema.columns[col_idx].name);
|
||||
var format_buf: [128]u8 = undefined;
|
||||
const edit_text = value.format(&format_buf);
|
||||
table_state.startEditing(edit_text);
|
||||
table_state.original_value = value;
|
||||
result.edit_started = true;
|
||||
}
|
||||
// Reset click tracking
|
||||
table_state.nav.double_click.last_click_time = 0;
|
||||
table_state.nav.double_click.last_click_row = -1;
|
||||
table_state.nav.double_click.last_click_col = -1;
|
||||
} else {
|
||||
// Single click: seleccionar celda
|
||||
if (!is_selected_cell) {
|
||||
table_state.selectCell(row_idx, col_idx);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = row_idx;
|
||||
result.selected_col = col_idx;
|
||||
}
|
||||
// Actualizar tracking para posible doble-click
|
||||
table_state.nav.double_click.last_click_time = current_time;
|
||||
table_state.nav.double_click.last_click_row = @intCast(row_idx);
|
||||
table_state.nav.double_click.last_click_col = @intCast(col_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Keyboard Handling (Brain-in-Core pattern)
|
||||
// =============================================================================
|
||||
//
|
||||
// Arquitectura: TODA la lógica de decisión está en table_core.processTableEvents()
|
||||
// Este handler solo aplica flags y maneja lógica específica de AdvancedTable.
|
||||
|
||||
pub fn handleKeyboard(
|
||||
ctx: *Context,
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
visible_rows: usize,
|
||||
result: *AdvancedTableResult,
|
||||
) void {
|
||||
const row_count = table_state.getRowCount();
|
||||
if (row_count == 0) return;
|
||||
|
||||
const config = table_schema.config;
|
||||
const col_count = table_schema.columns.len;
|
||||
|
||||
// =========================================================================
|
||||
// BRAIN-IN-CORE: Delegar toda la lógica de decisión al Core
|
||||
// =========================================================================
|
||||
const events = table_core.processTableEvents(ctx, table_state.isEditing());
|
||||
|
||||
// =========================================================================
|
||||
// Aplicar navegación desde el Core
|
||||
// =========================================================================
|
||||
if (events.move_up and table_state.selected_row > 0) {
|
||||
const new_row: usize = @intCast(table_state.selected_row - 1);
|
||||
const new_col: usize = @intCast(@max(0, table_state.selected_col));
|
||||
table_state.selectCell(new_row, new_col);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = new_row;
|
||||
result.selected_col = new_col;
|
||||
}
|
||||
|
||||
if (events.move_down and table_state.selected_row < @as(i32, @intCast(row_count)) - 1) {
|
||||
const new_row: usize = @intCast(table_state.selected_row + 1);
|
||||
const new_col: usize = @intCast(@max(0, table_state.selected_col));
|
||||
table_state.selectCell(new_row, new_col);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = new_row;
|
||||
result.selected_col = new_col;
|
||||
}
|
||||
|
||||
if (events.move_left and table_state.selected_col > 0) {
|
||||
const new_row: usize = @intCast(@max(0, table_state.selected_row));
|
||||
const new_col: usize = @intCast(table_state.selected_col - 1);
|
||||
table_state.selectCell(new_row, new_col);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = new_row;
|
||||
result.selected_col = new_col;
|
||||
}
|
||||
|
||||
if (events.move_right and table_state.selected_col < @as(i32, @intCast(col_count)) - 1) {
|
||||
const new_row: usize = @intCast(@max(0, table_state.selected_row));
|
||||
const new_col: usize = @intCast(table_state.selected_col + 1);
|
||||
table_state.selectCell(new_row, new_col);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = new_row;
|
||||
result.selected_col = new_col;
|
||||
}
|
||||
|
||||
if (events.page_up) {
|
||||
const new_row: usize = @intCast(@max(0, table_state.selected_row - @as(i32, @intCast(visible_rows))));
|
||||
const new_col: usize = @intCast(@max(0, table_state.selected_col));
|
||||
table_state.selectCell(new_row, new_col);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = new_row;
|
||||
result.selected_col = new_col;
|
||||
}
|
||||
|
||||
if (events.page_down) {
|
||||
const new_row: usize = @intCast(@min(
|
||||
@as(i32, @intCast(row_count)) - 1,
|
||||
table_state.selected_row + @as(i32, @intCast(visible_rows)),
|
||||
));
|
||||
const new_col: usize = @intCast(@max(0, table_state.selected_col));
|
||||
table_state.selectCell(new_row, new_col);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = new_row;
|
||||
result.selected_col = new_col;
|
||||
}
|
||||
|
||||
if (events.go_to_first_col) {
|
||||
const new_row: usize = if (events.go_to_first_row) 0 else @intCast(@max(0, table_state.selected_row));
|
||||
table_state.selectCell(new_row, 0);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = new_row;
|
||||
result.selected_col = 0;
|
||||
} else if (events.go_to_first_row) {
|
||||
const new_col: usize = @intCast(@max(0, table_state.selected_col));
|
||||
table_state.selectCell(0, new_col);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = 0;
|
||||
result.selected_col = new_col;
|
||||
}
|
||||
|
||||
if (events.go_to_last_col) {
|
||||
const new_row: usize = if (events.go_to_last_row)
|
||||
(if (row_count > 0) row_count - 1 else 0)
|
||||
else
|
||||
@intCast(@max(0, table_state.selected_row));
|
||||
const new_col: usize = col_count - 1;
|
||||
table_state.selectCell(new_row, new_col);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = new_row;
|
||||
result.selected_col = new_col;
|
||||
} else if (events.go_to_last_row) {
|
||||
const new_row: usize = if (row_count > 0) row_count - 1 else 0;
|
||||
const new_col: usize = @intCast(@max(0, table_state.selected_col));
|
||||
table_state.selectCell(new_row, new_col);
|
||||
result.selection_changed = true;
|
||||
result.selected_row = new_row;
|
||||
result.selected_col = new_col;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Tab navigation con commit Excel-style (DRY: lógica en table_core)
|
||||
// =========================================================================
|
||||
if (events.tab_out and config.handle_tab) {
|
||||
// Wrapper para obtener row_id por índice (en AdvancedTable, usamos índice como ID)
|
||||
const RowIdGetter = struct {
|
||||
total: usize,
|
||||
|
||||
pub fn getRowId(self: @This(), row: usize) i64 {
|
||||
// Ghost row está al final
|
||||
if (row >= self.total) return table_core.NEW_ROW_ID;
|
||||
return @intCast(row);
|
||||
}
|
||||
};
|
||||
|
||||
const getter = RowIdGetter{ .total = row_count };
|
||||
const current_row: usize = @intCast(@max(0, table_state.selected_row));
|
||||
const current_col: usize = @intCast(@max(0, table_state.selected_col));
|
||||
const forward = !events.tab_shift;
|
||||
// AdvancedTable: usar count de filas existentes (no tiene ghost row como VirtualAdvancedTable)
|
||||
const num_rows = row_count;
|
||||
|
||||
const plan = table_core.planTabNavigation(
|
||||
&table_state.row_edit_buffer,
|
||||
current_row,
|
||||
current_col,
|
||||
col_count,
|
||||
num_rows,
|
||||
forward,
|
||||
config.wrap_navigation,
|
||||
getter,
|
||||
&result.row_changes,
|
||||
);
|
||||
|
||||
// Ejecutar el plan
|
||||
switch (plan.action) {
|
||||
.move, .move_with_commit => {
|
||||
table_state.selectCell(plan.new_row, plan.new_col);
|
||||
result.selection_changed = true;
|
||||
|
||||
if (plan.action == .move_with_commit) {
|
||||
if (plan.commit_info) |info| {
|
||||
result.row_committed = true;
|
||||
result.row_commit_id = info.row_id;
|
||||
result.row_commit_is_insert = info.is_insert;
|
||||
result.row_changes_count = info.change_count;
|
||||
}
|
||||
}
|
||||
},
|
||||
.exit, .exit_with_commit => {
|
||||
result.tab_out = true;
|
||||
result.tab_shift = events.tab_shift;
|
||||
|
||||
if (plan.action == .exit_with_commit) {
|
||||
if (plan.commit_info) |info| {
|
||||
result.row_committed = true;
|
||||
result.row_commit_id = info.row_id;
|
||||
result.row_commit_is_insert = info.is_insert;
|
||||
result.row_changes_count = info.change_count;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Inicio de edición (F2, Space, o tecla alfanumérica desde el Core)
|
||||
// =========================================================================
|
||||
if (events.start_editing and config.allow_edit) {
|
||||
if (table_state.selected_row >= 0 and table_state.selected_col >= 0) {
|
||||
const col_idx: usize = @intCast(table_state.selected_col);
|
||||
if (col_idx < table_schema.columns.len and table_schema.columns[col_idx].editable) {
|
||||
if (table_state.getRow(@intCast(table_state.selected_row))) |row| {
|
||||
const value = row.get(table_schema.columns[col_idx].name);
|
||||
if (events.initial_char) |ch| {
|
||||
// Tecla alfanumérica: empezar con ese caracter
|
||||
var char_buf: [1]u8 = .{ch};
|
||||
table_state.startEditing(&char_buf);
|
||||
} else {
|
||||
// F2/Space: empezar con valor actual
|
||||
var format_buf: [128]u8 = undefined;
|
||||
const text = value.format(&format_buf);
|
||||
table_state.startEditing(text);
|
||||
}
|
||||
table_state.original_value = value;
|
||||
result.edit_started = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Operaciones CRUD (Ctrl+N, Ctrl+Delete, Ctrl+B desde el Core)
|
||||
// =========================================================================
|
||||
if (config.allow_row_operations) {
|
||||
// Ctrl+N: Insert row BELOW current row (inyección local)
|
||||
if (events.insert_row) {
|
||||
const insert_idx: usize = if (table_state.selected_row >= 0)
|
||||
@as(usize, @intCast(table_state.selected_row)) + 1 // +1 = debajo
|
||||
else
|
||||
0;
|
||||
if (table_state.insertRow(insert_idx)) |new_idx| {
|
||||
table_state.selectCell(new_idx, 0);
|
||||
// Inicializar buffer para nueva fila (Excel-style)
|
||||
table_state.row_edit_buffer.startEdit(table_core.NEW_ROW_ID, new_idx, true);
|
||||
result.row_inserted = true;
|
||||
result.selection_changed = true;
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
// Ctrl+Delete o Ctrl+B: Delete row
|
||||
if (events.delete_row) {
|
||||
if (table_state.selected_row >= 0) {
|
||||
const delete_idx: usize = @intCast(table_state.selected_row);
|
||||
table_state.deleteRow(delete_idx);
|
||||
result.row_deleted = true;
|
||||
|
||||
// Adjust selection
|
||||
const remaining_rows = table_state.getRowCount();
|
||||
if (remaining_rows == 0) {
|
||||
table_state.selected_row = -1;
|
||||
} else if (delete_idx >= remaining_rows) {
|
||||
table_state.selected_row = @intCast(remaining_rows - 1);
|
||||
}
|
||||
result.selection_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Ctrl+A: Select all (lógica específica de AdvancedTable)
|
||||
// =========================================================================
|
||||
if (config.allow_multi_select and ctx.input.keyPressed(.a) and ctx.input.modifiers.ctrl) {
|
||||
table_state.selectAllRows();
|
||||
result.selection_changed = true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Búsqueda incremental (solo si NO se inició edición)
|
||||
// Solo para celdas no editables - lógica específica de AdvancedTable
|
||||
// =========================================================================
|
||||
if (!events.start_editing and !ctx.input.modifiers.ctrl and !ctx.input.modifiers.alt) {
|
||||
if (ctx.input.text_input_len > 0) {
|
||||
const text = ctx.input.text_input[0..ctx.input.text_input_len];
|
||||
|
||||
// Solo búsqueda si la celda actual NO es editable
|
||||
const current_cell_editable = blk: {
|
||||
if (!config.allow_edit) break :blk false;
|
||||
if (table_state.selected_row < 0 or table_state.selected_col < 0) break :blk false;
|
||||
const col_idx: usize = @intCast(table_state.selected_col);
|
||||
if (col_idx >= table_schema.columns.len) break :blk false;
|
||||
break :blk table_schema.columns[col_idx].editable;
|
||||
};
|
||||
|
||||
if (!current_cell_editable) {
|
||||
// Incremental search (type-to-search) in first column
|
||||
for (text) |char| {
|
||||
if (char >= 32 and char < 127) {
|
||||
const search_term = table_state.addSearchChar(char, ctx.current_time_ms);
|
||||
|
||||
if (search_term.len > 0 and table_schema.columns.len > 0) {
|
||||
const first_col_name = table_schema.columns[0].name;
|
||||
const start_row: usize = if (table_state.selected_row >= 0)
|
||||
@intCast(table_state.selected_row)
|
||||
else
|
||||
0;
|
||||
|
||||
var found_row: ?usize = null;
|
||||
|
||||
// Search from current position to end
|
||||
for (start_row..row_count) |row| {
|
||||
if (table_state.getRowConst(row)) |row_data| {
|
||||
const cell_value = row_data.get(first_col_name);
|
||||
var format_buf: [128]u8 = undefined;
|
||||
const cell_text = cell_value.format(&format_buf);
|
||||
if (sorting.startsWithIgnoreCase(cell_text, search_term)) {
|
||||
found_row = row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap to beginning if not found
|
||||
if (found_row == null and start_row > 0) {
|
||||
for (0..start_row) |row| {
|
||||
if (table_state.getRowConst(row)) |row_data| {
|
||||
const cell_value = row_data.get(first_col_name);
|
||||
var format_buf: [128]u8 = undefined;
|
||||
const cell_text = cell_value.format(&format_buf);
|
||||
if (sorting.startsWithIgnoreCase(cell_text, search_term)) {
|
||||
found_row = row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move selection if found
|
||||
if (found_row) |row_idx| {
|
||||
table_state.selectCell(row_idx, @intCast(@max(0, table_state.selected_col)));
|
||||
result.selection_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Editing Keyboard
|
||||
// =============================================================================
|
||||
|
||||
pub fn handleEditingKeyboard(
|
||||
ctx: *Context,
|
||||
table_state: *AdvancedTableState,
|
||||
table_schema: *const TableSchema,
|
||||
result: *AdvancedTableResult,
|
||||
) void {
|
||||
const config = table_schema.config;
|
||||
|
||||
// Obtener texto original para revert
|
||||
var orig_format_buf: [128]u8 = undefined;
|
||||
const original_text: ?[]const u8 = if (table_state.original_value) |orig|
|
||||
orig.format(&orig_format_buf)
|
||||
else
|
||||
null;
|
||||
|
||||
// Usar table_core para procesamiento de teclado (DRY) con soporte selección
|
||||
const kb_result = table_core.handleEditingKeyboard(
|
||||
ctx,
|
||||
&table_state.cell_edit.edit_buffer,
|
||||
&table_state.cell_edit.edit_len,
|
||||
&table_state.cell_edit.edit_cursor,
|
||||
&table_state.cell_edit.escape_count,
|
||||
original_text,
|
||||
&table_state.cell_edit.selection_start,
|
||||
&table_state.cell_edit.selection_end,
|
||||
);
|
||||
|
||||
// Si no se procesó ningún evento, salir
|
||||
if (!kb_result.handled) return;
|
||||
|
||||
// Escape canceló la edición
|
||||
if (kb_result.cancelled) {
|
||||
table_state.stopEditing();
|
||||
result.edit_ended = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Commit (Enter, Tab, flechas) y navegación
|
||||
if (kb_result.committed) {
|
||||
helpers.commitEdit(table_state, table_schema, result);
|
||||
table_state.stopEditing();
|
||||
result.edit_ended = true;
|
||||
|
||||
// Procesar navegación después de commit
|
||||
switch (kb_result.navigate) {
|
||||
.next_cell, .prev_cell => {
|
||||
if (!config.handle_tab) return;
|
||||
|
||||
const col_count = table_schema.columns.len;
|
||||
const row_count = table_state.getRowCount();
|
||||
|
||||
if (kb_result.navigate == .prev_cell) {
|
||||
// Shift+Tab: move left
|
||||
if (table_state.selected_col > 0) {
|
||||
table_state.selectCell(
|
||||
@intCast(@max(0, table_state.selected_row)),
|
||||
@intCast(table_state.selected_col - 1),
|
||||
);
|
||||
} else if (table_state.selected_row > 0 and config.wrap_navigation) {
|
||||
table_state.selectCell(
|
||||
@intCast(table_state.selected_row - 1),
|
||||
col_count - 1,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Tab: move right
|
||||
if (table_state.selected_col < @as(i32, @intCast(col_count)) - 1) {
|
||||
table_state.selectCell(
|
||||
@intCast(@max(0, table_state.selected_row)),
|
||||
@intCast(table_state.selected_col + 1),
|
||||
);
|
||||
} else if (table_state.selected_row < @as(i32, @intCast(row_count)) - 1 and config.wrap_navigation) {
|
||||
table_state.selectCell(
|
||||
@intCast(table_state.selected_row + 1),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-start editing in new cell if editable
|
||||
const new_col: usize = @intCast(@max(0, table_state.selected_col));
|
||||
if (new_col < table_schema.columns.len and table_schema.columns[new_col].editable) {
|
||||
if (table_state.getRow(@intCast(@max(0, table_state.selected_row)))) |row| {
|
||||
const value = row.get(table_schema.columns[new_col].name);
|
||||
var format_buf: [128]u8 = undefined;
|
||||
const text = value.format(&format_buf);
|
||||
table_state.startEditing(text);
|
||||
result.edit_started = true;
|
||||
}
|
||||
}
|
||||
|
||||
result.selection_changed = true;
|
||||
},
|
||||
.next_row, .prev_row => {
|
||||
// Enter o flechas arriba/abajo: solo commit, sin navegación adicional aquí
|
||||
// (La navegación entre filas se maneja en otro lugar si es necesario)
|
||||
},
|
||||
.none => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/widgets/advanced_table/sorting.zig
Normal file
113
src/widgets/advanced_table/sorting.zig
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
//! AdvancedTable - Ordenación y Búsqueda
|
||||
//!
|
||||
//! Funciones de ordenación y búsqueda extraídas del archivo principal.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const types = @import("types.zig");
|
||||
const state = @import("state.zig");
|
||||
|
||||
pub const Row = types.Row;
|
||||
pub const SortDirection = types.SortDirection;
|
||||
pub const AdvancedTableState = state.AdvancedTableState;
|
||||
|
||||
// =============================================================================
|
||||
// Sorting
|
||||
// =============================================================================
|
||||
|
||||
/// Sort rows by column value
|
||||
pub fn sortRows(
|
||||
table_state: *AdvancedTableState,
|
||||
column_name: []const u8,
|
||||
direction: SortDirection,
|
||||
) void {
|
||||
if (direction == .none) return;
|
||||
if (table_state.rows.items.len < 2) return;
|
||||
|
||||
const len = table_state.rows.items.len;
|
||||
var swapped = true;
|
||||
|
||||
while (swapped) {
|
||||
swapped = false;
|
||||
for (0..len - 1) |i| {
|
||||
const val_a = table_state.rows.items[i].get(column_name);
|
||||
const val_b = table_state.rows.items[i + 1].get(column_name);
|
||||
const cmp = val_a.compare(val_b);
|
||||
|
||||
const should_swap = switch (direction) {
|
||||
.ascending => cmp > 0,
|
||||
.descending => cmp < 0,
|
||||
.none => false,
|
||||
};
|
||||
|
||||
if (should_swap) {
|
||||
std.mem.swap(Row, &table_state.rows.items[i], &table_state.rows.items[i + 1]);
|
||||
swapRowStates(table_state, i, i + 1);
|
||||
swapped = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Swap state map entries between two row indices
|
||||
pub fn swapRowStates(table_state: *AdvancedTableState, idx_a: usize, idx_b: usize) void {
|
||||
const swapInMap = struct {
|
||||
fn swap(map: anytype, a: usize, b: usize) void {
|
||||
const val_a = map.get(a);
|
||||
const val_b = map.get(b);
|
||||
|
||||
if (val_a != null and val_b != null) {
|
||||
// Both exist - no change needed
|
||||
} else if (val_a) |v| {
|
||||
_ = map.remove(a);
|
||||
map.put(b, v) catch {};
|
||||
} else if (val_b) |v| {
|
||||
_ = map.remove(b);
|
||||
map.put(a, v) catch {};
|
||||
}
|
||||
}
|
||||
}.swap;
|
||||
|
||||
swapInMap(&table_state.dirty_rows, idx_a, idx_b);
|
||||
swapInMap(&table_state.new_rows, idx_a, idx_b);
|
||||
swapInMap(&table_state.deleted_rows, idx_a, idx_b);
|
||||
swapInMap(&table_state.validation_errors, idx_a, idx_b);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Search Helpers
|
||||
// =============================================================================
|
||||
|
||||
/// Case-insensitive prefix match for incremental search
|
||||
pub fn startsWithIgnoreCase(haystack: []const u8, needle: []const u8) bool {
|
||||
if (needle.len > haystack.len) return false;
|
||||
if (needle.len == 0) return true;
|
||||
|
||||
for (needle, 0..) |needle_char, i| {
|
||||
const haystack_char = haystack[i];
|
||||
const needle_lower = if (needle_char >= 'A' and needle_char <= 'Z')
|
||||
needle_char + 32
|
||||
else
|
||||
needle_char;
|
||||
const haystack_lower = if (haystack_char >= 'A' and haystack_char <= 'Z')
|
||||
haystack_char + 32
|
||||
else
|
||||
haystack_char;
|
||||
|
||||
if (needle_lower != haystack_lower) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
test "startsWithIgnoreCase" {
|
||||
try std.testing.expect(startsWithIgnoreCase("Hello World", "Hello"));
|
||||
try std.testing.expect(startsWithIgnoreCase("Hello World", "hello"));
|
||||
try std.testing.expect(startsWithIgnoreCase("hello world", "HELLO"));
|
||||
try std.testing.expect(startsWithIgnoreCase("anything", ""));
|
||||
try std.testing.expect(!startsWithIgnoreCase("Hello", "World"));
|
||||
try std.testing.expect(!startsWithIgnoreCase("Hi", "Hello World"));
|
||||
}
|
||||
Loading…
Reference in a new issue