Compare commits
No commits in common. "74e83d2334dc436b7b6dd23e1ce62323826c1e07" and "ebad736c756afee44e51255728ae9632ec65f3da" have entirely different histories.
74e83d2334
...
ebad736c75
3 changed files with 4 additions and 265 deletions
|
|
@ -586,117 +586,3 @@ test "Spring snap" {
|
||||||
try std.testing.expectEqual(@as(f32, 100.0), spring.position);
|
try std.testing.expectEqual(@as(f32, 100.0), spring.position);
|
||||||
try std.testing.expectEqual(@as(f32, 0.0), spring.velocity);
|
try std.testing.expectEqual(@as(f32, 0.0), spring.velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Hover Transition (for widget state)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Simple hover transition for widgets.
|
|
||||||
/// Embed in widget state structs for smooth hover/press effects.
|
|
||||||
///
|
|
||||||
/// Usage:
|
|
||||||
/// ```zig
|
|
||||||
/// pub const ButtonState = struct {
|
|
||||||
/// hover: HoverTransition = .{},
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// // In widget draw:
|
|
||||||
/// state.hover.update(is_hovered, dt_ms);
|
|
||||||
/// const bg_color = state.hover.blend(normal_color, hover_color);
|
|
||||||
/// ```
|
|
||||||
pub const HoverTransition = struct {
|
|
||||||
/// Current transition value (0.0 = normal, 1.0 = fully hovered)
|
|
||||||
value: f32 = 0.0,
|
|
||||||
/// Transition speed (value change per millisecond)
|
|
||||||
speed: f32 = 0.008,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
/// Update transition towards target state
|
|
||||||
pub fn update(self: *Self, hovered: bool, dt_ms: u64) void {
|
|
||||||
const target: f32 = if (hovered) 1.0 else 0.0;
|
|
||||||
const delta = @as(f32, @floatFromInt(dt_ms)) * self.speed;
|
|
||||||
|
|
||||||
if (self.value < target) {
|
|
||||||
self.value = @min(target, self.value + delta);
|
|
||||||
} else if (self.value > target) {
|
|
||||||
self.value = @max(target, self.value - delta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update with press state (0=normal, 0.5=hover, 1.0=pressed)
|
|
||||||
pub fn updateWithPress(self: *Self, hovered: bool, pressed: bool, dt_ms: u64) void {
|
|
||||||
const target: f32 = if (pressed) 1.0 else if (hovered) 0.5 else 0.0;
|
|
||||||
const delta = @as(f32, @floatFromInt(dt_ms)) * self.speed * 1.5; // Press is faster
|
|
||||||
|
|
||||||
if (self.value < target) {
|
|
||||||
self.value = @min(target, self.value + delta);
|
|
||||||
} else if (self.value > target) {
|
|
||||||
self.value = @max(target, self.value - delta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Blend between two colors based on transition value
|
|
||||||
pub fn blend(self: Self, normal: anytype, target: anytype) @TypeOf(normal) {
|
|
||||||
const Color = @TypeOf(normal);
|
|
||||||
return Color{
|
|
||||||
.r = @intFromFloat(lerp(@floatFromInt(normal.r), @floatFromInt(target.r), self.value)),
|
|
||||||
.g = @intFromFloat(lerp(@floatFromInt(normal.g), @floatFromInt(target.g), self.value)),
|
|
||||||
.b = @intFromFloat(lerp(@floatFromInt(normal.b), @floatFromInt(target.b), self.value)),
|
|
||||||
.a = @intFromFloat(lerp(@floatFromInt(normal.a), @floatFromInt(target.a), self.value)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Blend between three states: normal (0), hover (0.5), pressed (1.0)
|
|
||||||
pub fn blendThree(self: Self, normal: anytype, hover: anytype, pressed: anytype) @TypeOf(normal) {
|
|
||||||
const Color = @TypeOf(normal);
|
|
||||||
if (self.value <= 0.5) {
|
|
||||||
// Blend normal -> hover (t maps 0-0.5 to 0-1)
|
|
||||||
const t = self.value * 2.0;
|
|
||||||
return Color{
|
|
||||||
.r = @intFromFloat(lerp(@floatFromInt(normal.r), @floatFromInt(hover.r), t)),
|
|
||||||
.g = @intFromFloat(lerp(@floatFromInt(normal.g), @floatFromInt(hover.g), t)),
|
|
||||||
.b = @intFromFloat(lerp(@floatFromInt(normal.b), @floatFromInt(hover.b), t)),
|
|
||||||
.a = @intFromFloat(lerp(@floatFromInt(normal.a), @floatFromInt(hover.a), t)),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Blend hover -> pressed (t maps 0.5-1.0 to 0-1)
|
|
||||||
const t = (self.value - 0.5) * 2.0;
|
|
||||||
return Color{
|
|
||||||
.r = @intFromFloat(lerp(@floatFromInt(hover.r), @floatFromInt(pressed.r), t)),
|
|
||||||
.g = @intFromFloat(lerp(@floatFromInt(hover.g), @floatFromInt(pressed.g), t)),
|
|
||||||
.b = @intFromFloat(lerp(@floatFromInt(hover.b), @floatFromInt(pressed.b), t)),
|
|
||||||
.a = @intFromFloat(lerp(@floatFromInt(hover.a), @floatFromInt(pressed.a), t)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if transition is complete (at target)
|
|
||||||
pub fn isSettled(self: Self, hovered: bool) bool {
|
|
||||||
const target: f32 = if (hovered) 1.0 else 0.0;
|
|
||||||
return @abs(self.value - target) < 0.001;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "HoverTransition basic" {
|
|
||||||
var hover = HoverTransition{};
|
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(f32, 0.0), hover.value);
|
|
||||||
|
|
||||||
// Simulate hovering for 125ms (should reach 1.0)
|
|
||||||
hover.update(true, 125);
|
|
||||||
try std.testing.expectEqual(@as(f32, 1.0), hover.value);
|
|
||||||
|
|
||||||
// Simulate un-hovering for 125ms (should reach 0.0)
|
|
||||||
hover.update(false, 125);
|
|
||||||
try std.testing.expectEqual(@as(f32, 0.0), hover.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "HoverTransition gradual" {
|
|
||||||
var hover = HoverTransition{};
|
|
||||||
|
|
||||||
// 50ms should be partial
|
|
||||||
hover.update(true, 50);
|
|
||||||
try std.testing.expect(hover.value > 0.0);
|
|
||||||
try std.testing.expect(hover.value < 1.0);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
//!
|
//!
|
||||||
//! An immediate mode button that returns true when clicked.
|
//! An immediate mode button that returns true when clicked.
|
||||||
//! Supports hover/active states and keyboard activation.
|
//! Supports hover/active states and keyboard activation.
|
||||||
//!
|
|
||||||
//! Two modes:
|
|
||||||
//! - Stateless: `button()`, `buttonEx()` - instant color changes
|
|
||||||
//! - Stateful: `buttonStateful()` - smooth hover/press transitions
|
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Context = @import("../core/context.zig").Context;
|
const Context = @import("../core/context.zig").Context;
|
||||||
|
|
@ -13,7 +9,6 @@ const Command = @import("../core/command.zig");
|
||||||
const Layout = @import("../core/layout.zig");
|
const Layout = @import("../core/layout.zig");
|
||||||
const Style = @import("../core/style.zig");
|
const Style = @import("../core/style.zig");
|
||||||
const Input = @import("../core/input.zig");
|
const Input = @import("../core/input.zig");
|
||||||
const animation = @import("../render/animation.zig");
|
|
||||||
|
|
||||||
/// Button importance level
|
/// Button importance level
|
||||||
pub const Importance = enum {
|
pub const Importance = enum {
|
||||||
|
|
@ -22,15 +17,6 @@ pub const Importance = enum {
|
||||||
danger,
|
danger,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Button state for smooth transitions (optional)
|
|
||||||
/// Pass to buttonStateful() for animated hover/press effects.
|
|
||||||
pub const ButtonState = struct {
|
|
||||||
/// Hover/press transition (0=normal, 0.5=hover, 1.0=pressed)
|
|
||||||
transition: animation.HoverTransition = .{},
|
|
||||||
/// Last frame time for delta calculation
|
|
||||||
last_time_ms: u64 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Button configuration
|
/// Button configuration
|
||||||
pub const ButtonConfig = struct {
|
pub const ButtonConfig = struct {
|
||||||
/// Background color (overrides theme)
|
/// Background color (overrides theme)
|
||||||
|
|
@ -117,97 +103,6 @@ pub fn buttonRect(ctx: *Context, bounds: Layout.Rect, text: []const u8, config:
|
||||||
return clicked and !config.disabled;
|
return clicked and !config.disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Stateful Button (with smooth transitions)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Draw a button with smooth hover/press transitions
|
|
||||||
pub fn buttonStateful(ctx: *Context, state: *ButtonState, text: []const u8) bool {
|
|
||||||
return buttonStatefulEx(ctx, state, text, .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw a stateful button with custom configuration
|
|
||||||
pub fn buttonStatefulEx(ctx: *Context, state: *ButtonState, text: []const u8, config: ButtonConfig) bool {
|
|
||||||
const bounds = ctx.layout.nextRect();
|
|
||||||
return buttonStatefulRect(ctx, bounds, state, text, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw a stateful button in a specific rectangle
|
|
||||||
pub fn buttonStatefulRect(
|
|
||||||
ctx: *Context,
|
|
||||||
bounds: Layout.Rect,
|
|
||||||
state: *ButtonState,
|
|
||||||
text: []const u8,
|
|
||||||
config: ButtonConfig,
|
|
||||||
) bool {
|
|
||||||
if (bounds.isEmpty()) return false;
|
|
||||||
|
|
||||||
// Calculate delta time
|
|
||||||
const current_time = ctx.current_time_ms;
|
|
||||||
const dt_ms: u64 = if (state.last_time_ms > 0 and current_time > state.last_time_ms)
|
|
||||||
current_time - state.last_time_ms
|
|
||||||
else
|
|
||||||
16; // Default ~60fps
|
|
||||||
state.last_time_ms = current_time;
|
|
||||||
|
|
||||||
// Check mouse interaction
|
|
||||||
const mouse = ctx.input.mousePos();
|
|
||||||
const hovered = bounds.contains(mouse.x, mouse.y) and !config.disabled;
|
|
||||||
const pressed = hovered and ctx.input.mouseDown(.left);
|
|
||||||
const clicked = hovered and ctx.input.mouseReleased(.left);
|
|
||||||
|
|
||||||
// Update transition animation
|
|
||||||
state.transition.updateWithPress(hovered, pressed, dt_ms);
|
|
||||||
|
|
||||||
// Determine colors based on animated state
|
|
||||||
const theme = Style.Theme.dark;
|
|
||||||
|
|
||||||
const base_bg = config.bg orelse switch (config.importance) {
|
|
||||||
.normal => theme.button_bg,
|
|
||||||
.primary => theme.primary,
|
|
||||||
.danger => theme.danger,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate animated background color
|
|
||||||
const bg_color = if (config.disabled)
|
|
||||||
base_bg.darken(30)
|
|
||||||
else
|
|
||||||
state.transition.blendThree(
|
|
||||||
base_bg, // normal
|
|
||||||
base_bg.lighten(10), // hover
|
|
||||||
base_bg.darken(20), // pressed
|
|
||||||
);
|
|
||||||
|
|
||||||
const fg_color = config.fg orelse if (config.disabled)
|
|
||||||
theme.button_fg.darken(40)
|
|
||||||
else
|
|
||||||
theme.button_fg;
|
|
||||||
|
|
||||||
// Draw background and border based on render mode
|
|
||||||
if (Style.isFancy() and config.corner_radius > 0) {
|
|
||||||
ctx.pushCommand(Command.roundedRect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color, config.corner_radius));
|
|
||||||
ctx.pushCommand(Command.roundedRectOutline(bounds.x, bounds.y, bounds.w, bounds.h, theme.border, config.corner_radius));
|
|
||||||
} else {
|
|
||||||
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color));
|
|
||||||
ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, theme.border));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw text centered
|
|
||||||
const char_width: u32 = 8;
|
|
||||||
const char_height: u32 = 8;
|
|
||||||
const text_width = @as(u32, @intCast(text.len)) * char_width;
|
|
||||||
const text_x = bounds.x + @as(i32, @intCast((bounds.w -| text_width) / 2));
|
|
||||||
const text_y = bounds.y + @as(i32, @intCast((bounds.h -| char_height) / 2));
|
|
||||||
|
|
||||||
ctx.pushCommand(Command.text(text_x, text_y, text, fg_color));
|
|
||||||
|
|
||||||
return clicked and !config.disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Convenience Functions
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Draw a primary button (convenience function)
|
/// Draw a primary button (convenience function)
|
||||||
pub fn buttonPrimary(ctx: *Context, text: []const u8) bool {
|
pub fn buttonPrimary(ctx: *Context, text: []const u8) bool {
|
||||||
return buttonEx(ctx, text, .{ .importance = .primary });
|
return buttonEx(ctx, text, .{ .importance = .primary });
|
||||||
|
|
@ -289,32 +184,3 @@ test "button disabled no click" {
|
||||||
try std.testing.expect(!clicked);
|
try std.testing.expect(!clicked);
|
||||||
ctx.endFrame();
|
ctx.endFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
test "buttonStateful transitions" {
|
|
||||||
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
|
||||||
defer ctx.deinit();
|
|
||||||
|
|
||||||
var state = ButtonState{};
|
|
||||||
|
|
||||||
// Frame 1: Not hovered
|
|
||||||
ctx.beginFrame();
|
|
||||||
ctx.current_time_ms = 0;
|
|
||||||
ctx.layout.row_height = 30;
|
|
||||||
ctx.input.setMousePos(500, 500); // Outside button
|
|
||||||
_ = buttonStateful(&ctx, &state, "Test");
|
|
||||||
ctx.endFrame();
|
|
||||||
|
|
||||||
// Transition should be at 0
|
|
||||||
try std.testing.expectEqual(@as(f32, 0.0), state.transition.value);
|
|
||||||
|
|
||||||
// Frame 2: Hovered
|
|
||||||
ctx.beginFrame();
|
|
||||||
ctx.current_time_ms = 100;
|
|
||||||
ctx.layout.row_height = 30;
|
|
||||||
ctx.input.setMousePos(50, 15); // Inside button
|
|
||||||
_ = buttonStateful(&ctx, &state, "Test");
|
|
||||||
ctx.endFrame();
|
|
||||||
|
|
||||||
// Transition should have started moving (>0)
|
|
||||||
try std.testing.expect(state.transition.value > 0.0);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
//!
|
//!
|
||||||
//! A dropdown menu for selecting one option from a list.
|
//! A dropdown menu for selecting one option from a list.
|
||||||
//! The dropdown opens on click and closes when an option is selected.
|
//! The dropdown opens on click and closes when an option is selected.
|
||||||
//! Supports smooth hover transitions via HoverTransition.
|
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Context = @import("../core/context.zig").Context;
|
const Context = @import("../core/context.zig").Context;
|
||||||
|
|
@ -10,7 +9,6 @@ const Command = @import("../core/command.zig");
|
||||||
const Layout = @import("../core/layout.zig");
|
const Layout = @import("../core/layout.zig");
|
||||||
const Style = @import("../core/style.zig");
|
const Style = @import("../core/style.zig");
|
||||||
const Input = @import("../core/input.zig");
|
const Input = @import("../core/input.zig");
|
||||||
const animation = @import("../render/animation.zig");
|
|
||||||
|
|
||||||
/// Select state (caller-managed)
|
/// Select state (caller-managed)
|
||||||
pub const SelectState = struct {
|
pub const SelectState = struct {
|
||||||
|
|
@ -22,10 +20,6 @@ pub const SelectState = struct {
|
||||||
scroll_offset: usize = 0,
|
scroll_offset: usize = 0,
|
||||||
/// Whether this widget has focus
|
/// Whether this widget has focus
|
||||||
focused: bool = false,
|
focused: bool = false,
|
||||||
/// Hover transition for smooth effects
|
|
||||||
hover: animation.HoverTransition = .{},
|
|
||||||
/// Last frame time for delta calculation
|
|
||||||
last_time_ms: u64 = 0,
|
|
||||||
|
|
||||||
/// Get selected index as optional usize
|
/// Get selected index as optional usize
|
||||||
pub fn selectedIndex(self: SelectState) ?usize {
|
pub fn selectedIndex(self: SelectState) ?usize {
|
||||||
|
|
@ -116,22 +110,15 @@ pub fn selectRect(
|
||||||
const has_focus = ctx.hasFocus(widget_id);
|
const has_focus = ctx.hasFocus(widget_id);
|
||||||
state.focused = has_focus;
|
state.focused = has_focus;
|
||||||
|
|
||||||
// Update hover transition
|
// Determine button colors
|
||||||
const current_time = ctx.current_time_ms;
|
|
||||||
const dt_ms: u64 = if (state.last_time_ms > 0 and current_time > state.last_time_ms)
|
|
||||||
current_time - state.last_time_ms
|
|
||||||
else
|
|
||||||
16;
|
|
||||||
state.last_time_ms = current_time;
|
|
||||||
state.hover.update(hovered and !config.disabled and !state.open, dt_ms);
|
|
||||||
|
|
||||||
// Determine button colors with smooth transition
|
|
||||||
const bg_color = if (config.disabled)
|
const bg_color = if (config.disabled)
|
||||||
theme.button_bg.darken(20)
|
theme.button_bg.darken(20)
|
||||||
else if (state.open)
|
else if (state.open)
|
||||||
theme.button_bg.lighten(10)
|
theme.button_bg.lighten(10)
|
||||||
|
else if (hovered)
|
||||||
|
theme.button_bg.lighten(5)
|
||||||
else
|
else
|
||||||
state.hover.blend(theme.button_bg, theme.button_bg.lighten(5));
|
theme.button_bg;
|
||||||
|
|
||||||
const border_color = if (has_focus or state.open) theme.primary else theme.border;
|
const border_color = if (has_focus or state.open) theme.primary else theme.border;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue