feat: zcatgui v0.12.0 - Phase 6 Advanced Input
New Core Modules (4): Clipboard - System clipboard integration - getText()/setText() via SDL2 clipboard API - hasText() and clear() utilities - Cross-platform support DragDrop - Drag and drop system - DragData with typed data transfer - DropZone registration with type filtering - DragDropManager for coordinating operations - Hover detection and drop results - Helper functions: makeDraggable(), makeDropZone() Shortcuts - Keyboard shortcuts system - Shortcut struct with key + modifiers - ShortcutManager for registration and checking - Common shortcuts (Ctrl+C/V/X/Z, etc.) - Human-readable formatting (formatShortcut) - Enable/disable individual shortcuts FocusGroup - Focus group management - FocusGroup for widget tab order - focusNext/Previous with wrap support - FocusGroupManager for multiple groups - Group switching (focusNextGroup) - Tab/Shift+Tab navigation All tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bb5b201203
commit
976d172501
5 changed files with 1276 additions and 0 deletions
103
src/core/clipboard.zig
Normal file
103
src/core/clipboard.zig
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
//! Clipboard - System clipboard integration
|
||||
//!
|
||||
//! Provides clipboard operations for copy/paste functionality.
|
||||
//! Uses SDL2 clipboard API for cross-platform support.
|
||||
|
||||
const std = @import("std");
|
||||
const c = @cImport({
|
||||
@cInclude("SDL2/SDL.h");
|
||||
});
|
||||
|
||||
/// Clipboard error types
|
||||
pub const ClipboardError = error{
|
||||
SDLError,
|
||||
AllocationError,
|
||||
NoText,
|
||||
};
|
||||
|
||||
/// Get text from clipboard
|
||||
/// Caller owns returned memory and must free it
|
||||
pub fn getText(allocator: std.mem.Allocator) ClipboardError!?[]u8 {
|
||||
if (!hasText()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sdl_text = c.SDL_GetClipboardText();
|
||||
if (sdl_text == null) {
|
||||
return ClipboardError.SDLError;
|
||||
}
|
||||
defer c.SDL_free(sdl_text);
|
||||
|
||||
// Get length
|
||||
const len = std.mem.len(sdl_text);
|
||||
if (len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Copy to Zig-managed memory
|
||||
const result = allocator.alloc(u8, len) catch {
|
||||
return ClipboardError.AllocationError;
|
||||
};
|
||||
|
||||
@memcpy(result, sdl_text[0..len]);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Set text to clipboard
|
||||
pub fn setText(text: []const u8) ClipboardError!void {
|
||||
// SDL requires null-terminated string
|
||||
// Create a temporary null-terminated copy
|
||||
var buf: [4096]u8 = undefined;
|
||||
if (text.len >= buf.len) {
|
||||
return ClipboardError.AllocationError;
|
||||
}
|
||||
|
||||
@memcpy(buf[0..text.len], text);
|
||||
buf[text.len] = 0;
|
||||
|
||||
const result = c.SDL_SetClipboardText(&buf);
|
||||
if (result != 0) {
|
||||
return ClipboardError.SDLError;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if clipboard has text
|
||||
pub fn hasText() bool {
|
||||
return c.SDL_HasClipboardText() == c.SDL_TRUE;
|
||||
}
|
||||
|
||||
/// Clear clipboard
|
||||
pub fn clear() void {
|
||||
_ = c.SDL_SetClipboardText("");
|
||||
}
|
||||
|
||||
/// Get clipboard text length without copying
|
||||
pub fn getTextLength() usize {
|
||||
if (!hasText()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const sdl_text = c.SDL_GetClipboardText();
|
||||
if (sdl_text == null) {
|
||||
return 0;
|
||||
}
|
||||
defer c.SDL_free(sdl_text);
|
||||
|
||||
return std.mem.len(sdl_text);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
test "clipboard operations" {
|
||||
// Note: These tests require SDL2 to be initialized
|
||||
// In a real test environment, SDL_Init would be called first
|
||||
|
||||
// Test hasText (should work without SDL init for checking)
|
||||
_ = hasText();
|
||||
}
|
||||
|
||||
test "clipboard text length" {
|
||||
_ = getTextLength();
|
||||
}
|
||||
362
src/core/dragdrop.zig
Normal file
362
src/core/dragdrop.zig
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
//! Drag & Drop System
|
||||
//!
|
||||
//! Provides generic drag and drop functionality for widgets.
|
||||
//! Supports typed data transfer between drag sources and drop zones.
|
||||
|
||||
const std = @import("std");
|
||||
const Layout = @import("layout.zig");
|
||||
const Input = @import("input.zig");
|
||||
|
||||
/// Maximum number of drop zones that can be registered
|
||||
const MAX_DROP_ZONES = 64;
|
||||
|
||||
/// Drag data that can be transferred
|
||||
pub const DragData = struct {
|
||||
/// Unique identifier of the drag source
|
||||
source_id: u64,
|
||||
/// Type identifier for the data (e.g., "file", "item", "node")
|
||||
data_type: []const u8,
|
||||
/// Optional user data pointer
|
||||
user_data: ?*anyopaque = null,
|
||||
/// Display text for drag preview
|
||||
preview_text: ?[]const u8 = null,
|
||||
/// Start position of drag
|
||||
start_x: i32 = 0,
|
||||
start_y: i32 = 0,
|
||||
};
|
||||
|
||||
/// Drop zone configuration
|
||||
pub const DropZone = struct {
|
||||
/// Unique identifier for this zone
|
||||
id: u64,
|
||||
/// Bounds of the drop zone
|
||||
bounds: Layout.Rect,
|
||||
/// Types this zone accepts (empty = accept all)
|
||||
accepts: []const []const u8,
|
||||
/// Highlight when drag hovers over
|
||||
highlight_on_hover: bool = true,
|
||||
/// User data for this zone
|
||||
user_data: ?*anyopaque = null,
|
||||
};
|
||||
|
||||
/// Result of a drop operation
|
||||
pub const DropResult = struct {
|
||||
/// Whether a drop occurred
|
||||
dropped: bool = false,
|
||||
/// The dropped data
|
||||
data: ?DragData = null,
|
||||
/// The zone that received the drop
|
||||
zone_id: u64 = 0,
|
||||
};
|
||||
|
||||
/// Drag & Drop manager
|
||||
pub const DragDropManager = struct {
|
||||
/// Currently dragged data
|
||||
current_drag: ?DragData = null,
|
||||
/// Is drag in progress
|
||||
dragging: bool = false,
|
||||
/// Current mouse position during drag
|
||||
drag_x: i32 = 0,
|
||||
drag_y: i32 = 0,
|
||||
/// Registered drop zones
|
||||
drop_zones: [MAX_DROP_ZONES]DropZone = undefined,
|
||||
/// Number of registered zones
|
||||
zone_count: usize = 0,
|
||||
/// Currently hovered zone index
|
||||
hovered_zone: ?usize = null,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Initialize a new drag drop manager
|
||||
pub fn init() Self {
|
||||
return .{};
|
||||
}
|
||||
|
||||
/// Start a drag operation
|
||||
pub fn startDrag(self: *Self, data: DragData) void {
|
||||
self.current_drag = data;
|
||||
self.dragging = true;
|
||||
self.drag_x = data.start_x;
|
||||
self.drag_y = data.start_y;
|
||||
}
|
||||
|
||||
/// Update drag position
|
||||
pub fn updateDrag(self: *Self, x: i32, y: i32) void {
|
||||
if (!self.dragging) return;
|
||||
|
||||
self.drag_x = x;
|
||||
self.drag_y = y;
|
||||
|
||||
// Update hovered zone
|
||||
self.hovered_zone = null;
|
||||
for (self.drop_zones[0..self.zone_count], 0..) |zone, i| {
|
||||
if (zone.bounds.contains(x, y)) {
|
||||
// Check if zone accepts this data type
|
||||
if (self.current_drag) |drag| {
|
||||
if (self.zoneAccepts(zone, drag.data_type)) {
|
||||
self.hovered_zone = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// End drag and potentially drop
|
||||
pub fn endDrag(self: *Self) DropResult {
|
||||
var result = DropResult{};
|
||||
|
||||
if (self.dragging and self.current_drag != null) {
|
||||
if (self.hovered_zone) |zone_idx| {
|
||||
const zone = self.drop_zones[zone_idx];
|
||||
result.dropped = true;
|
||||
result.data = self.current_drag;
|
||||
result.zone_id = zone.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset state
|
||||
self.current_drag = null;
|
||||
self.dragging = false;
|
||||
self.hovered_zone = null;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Cancel drag without dropping
|
||||
pub fn cancelDrag(self: *Self) void {
|
||||
self.current_drag = null;
|
||||
self.dragging = false;
|
||||
self.hovered_zone = null;
|
||||
}
|
||||
|
||||
/// Check if drag is in progress
|
||||
pub fn isDragging(self: Self) bool {
|
||||
return self.dragging;
|
||||
}
|
||||
|
||||
/// Get current drag data
|
||||
pub fn getDragData(self: Self) ?DragData {
|
||||
return self.current_drag;
|
||||
}
|
||||
|
||||
/// Register a drop zone (valid for one frame)
|
||||
pub fn registerDropZone(self: *Self, zone: DropZone) void {
|
||||
if (self.zone_count >= MAX_DROP_ZONES) return;
|
||||
self.drop_zones[self.zone_count] = zone;
|
||||
self.zone_count += 1;
|
||||
}
|
||||
|
||||
/// Clear all drop zones (call at frame start)
|
||||
pub fn clearDropZones(self: *Self) void {
|
||||
self.zone_count = 0;
|
||||
self.hovered_zone = null;
|
||||
}
|
||||
|
||||
/// Get the currently hovered drop zone
|
||||
pub fn getHoveredZone(self: Self) ?DropZone {
|
||||
if (self.hovered_zone) |idx| {
|
||||
return self.drop_zones[idx];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Check if a zone is being hovered
|
||||
pub fn isZoneHovered(self: Self, zone_id: u64) bool {
|
||||
if (self.hovered_zone) |idx| {
|
||||
return self.drop_zones[idx].id == zone_id;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Check if a zone accepts a data type
|
||||
fn zoneAccepts(self: Self, zone: DropZone, data_type: []const u8) bool {
|
||||
_ = self;
|
||||
// Empty accepts list means accept all
|
||||
if (zone.accepts.len == 0) return true;
|
||||
|
||||
for (zone.accepts) |accepted| {
|
||||
if (std.mem.eql(u8, accepted, data_type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Get drag offset from start position
|
||||
pub fn getDragOffset(self: Self) struct { x: i32, y: i32 } {
|
||||
if (self.current_drag) |drag| {
|
||||
return .{
|
||||
.x = self.drag_x - drag.start_x,
|
||||
.y = self.drag_y - drag.start_y,
|
||||
};
|
||||
}
|
||||
return .{ .x = 0, .y = 0 };
|
||||
}
|
||||
};
|
||||
|
||||
/// Helper to make a widget draggable
|
||||
pub fn makeDraggable(
|
||||
manager: *DragDropManager,
|
||||
input: *const Input.InputState,
|
||||
bounds: Layout.Rect,
|
||||
source_id: u64,
|
||||
data_type: []const u8,
|
||||
) bool {
|
||||
const mouse = input.mousePos();
|
||||
const left_pressed = input.mousePressed(.left);
|
||||
const left_down = input.mouseDown(.left);
|
||||
const left_released = input.mouseReleased(.left);
|
||||
|
||||
// Check if starting drag
|
||||
if (bounds.contains(mouse.x, mouse.y) and left_pressed and !manager.isDragging()) {
|
||||
manager.startDrag(.{
|
||||
.source_id = source_id,
|
||||
.data_type = data_type,
|
||||
.start_x = mouse.x,
|
||||
.start_y = mouse.y,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Update drag position
|
||||
if (manager.isDragging() and left_down) {
|
||||
manager.updateDrag(mouse.x, mouse.y);
|
||||
}
|
||||
|
||||
// End drag on release
|
||||
if (manager.isDragging() and left_released) {
|
||||
_ = manager.endDrag();
|
||||
}
|
||||
|
||||
return manager.isDragging();
|
||||
}
|
||||
|
||||
/// Helper to register a drop zone
|
||||
pub fn makeDropZone(
|
||||
manager: *DragDropManager,
|
||||
bounds: Layout.Rect,
|
||||
zone_id: u64,
|
||||
accepts: []const []const u8,
|
||||
) void {
|
||||
manager.registerDropZone(.{
|
||||
.id = zone_id,
|
||||
.bounds = bounds,
|
||||
.accepts = accepts,
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
test "DragDropManager init" {
|
||||
var manager = DragDropManager.init();
|
||||
try std.testing.expect(!manager.isDragging());
|
||||
try std.testing.expect(manager.getDragData() == null);
|
||||
}
|
||||
|
||||
test "DragDropManager drag lifecycle" {
|
||||
var manager = DragDropManager.init();
|
||||
|
||||
// Start drag
|
||||
manager.startDrag(.{
|
||||
.source_id = 1,
|
||||
.data_type = "item",
|
||||
.start_x = 100,
|
||||
.start_y = 100,
|
||||
});
|
||||
|
||||
try std.testing.expect(manager.isDragging());
|
||||
try std.testing.expect(manager.getDragData() != null);
|
||||
|
||||
// Update position
|
||||
manager.updateDrag(150, 150);
|
||||
try std.testing.expectEqual(@as(i32, 150), manager.drag_x);
|
||||
|
||||
// End drag
|
||||
const result = manager.endDrag();
|
||||
try std.testing.expect(!result.dropped); // No drop zone registered
|
||||
|
||||
try std.testing.expect(!manager.isDragging());
|
||||
}
|
||||
|
||||
test "DragDropManager drop zones" {
|
||||
var manager = DragDropManager.init();
|
||||
|
||||
// Register drop zone
|
||||
manager.registerDropZone(.{
|
||||
.id = 1,
|
||||
.bounds = Layout.Rect.init(0, 0, 100, 100),
|
||||
.accepts = &.{},
|
||||
});
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 1), manager.zone_count);
|
||||
|
||||
// Start drag
|
||||
manager.startDrag(.{
|
||||
.source_id = 1,
|
||||
.data_type = "item",
|
||||
.start_x = 50,
|
||||
.start_y = 50,
|
||||
});
|
||||
|
||||
// Move to drop zone
|
||||
manager.updateDrag(50, 50);
|
||||
try std.testing.expect(manager.hovered_zone != null);
|
||||
|
||||
// Drop
|
||||
const result = manager.endDrag();
|
||||
try std.testing.expect(result.dropped);
|
||||
try std.testing.expectEqual(@as(u64, 1), result.zone_id);
|
||||
}
|
||||
|
||||
test "DragDropManager clear zones" {
|
||||
var manager = DragDropManager.init();
|
||||
|
||||
manager.registerDropZone(.{
|
||||
.id = 1,
|
||||
.bounds = Layout.Rect.init(0, 0, 100, 100),
|
||||
.accepts = &.{},
|
||||
});
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 1), manager.zone_count);
|
||||
|
||||
manager.clearDropZones();
|
||||
try std.testing.expectEqual(@as(usize, 0), manager.zone_count);
|
||||
}
|
||||
|
||||
test "DragDropManager type filtering" {
|
||||
var manager = DragDropManager.init();
|
||||
|
||||
const accepted_types = [_][]const u8{"file"};
|
||||
manager.registerDropZone(.{
|
||||
.id = 1,
|
||||
.bounds = Layout.Rect.init(0, 0, 100, 100),
|
||||
.accepts = &accepted_types,
|
||||
});
|
||||
|
||||
// Drag wrong type
|
||||
manager.startDrag(.{
|
||||
.source_id = 1,
|
||||
.data_type = "item", // Not "file"
|
||||
.start_x = 50,
|
||||
.start_y = 50,
|
||||
});
|
||||
|
||||
manager.updateDrag(50, 50);
|
||||
try std.testing.expect(manager.hovered_zone == null); // Not accepted
|
||||
|
||||
manager.cancelDrag();
|
||||
|
||||
// Drag correct type
|
||||
manager.startDrag(.{
|
||||
.source_id = 1,
|
||||
.data_type = "file",
|
||||
.start_x = 50,
|
||||
.start_y = 50,
|
||||
});
|
||||
|
||||
manager.updateDrag(50, 50);
|
||||
try std.testing.expect(manager.hovered_zone != null); // Accepted
|
||||
}
|
||||
416
src/core/focus_group.zig
Normal file
416
src/core/focus_group.zig
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
//! Focus Groups
|
||||
//!
|
||||
//! Manages focus navigation within groups of widgets.
|
||||
//! Supports tab order, wrapping, and nested groups.
|
||||
|
||||
const std = @import("std");
|
||||
const Input = @import("input.zig");
|
||||
|
||||
/// Maximum widgets per group
|
||||
const MAX_GROUP_SIZE = 64;
|
||||
|
||||
/// Maximum number of groups
|
||||
const MAX_GROUPS = 32;
|
||||
|
||||
/// Focus direction
|
||||
pub const Direction = enum {
|
||||
next,
|
||||
previous,
|
||||
up,
|
||||
down,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
|
||||
/// A focus group containing related widgets
|
||||
pub const FocusGroup = struct {
|
||||
/// Group identifier
|
||||
id: u64,
|
||||
/// Widget IDs in this group (in tab order)
|
||||
widgets: [MAX_GROUP_SIZE]u64 = undefined,
|
||||
/// Number of widgets
|
||||
count: usize = 0,
|
||||
/// Currently focused widget index
|
||||
focused_index: ?usize = null,
|
||||
/// Wrap focus at boundaries
|
||||
wrap: bool = true,
|
||||
/// Is this group active
|
||||
active: bool = true,
|
||||
/// Parent group (for nested focus)
|
||||
parent_group: ?u64 = null,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Create a new focus group
|
||||
pub fn init(id: u64) Self {
|
||||
return .{ .id = id };
|
||||
}
|
||||
|
||||
/// Add a widget to the group
|
||||
pub fn add(self: *Self, widget_id: u64) void {
|
||||
if (self.count >= MAX_GROUP_SIZE) return;
|
||||
self.widgets[self.count] = widget_id;
|
||||
self.count += 1;
|
||||
}
|
||||
|
||||
/// Remove a widget from the group
|
||||
pub fn remove(self: *Self, widget_id: u64) void {
|
||||
var i: usize = 0;
|
||||
while (i < self.count) {
|
||||
if (self.widgets[i] == widget_id) {
|
||||
// Shift remaining widgets
|
||||
var j = i;
|
||||
while (j < self.count - 1) : (j += 1) {
|
||||
self.widgets[j] = self.widgets[j + 1];
|
||||
}
|
||||
self.count -= 1;
|
||||
|
||||
// Adjust focused index
|
||||
if (self.focused_index) |idx| {
|
||||
if (idx == i) {
|
||||
self.focused_index = if (self.count > 0) @min(idx, self.count - 1) else null;
|
||||
} else if (idx > i) {
|
||||
self.focused_index = idx - 1;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the currently focused widget ID
|
||||
pub fn getFocused(self: Self) ?u64 {
|
||||
if (self.focused_index) |idx| {
|
||||
if (idx < self.count) {
|
||||
return self.widgets[idx];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Set focus to a specific widget
|
||||
pub fn setFocus(self: *Self, widget_id: u64) bool {
|
||||
for (self.widgets[0..self.count], 0..) |id, i| {
|
||||
if (id == widget_id) {
|
||||
self.focused_index = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Focus the next widget
|
||||
pub fn focusNext(self: *Self) ?u64 {
|
||||
if (self.count == 0) return null;
|
||||
|
||||
if (self.focused_index) |idx| {
|
||||
if (idx + 1 < self.count) {
|
||||
self.focused_index = idx + 1;
|
||||
} else if (self.wrap) {
|
||||
self.focused_index = 0;
|
||||
}
|
||||
} else {
|
||||
self.focused_index = 0;
|
||||
}
|
||||
|
||||
return self.getFocused();
|
||||
}
|
||||
|
||||
/// Focus the previous widget
|
||||
pub fn focusPrevious(self: *Self) ?u64 {
|
||||
if (self.count == 0) return null;
|
||||
|
||||
if (self.focused_index) |idx| {
|
||||
if (idx > 0) {
|
||||
self.focused_index = idx - 1;
|
||||
} else if (self.wrap) {
|
||||
self.focused_index = self.count - 1;
|
||||
}
|
||||
} else {
|
||||
self.focused_index = self.count - 1;
|
||||
}
|
||||
|
||||
return self.getFocused();
|
||||
}
|
||||
|
||||
/// Focus first widget
|
||||
pub fn focusFirst(self: *Self) ?u64 {
|
||||
if (self.count == 0) return null;
|
||||
self.focused_index = 0;
|
||||
return self.getFocused();
|
||||
}
|
||||
|
||||
/// Focus last widget
|
||||
pub fn focusLast(self: *Self) ?u64 {
|
||||
if (self.count == 0) return null;
|
||||
self.focused_index = self.count - 1;
|
||||
return self.getFocused();
|
||||
}
|
||||
|
||||
/// Clear focus
|
||||
pub fn clearFocus(self: *Self) void {
|
||||
self.focused_index = null;
|
||||
}
|
||||
|
||||
/// Check if a widget has focus
|
||||
pub fn hasFocus(self: Self, widget_id: u64) bool {
|
||||
if (self.getFocused()) |focused| {
|
||||
return focused == widget_id;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Get index of a widget
|
||||
pub fn indexOf(self: Self, widget_id: u64) ?usize {
|
||||
for (self.widgets[0..self.count], 0..) |id, i| {
|
||||
if (id == widget_id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Focus Group Manager - manages multiple focus groups
|
||||
pub const FocusGroupManager = struct {
|
||||
groups: [MAX_GROUPS]FocusGroup = undefined,
|
||||
group_count: usize = 0,
|
||||
active_group: ?u64 = null,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Initialize the manager
|
||||
pub fn init() Self {
|
||||
return .{};
|
||||
}
|
||||
|
||||
/// Create a new group
|
||||
pub fn createGroup(self: *Self, id: u64) *FocusGroup {
|
||||
if (self.group_count >= MAX_GROUPS) {
|
||||
// Return first group as fallback
|
||||
return &self.groups[0];
|
||||
}
|
||||
|
||||
self.groups[self.group_count] = FocusGroup.init(id);
|
||||
const group = &self.groups[self.group_count];
|
||||
self.group_count += 1;
|
||||
|
||||
// Set as active if first group
|
||||
if (self.active_group == null) {
|
||||
self.active_group = id;
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
/// Get a group by ID
|
||||
pub fn getGroup(self: *Self, id: u64) ?*FocusGroup {
|
||||
for (self.groups[0..self.group_count]) |*group| {
|
||||
if (group.id == id) {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Remove a group
|
||||
pub fn removeGroup(self: *Self, id: u64) void {
|
||||
var i: usize = 0;
|
||||
while (i < self.group_count) {
|
||||
if (self.groups[i].id == id) {
|
||||
var j = i;
|
||||
while (j < self.group_count - 1) : (j += 1) {
|
||||
self.groups[j] = self.groups[j + 1];
|
||||
}
|
||||
self.group_count -= 1;
|
||||
|
||||
if (self.active_group == id) {
|
||||
self.active_group = if (self.group_count > 0) self.groups[0].id else null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the active group
|
||||
pub fn setActiveGroup(self: *Self, id: u64) void {
|
||||
if (self.getGroup(id) != null) {
|
||||
self.active_group = id;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the active group
|
||||
pub fn getActiveGroup(self: *Self) ?*FocusGroup {
|
||||
if (self.active_group) |id| {
|
||||
return self.getGroup(id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Handle focus navigation input
|
||||
pub fn handleInput(self: *Self, input: *const Input.InputState) ?u64 {
|
||||
const group = self.getActiveGroup() orelse return null;
|
||||
|
||||
if (input.keyPressed(.tab)) {
|
||||
if (input.keyDown(.left_shift) or input.keyDown(.right_shift)) {
|
||||
return group.focusPrevious();
|
||||
} else {
|
||||
return group.focusNext();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get currently focused widget across all groups
|
||||
pub fn getGlobalFocus(self: Self) ?u64 {
|
||||
if (self.active_group) |active_id| {
|
||||
for (self.groups[0..self.group_count]) |group| {
|
||||
if (group.id == active_id) {
|
||||
return group.getFocused();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Focus next group
|
||||
pub fn focusNextGroup(self: *Self) void {
|
||||
if (self.group_count <= 1) return;
|
||||
|
||||
if (self.active_group) |active_id| {
|
||||
for (self.groups[0..self.group_count], 0..) |group, i| {
|
||||
if (group.id == active_id) {
|
||||
const next_idx = (i + 1) % self.group_count;
|
||||
self.active_group = self.groups[next_idx].id;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Focus previous group
|
||||
pub fn focusPreviousGroup(self: *Self) void {
|
||||
if (self.group_count <= 1) return;
|
||||
|
||||
if (self.active_group) |active_id| {
|
||||
for (self.groups[0..self.group_count], 0..) |group, i| {
|
||||
if (group.id == active_id) {
|
||||
const prev_idx = if (i == 0) self.group_count - 1 else i - 1;
|
||||
self.active_group = self.groups[prev_idx].id;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
test "FocusGroup init" {
|
||||
const group = FocusGroup.init(1);
|
||||
try std.testing.expectEqual(@as(u64, 1), group.id);
|
||||
try std.testing.expectEqual(@as(usize, 0), group.count);
|
||||
}
|
||||
|
||||
test "FocusGroup add and remove" {
|
||||
var group = FocusGroup.init(1);
|
||||
|
||||
group.add(100);
|
||||
group.add(200);
|
||||
group.add(300);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 3), group.count);
|
||||
|
||||
group.remove(200);
|
||||
try std.testing.expectEqual(@as(usize, 2), group.count);
|
||||
try std.testing.expectEqual(@as(u64, 100), group.widgets[0]);
|
||||
try std.testing.expectEqual(@as(u64, 300), group.widgets[1]);
|
||||
}
|
||||
|
||||
test "FocusGroup navigation" {
|
||||
var group = FocusGroup.init(1);
|
||||
group.add(100);
|
||||
group.add(200);
|
||||
group.add(300);
|
||||
|
||||
// Focus first
|
||||
try std.testing.expectEqual(@as(?u64, 100), group.focusFirst());
|
||||
try std.testing.expectEqual(@as(?usize, 0), group.focused_index);
|
||||
|
||||
// Focus next
|
||||
try std.testing.expectEqual(@as(?u64, 200), group.focusNext());
|
||||
try std.testing.expectEqual(@as(?u64, 300), group.focusNext());
|
||||
|
||||
// Wrap around
|
||||
try std.testing.expectEqual(@as(?u64, 100), group.focusNext());
|
||||
|
||||
// Focus previous
|
||||
try std.testing.expectEqual(@as(?u64, 300), group.focusPrevious());
|
||||
}
|
||||
|
||||
test "FocusGroup setFocus" {
|
||||
var group = FocusGroup.init(1);
|
||||
group.add(100);
|
||||
group.add(200);
|
||||
group.add(300);
|
||||
|
||||
try std.testing.expect(group.setFocus(200));
|
||||
try std.testing.expectEqual(@as(?u64, 200), group.getFocused());
|
||||
|
||||
try std.testing.expect(!group.setFocus(999)); // Non-existent
|
||||
}
|
||||
|
||||
test "FocusGroup hasFocus" {
|
||||
var group = FocusGroup.init(1);
|
||||
group.add(100);
|
||||
group.add(200);
|
||||
_ = group.focusFirst();
|
||||
|
||||
try std.testing.expect(group.hasFocus(100));
|
||||
try std.testing.expect(!group.hasFocus(200));
|
||||
}
|
||||
|
||||
test "FocusGroupManager create groups" {
|
||||
var manager = FocusGroupManager.init();
|
||||
|
||||
const group1 = manager.createGroup(1);
|
||||
const group2 = manager.createGroup(2);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 2), manager.group_count);
|
||||
try std.testing.expectEqual(@as(u64, 1), group1.id);
|
||||
try std.testing.expectEqual(@as(u64, 2), group2.id);
|
||||
|
||||
// First group should be active
|
||||
try std.testing.expectEqual(@as(?u64, 1), manager.active_group);
|
||||
}
|
||||
|
||||
test "FocusGroupManager get and remove" {
|
||||
var manager = FocusGroupManager.init();
|
||||
_ = manager.createGroup(1);
|
||||
_ = manager.createGroup(2);
|
||||
|
||||
const group = manager.getGroup(2);
|
||||
try std.testing.expect(group != null);
|
||||
try std.testing.expectEqual(@as(u64, 2), group.?.id);
|
||||
|
||||
manager.removeGroup(1);
|
||||
try std.testing.expectEqual(@as(usize, 1), manager.group_count);
|
||||
try std.testing.expect(manager.getGroup(1) == null);
|
||||
}
|
||||
|
||||
test "FocusGroupManager active group" {
|
||||
var manager = FocusGroupManager.init();
|
||||
_ = manager.createGroup(1);
|
||||
_ = manager.createGroup(2);
|
||||
|
||||
manager.setActiveGroup(2);
|
||||
try std.testing.expectEqual(@as(?u64, 2), manager.active_group);
|
||||
|
||||
manager.focusNextGroup();
|
||||
try std.testing.expectEqual(@as(?u64, 1), manager.active_group);
|
||||
}
|
||||
383
src/core/shortcuts.zig
Normal file
383
src/core/shortcuts.zig
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
//! Keyboard Shortcuts System
|
||||
//!
|
||||
//! Manages application-wide keyboard shortcuts with modifier support.
|
||||
//! Provides human-readable shortcut text (e.g., "Ctrl+S").
|
||||
|
||||
const std = @import("std");
|
||||
const Input = @import("input.zig");
|
||||
|
||||
/// Maximum shortcuts that can be registered
|
||||
const MAX_SHORTCUTS = 128;
|
||||
|
||||
/// Key modifiers
|
||||
pub const Modifiers = packed struct {
|
||||
ctrl: bool = false,
|
||||
shift: bool = false,
|
||||
alt: bool = false,
|
||||
super: bool = false, // Windows/Command key
|
||||
|
||||
pub fn none() Modifiers {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn ctrl_only() Modifiers {
|
||||
return .{ .ctrl = true };
|
||||
}
|
||||
|
||||
pub fn ctrl_shift() Modifiers {
|
||||
return .{ .ctrl = true, .shift = true };
|
||||
}
|
||||
|
||||
pub fn alt_only() Modifiers {
|
||||
return .{ .alt = true };
|
||||
}
|
||||
|
||||
pub fn eql(self: Modifiers, other: Modifiers) bool {
|
||||
return self.ctrl == other.ctrl and
|
||||
self.shift == other.shift and
|
||||
self.alt == other.alt and
|
||||
self.super == other.super;
|
||||
}
|
||||
|
||||
pub fn fromInput(input: *const Input.InputState) Modifiers {
|
||||
return .{
|
||||
.ctrl = input.keyDown(.left_ctrl) or input.keyDown(.right_ctrl),
|
||||
.shift = input.keyDown(.left_shift) or input.keyDown(.right_shift),
|
||||
.alt = input.keyDown(.left_alt) or input.keyDown(.right_alt),
|
||||
.super = input.keyDown(.left_super) or input.keyDown(.right_super),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// A keyboard shortcut definition
|
||||
pub const Shortcut = struct {
|
||||
/// The key code
|
||||
key: Input.Key,
|
||||
/// Required modifiers
|
||||
modifiers: Modifiers,
|
||||
/// Action identifier
|
||||
action: []const u8,
|
||||
/// Human-readable description
|
||||
description: []const u8 = "",
|
||||
/// Is this shortcut enabled
|
||||
enabled: bool = true,
|
||||
|
||||
/// Create a shortcut with Ctrl modifier
|
||||
pub fn ctrl(key: Input.Key, action: []const u8) Shortcut {
|
||||
return .{
|
||||
.key = key,
|
||||
.modifiers = Modifiers.ctrl_only(),
|
||||
.action = action,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a shortcut with Ctrl+Shift modifiers
|
||||
pub fn ctrlShift(key: Input.Key, action: []const u8) Shortcut {
|
||||
return .{
|
||||
.key = key,
|
||||
.modifiers = Modifiers.ctrl_shift(),
|
||||
.action = action,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a shortcut with Alt modifier
|
||||
pub fn alt(key: Input.Key, action: []const u8) Shortcut {
|
||||
return .{
|
||||
.key = key,
|
||||
.modifiers = Modifiers.alt_only(),
|
||||
.action = action,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a shortcut with no modifiers
|
||||
pub fn key_only(key: Input.Key, action: []const u8) Shortcut {
|
||||
return .{
|
||||
.key = key,
|
||||
.modifiers = Modifiers.none(),
|
||||
.action = action,
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if this shortcut matches the given key and modifiers
|
||||
pub fn matches(self: Shortcut, key: Input.Key, mods: Modifiers) bool {
|
||||
return self.enabled and self.key == key and self.modifiers.eql(mods);
|
||||
}
|
||||
};
|
||||
|
||||
/// Shortcut manager for registering and checking shortcuts
|
||||
pub const ShortcutManager = struct {
|
||||
shortcuts: [MAX_SHORTCUTS]Shortcut = undefined,
|
||||
count: usize = 0,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Initialize a new shortcut manager
|
||||
pub fn init() Self {
|
||||
return .{};
|
||||
}
|
||||
|
||||
/// Register a shortcut
|
||||
pub fn register(self: *Self, shortcut: Shortcut) void {
|
||||
if (self.count >= MAX_SHORTCUTS) return;
|
||||
self.shortcuts[self.count] = shortcut;
|
||||
self.count += 1;
|
||||
}
|
||||
|
||||
/// Register common shortcuts (Ctrl+C, Ctrl+V, etc.)
|
||||
pub fn registerCommon(self: *Self) void {
|
||||
self.register(Shortcut.ctrl(.c, "copy"));
|
||||
self.register(Shortcut.ctrl(.v, "paste"));
|
||||
self.register(Shortcut.ctrl(.x, "cut"));
|
||||
self.register(Shortcut.ctrl(.z, "undo"));
|
||||
self.register(Shortcut.ctrlShift(.z, "redo"));
|
||||
self.register(Shortcut.ctrl(.y, "redo"));
|
||||
self.register(Shortcut.ctrl(.s, "save"));
|
||||
self.register(Shortcut.ctrl(.a, "select_all"));
|
||||
self.register(Shortcut.ctrl(.n, "new"));
|
||||
self.register(Shortcut.ctrl(.o, "open"));
|
||||
self.register(Shortcut.ctrl(.w, "close"));
|
||||
self.register(Shortcut.ctrl(.q, "quit"));
|
||||
self.register(Shortcut.ctrl(.f, "find"));
|
||||
self.register(Shortcut.key_only(.f1, "help"));
|
||||
self.register(Shortcut.key_only(.escape, "cancel"));
|
||||
}
|
||||
|
||||
/// Unregister a shortcut by action
|
||||
pub fn unregister(self: *Self, action: []const u8) void {
|
||||
var i: usize = 0;
|
||||
while (i < self.count) {
|
||||
if (std.mem.eql(u8, self.shortcuts[i].action, action)) {
|
||||
// Shift remaining shortcuts down
|
||||
var j = i;
|
||||
while (j < self.count - 1) : (j += 1) {
|
||||
self.shortcuts[j] = self.shortcuts[j + 1];
|
||||
}
|
||||
self.count -= 1;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable/disable a shortcut by action
|
||||
pub fn setEnabled(self: *Self, action: []const u8, enabled: bool) void {
|
||||
for (self.shortcuts[0..self.count]) |*shortcut| {
|
||||
if (std.mem.eql(u8, shortcut.action, action)) {
|
||||
shortcut.enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if any shortcut was triggered
|
||||
/// Returns the action string if a shortcut matched, null otherwise
|
||||
pub fn check(self: *Self, input: *const Input.InputState) ?[]const u8 {
|
||||
const mods = Modifiers.fromInput(input);
|
||||
|
||||
for (self.shortcuts[0..self.count]) |shortcut| {
|
||||
if (input.keyPressed(shortcut.key) and shortcut.matches(shortcut.key, mods)) {
|
||||
return shortcut.action;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get shortcut by action
|
||||
pub fn getByAction(self: Self, action: []const u8) ?Shortcut {
|
||||
for (self.shortcuts[0..self.count]) |shortcut| {
|
||||
if (std.mem.eql(u8, shortcut.action, action)) {
|
||||
return shortcut;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get human-readable text for a shortcut (e.g., "Ctrl+S")
|
||||
pub fn getShortcutText(self: Self, buf: []u8, action: []const u8) []const u8 {
|
||||
if (self.getByAction(action)) |shortcut| {
|
||||
return formatShortcut(buf, shortcut);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/// Clear all shortcuts
|
||||
pub fn clear(self: *Self) void {
|
||||
self.count = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Format a shortcut as human-readable text
|
||||
pub fn formatShortcut(buf: []u8, shortcut: Shortcut) []const u8 {
|
||||
var stream = std.io.fixedBufferStream(buf);
|
||||
const writer = stream.writer();
|
||||
|
||||
if (shortcut.modifiers.ctrl) {
|
||||
writer.writeAll("Ctrl+") catch return "";
|
||||
}
|
||||
if (shortcut.modifiers.alt) {
|
||||
writer.writeAll("Alt+") catch return "";
|
||||
}
|
||||
if (shortcut.modifiers.shift) {
|
||||
writer.writeAll("Shift+") catch return "";
|
||||
}
|
||||
if (shortcut.modifiers.super) {
|
||||
writer.writeAll("Super+") catch return "";
|
||||
}
|
||||
|
||||
const key_name = keyName(shortcut.key);
|
||||
writer.writeAll(key_name) catch return "";
|
||||
|
||||
return buf[0..stream.pos];
|
||||
}
|
||||
|
||||
/// Get human-readable name for a key
|
||||
pub fn keyName(key: Input.Key) []const u8 {
|
||||
return switch (key) {
|
||||
.a => "A",
|
||||
.b => "B",
|
||||
.c => "C",
|
||||
.d => "D",
|
||||
.e => "E",
|
||||
.f => "F",
|
||||
.g => "G",
|
||||
.h => "H",
|
||||
.i => "I",
|
||||
.j => "J",
|
||||
.k => "K",
|
||||
.l => "L",
|
||||
.m => "M",
|
||||
.n => "N",
|
||||
.o => "O",
|
||||
.p => "P",
|
||||
.q => "Q",
|
||||
.r => "R",
|
||||
.s => "S",
|
||||
.t => "T",
|
||||
.u => "U",
|
||||
.v => "V",
|
||||
.w => "W",
|
||||
.x => "X",
|
||||
.y => "Y",
|
||||
.z => "Z",
|
||||
.@"0" => "0",
|
||||
.@"1" => "1",
|
||||
.@"2" => "2",
|
||||
.@"3" => "3",
|
||||
.@"4" => "4",
|
||||
.@"5" => "5",
|
||||
.@"6" => "6",
|
||||
.@"7" => "7",
|
||||
.@"8" => "8",
|
||||
.@"9" => "9",
|
||||
.f1 => "F1",
|
||||
.f2 => "F2",
|
||||
.f3 => "F3",
|
||||
.f4 => "F4",
|
||||
.f5 => "F5",
|
||||
.f6 => "F6",
|
||||
.f7 => "F7",
|
||||
.f8 => "F8",
|
||||
.f9 => "F9",
|
||||
.f10 => "F10",
|
||||
.f11 => "F11",
|
||||
.f12 => "F12",
|
||||
.escape => "Esc",
|
||||
.enter => "Enter",
|
||||
.tab => "Tab",
|
||||
.backspace => "Backspace",
|
||||
.insert => "Insert",
|
||||
.delete => "Delete",
|
||||
.home => "Home",
|
||||
.end => "End",
|
||||
.page_up => "PageUp",
|
||||
.page_down => "PageDown",
|
||||
.up => "Up",
|
||||
.down => "Down",
|
||||
.left => "Left",
|
||||
.right => "Right",
|
||||
.space => "Space",
|
||||
else => "?",
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
test "Modifiers equality" {
|
||||
const m1 = Modifiers{ .ctrl = true };
|
||||
const m2 = Modifiers{ .ctrl = true };
|
||||
const m3 = Modifiers{ .shift = true };
|
||||
|
||||
try std.testing.expect(m1.eql(m2));
|
||||
try std.testing.expect(!m1.eql(m3));
|
||||
}
|
||||
|
||||
test "Shortcut creation" {
|
||||
const shortcut = Shortcut.ctrl(.s, "save");
|
||||
try std.testing.expectEqual(Input.Key.s, shortcut.key);
|
||||
try std.testing.expect(shortcut.modifiers.ctrl);
|
||||
try std.testing.expectEqualStrings("save", shortcut.action);
|
||||
}
|
||||
|
||||
test "Shortcut matches" {
|
||||
const shortcut = Shortcut.ctrl(.s, "save");
|
||||
try std.testing.expect(shortcut.matches(.s, .{ .ctrl = true }));
|
||||
try std.testing.expect(!shortcut.matches(.s, .{ .shift = true }));
|
||||
try std.testing.expect(!shortcut.matches(.a, .{ .ctrl = true }));
|
||||
}
|
||||
|
||||
test "ShortcutManager register" {
|
||||
var manager = ShortcutManager.init();
|
||||
|
||||
manager.register(Shortcut.ctrl(.s, "save"));
|
||||
manager.register(Shortcut.ctrl(.z, "undo"));
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 2), manager.count);
|
||||
}
|
||||
|
||||
test "ShortcutManager getByAction" {
|
||||
var manager = ShortcutManager.init();
|
||||
manager.register(Shortcut.ctrl(.s, "save"));
|
||||
|
||||
const shortcut = manager.getByAction("save");
|
||||
try std.testing.expect(shortcut != null);
|
||||
try std.testing.expectEqual(Input.Key.s, shortcut.?.key);
|
||||
|
||||
const missing = manager.getByAction("nonexistent");
|
||||
try std.testing.expect(missing == null);
|
||||
}
|
||||
|
||||
test "ShortcutManager unregister" {
|
||||
var manager = ShortcutManager.init();
|
||||
manager.register(Shortcut.ctrl(.s, "save"));
|
||||
manager.register(Shortcut.ctrl(.z, "undo"));
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 2), manager.count);
|
||||
|
||||
manager.unregister("save");
|
||||
try std.testing.expectEqual(@as(usize, 1), manager.count);
|
||||
try std.testing.expect(manager.getByAction("save") == null);
|
||||
try std.testing.expect(manager.getByAction("undo") != null);
|
||||
}
|
||||
|
||||
test "formatShortcut" {
|
||||
var buf: [32]u8 = undefined;
|
||||
|
||||
const ctrl_s = Shortcut.ctrl(.s, "save");
|
||||
const text1 = formatShortcut(&buf, ctrl_s);
|
||||
try std.testing.expectEqualStrings("Ctrl+S", text1);
|
||||
|
||||
const ctrl_shift_z = Shortcut.ctrlShift(.z, "redo");
|
||||
const text2 = formatShortcut(&buf, ctrl_shift_z);
|
||||
try std.testing.expectEqualStrings("Ctrl+Shift+Z", text2);
|
||||
}
|
||||
|
||||
test "registerCommon" {
|
||||
var manager = ShortcutManager.init();
|
||||
manager.registerCommon();
|
||||
|
||||
try std.testing.expect(manager.count >= 10);
|
||||
try std.testing.expect(manager.getByAction("copy") != null);
|
||||
try std.testing.expect(manager.getByAction("paste") != null);
|
||||
try std.testing.expect(manager.getByAction("undo") != null);
|
||||
}
|
||||
|
|
@ -38,6 +38,18 @@ pub const Layout = @import("core/layout.zig");
|
|||
pub const Style = @import("core/style.zig");
|
||||
pub const Input = @import("core/input.zig");
|
||||
pub const Command = @import("core/command.zig");
|
||||
pub const clipboard = @import("core/clipboard.zig");
|
||||
pub const dragdrop = @import("core/dragdrop.zig");
|
||||
pub const DragDropManager = dragdrop.DragDropManager;
|
||||
pub const DragData = dragdrop.DragData;
|
||||
pub const DropZone = dragdrop.DropZone;
|
||||
pub const DropResult = dragdrop.DropResult;
|
||||
pub const shortcuts = @import("core/shortcuts.zig");
|
||||
pub const ShortcutManager = shortcuts.ShortcutManager;
|
||||
pub const Shortcut = shortcuts.Shortcut;
|
||||
pub const focus_group = @import("core/focus_group.zig");
|
||||
pub const FocusGroup = focus_group.FocusGroup;
|
||||
pub const FocusGroupManager = focus_group.FocusGroupManager;
|
||||
|
||||
// =============================================================================
|
||||
// Macro system
|
||||
|
|
|
|||
Loading…
Reference in a new issue