diff --git a/src/core/context.zig b/src/core/context.zig index f8de154..f588a79 100644 --- a/src/core/context.zig +++ b/src/core/context.zig @@ -60,6 +60,9 @@ pub const Context = struct { /// Draw commands for current frame commands: std.ArrayListUnmanaged(Command.DrawCommand), + /// Overlay commands (drawn AFTER all regular commands - for dropdowns, tooltips, popups) + overlay_commands: std.ArrayListUnmanaged(Command.DrawCommand), + /// Input state input: Input.InputState, @@ -136,6 +139,7 @@ pub const Context = struct { .allocator = allocator, .frame_arena = try FrameArena.init(allocator), .commands = .{}, + .overlay_commands = .{}, .input = Input.InputState.init(), .layout = Layout.LayoutState.init(width, height), .id_stack = .{}, @@ -155,6 +159,7 @@ pub const Context = struct { .allocator = allocator, .frame_arena = try FrameArena.initWithSize(allocator, arena_size), .commands = .{}, + .overlay_commands = .{}, .input = Input.InputState.init(), .layout = Layout.LayoutState.init(width, height), .id_stack = .{}, @@ -171,6 +176,7 @@ pub const Context = struct { /// Clean up resources pub fn deinit(self: *Self) void { self.commands.deinit(self.allocator); + self.overlay_commands.deinit(self.allocator); self.id_stack.deinit(self.allocator); self.dirty_rects.deinit(self.allocator); self.frame_arena.deinit(); @@ -183,6 +189,7 @@ pub const Context = struct { // Reset per-frame state self.commands.clearRetainingCapacity(); + self.overlay_commands.clearRetainingCapacity(); self.id_stack.clearRetainingCapacity(); self.dirty_rects.clearRetainingCapacity(); self.layout.reset(self.width, self.height); @@ -212,8 +219,8 @@ pub const Context = struct { self.input.endFrame(); - // Update final stats - self.stats.command_count = self.commands.items.len; + // Update final stats (includes overlay commands) + self.stats.command_count = self.commands.items.len + self.overlay_commands.items.len; self.stats.arena_bytes = self.frame_arena.bytesUsed(); self.stats.dirty_rect_count = self.dirty_rects.items.len; @@ -395,6 +402,12 @@ pub const Context = struct { self.commands.append(self.allocator, cmd) catch {}; } + /// Push an overlay command (drawn AFTER all regular commands) + /// Use this for dropdowns, tooltips, popups that need to appear on top + pub fn pushOverlayCommand(self: *Self, cmd: Command.DrawCommand) void { + self.overlay_commands.append(self.allocator, cmd) catch {}; + } + /// Resize the context pub fn resize(self: *Self, width: u32, height: u32) void { self.width = width; diff --git a/src/core/mainloop.zig b/src/core/mainloop.zig index edfa2a0..5b0c5d2 100644 --- a/src/core/mainloop.zig +++ b/src/core/mainloop.zig @@ -214,6 +214,11 @@ pub const MainLoop = struct { // Execute draw commands self.renderer.executeAll(self.ctx.commands.items); + // Execute overlay commands (dropdowns, tooltips, popups - drawn on top) + if (self.ctx.overlay_commands.items.len > 0) { + self.renderer.executeAll(self.ctx.overlay_commands.items); + } + self.ctx.endFrame(); // Present to backend @@ -222,9 +227,10 @@ pub const MainLoop = struct { if (self.config.debug_timing) { const elapsed_ns = std.time.nanoTimestamp() - start_time; const elapsed_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0; + const total_cmds = self.ctx.commands.items.len + self.ctx.overlay_commands.items.len; std.debug.print("Frame {}: {} cmds, {d:.1}ms\n", .{ self.frame_count, - self.ctx.commands.items.len, + total_cmds, elapsed_ms, }); } diff --git a/src/widgets/autocomplete.zig b/src/widgets/autocomplete.zig index a309470..af1a71c 100644 --- a/src/widgets/autocomplete.zig +++ b/src/widgets/autocomplete.zig @@ -297,10 +297,26 @@ pub fn autocompleteRect( const input_hovered = bounds.contains(mouse.x, mouse.y); const input_clicked = input_hovered and ctx.input.mousePressed(.left); + // Calcular área de la flecha para detección de clicks + const arrow_click_width: u32 = 20; // Zona clicable de la flecha + const arrow_area_x = bounds.x + @as(i32, @intCast(bounds.w -| arrow_click_width)); + const arrow_hovered = mouse.x >= arrow_area_x and mouse.x <= bounds.x + @as(i32, @intCast(bounds.w)) and + mouse.y >= bounds.y and mouse.y <= bounds.y + @as(i32, @intCast(bounds.h)); + const arrow_clicked = arrow_hovered and ctx.input.mousePressed(.left); + // Handle click to request focus if (input_clicked and !config.disabled) { ctx.requestFocus(widget_id); - if (config.show_on_focus) { + + // Click en la flecha: toggle dropdown (forzar abrir/cerrar) + if (arrow_clicked) { + if (state.open) { + state.closeDropdown(); + } else { + state.openDropdown(); + } + } else if (config.show_on_focus) { + // Click en el área de texto: abrir si show_on_focus está activo state.openDropdown(); } } @@ -504,9 +520,17 @@ pub fn autocompleteRect( }, .backspace => { state.backspace(); + // Abrir dropdown después de borrar (el usuario está editando) + if (state.len >= config.min_chars) { + state.open = true; + } }, .delete => { state.delete(); + // Abrir dropdown después de borrar + if (state.len >= config.min_chars) { + state.open = true; + } }, .left => { if (!state.open) { @@ -533,17 +557,23 @@ pub fn autocompleteRect( if (text_in.len > 0) { state.insert(text_in); result.text_changed = true; + // IMPORTANTE: Abrir dropdown inmediatamente después de insertar texto + // (no esperar al siguiente frame para detectar el cambio) + if (state.len >= config.min_chars) { + state.open = true; + } } } // Draw dropdown if open and has items + // OVERLAY: El dropdown se dibuja en la capa overlay para aparecer ENCIMA de otros widgets if (state.open and filtered_count > 0) { const visible_items = @min(filtered_count, config.max_visible_items); const dropdown_h = visible_items * config.item_height; const dropdown_y = bounds.y + @as(i32, @intCast(bounds.h)); - // Dropdown background - ctx.pushCommand(Command.rect( + // Dropdown background (overlay) + ctx.pushOverlayCommand(Command.rect( bounds.x, dropdown_y, bounds.w, @@ -551,7 +581,7 @@ pub fn autocompleteRect( colors.dropdown_bg, )); - ctx.pushCommand(Command.rectOutline( + ctx.pushOverlayCommand(Command.rectOutline( bounds.x, dropdown_y, bounds.w, @@ -592,7 +622,7 @@ pub fn autocompleteRect( Style.Color.transparent; if (item_bg.a > 0) { - ctx.pushCommand(Command.rect( + ctx.pushOverlayCommand(Command.rect( item_bounds.x + 1, item_bounds.y, item_bounds.w - 2, @@ -601,11 +631,11 @@ pub fn autocompleteRect( )); } - // Item text + // Item text (overlay) const item_inner = item_bounds.shrink(config.padding); const item_text_y = item_inner.y + @as(i32, @intCast((item_inner.h -| char_height) / 2)); - ctx.pushCommand(Command.text(item_inner.x, item_text_y, options[i], Style.Color.rgb(220, 220, 220))); + ctx.pushOverlayCommand(Command.text(item_inner.x, item_text_y, options[i], Style.Color.rgb(220, 220, 220))); // Handle click selection if (item_clicked) { @@ -633,11 +663,11 @@ pub fn autocompleteRect( } } } else if (state.open and filtered_count == 0 and filter_text.len > 0) { - // Show "no matches" message + // Show "no matches" message (overlay) const no_match_h: u32 = config.item_height; const dropdown_y = bounds.y + @as(i32, @intCast(bounds.h)); - ctx.pushCommand(Command.rect( + ctx.pushOverlayCommand(Command.rect( bounds.x, dropdown_y, bounds.w, @@ -645,7 +675,7 @@ pub fn autocompleteRect( colors.dropdown_bg, )); - ctx.pushCommand(Command.rectOutline( + ctx.pushOverlayCommand(Command.rectOutline( bounds.x, dropdown_y, bounds.w, @@ -655,7 +685,7 @@ pub fn autocompleteRect( const no_match_text = if (config.allow_custom) "Press Enter to use custom value" else "No matches found"; const msg_y = dropdown_y + @as(i32, @intCast((no_match_h -| char_height) / 2)); - ctx.pushCommand(Command.text(bounds.x + @as(i32, @intCast(config.padding)), msg_y, no_match_text, Style.Color.rgb(120, 120, 120))); + ctx.pushOverlayCommand(Command.text(bounds.x + @as(i32, @intCast(config.padding)), msg_y, no_match_text, Style.Color.rgb(120, 120, 120))); // Close if clicked outside if (ctx.input.mousePressed(.left) and !input_hovered) {