sliderEntry
Combines a slider and a text entry box on key press. Displays value on top of slider.
Returns true if value was changed.
Parameters
- src:std.builtin.SourceLocation
- std.builtin.SourceLocation
- label_fmt:?[]const u8
- ?[]const u8
- init_opts:SliderEntryInitOptions
- SliderEntryInitOptions
- opts:Options
- Options
Source
Implementation
pub fn sliderEntry(src: std.builtin.SourceLocation, comptime label_fmt: ?[]const u8, init_opts: SliderEntryInitOptions, opts: Options) bool {
// This widget swaps between either a slider with a label or a text entry.
// The tricky part of this is maintaining focus. Strategy is a containing
// box that will keep focus, and forward events to the text entry.
//
// We are keeping this simple by only swapping between slider and textEntry
// on a frame boundary.
const exp_min_change = 0.1;
const exp_stretch = 0.02;
const key_percentage = 0.05;
var options = slider_entry_defaults.min_sizeM(10, 1).override(opts);
var ret = false;
var hover = false;
var b: BoxWidget = undefined;
b.init(src, .{ .dir = .horizontal }, options);
defer b.deinit();
if (b.data().accesskit_node()) |ak_node| {
AccessKit.nodeAddAction(ak_node, AccessKit.Action.focus);
AccessKit.nodeAddAction(ak_node, AccessKit.Action.set_value);
AccessKit.nodeSetNumericValue(ak_node, init_opts.value.*);
AccessKit.nodeSetOrientation(ak_node, AccessKit.Orientation.horizontal);
if (init_opts.min) |min| AccessKit.nodeSetMinNumericValue(ak_node, min);
if (init_opts.max) |max| AccessKit.nodeSetMaxNumericValue(ak_node, max);
if (init_opts.min != null and init_opts.max != null) {
const delta = init_opts.max.? - init_opts.min.?;
AccessKit.nodeSetNumericValueStep(ak_node, delta / 100);
AccessKit.nodeSetNumericValueJump(ak_node, delta / 10);
} else {
AccessKit.nodeSetNumericValueStep(ak_node, 1);
AccessKit.nodeSetNumericValueJump(ak_node, 10);
}
}
tabIndexSet(b.data().id, options.tab_index, b.data().rectScale().r);
const br = b.data().contentRect();
const knobsize = @min(br.w, br.h);
const rs = b.data().contentRectScale();
var text_mode = dataGet(null, b.data().id, "_text_mode", bool) orelse false;
// must call dataGet/dataSet on these every frame to prevent them from
// getting purged
_ = dataGet(null, b.data().id, "_start_x", f32);
_ = dataGet(null, b.data().id, "_start_v", f32);
if (text_mode) {
var te_buf = dataGetSlice(null, b.data().id, "_buf", []u8) orelse blk: {
var buf: [20]u8 = @splat(0);
_ = std.fmt.bufPrintSentinel(&buf, "{d:0.3}", .{init_opts.value.*}, 0) catch {};
dataSetSlice(null, b.data().id, "_buf", &buf);
break :blk dataGetSlice(null, b.data().id, "_buf", []u8).?;
};
// pass 0 for tab_index so you can't tab to TextEntry
var te: TextEntryWidget = undefined;
te.init(@src(), .{ .text = .{ .buffer = te_buf } }, options.strip().override(.{ .min_size_content = .{}, .expand = .both, .tab_index = 0 }));
if (firstFrame(te.data().id)) {
var sel = te.textLayout.selection;
sel.start = 0;
sel.cursor = 0;
sel.end = std.math.maxInt(usize);
}
var new_val: ?f32 = null;
const evts = events();
for (evts) |*e| {
if (!text_mode) {
// if we are switching out of text mode, skip processing any
// remaining events
continue;
}
// te.matchEvent could be passively listening to events, so don't
// short-circuit it
const match1 = eventMatch(e, .{ .id = b.data().id, .r = rs.r });
const match2 = te.matchEvent(e);
if (!match1 and !match2)
continue;
if (e.evt == .key and e.evt.key.action == .down and e.evt.key.code == .enter) {
e.handle(@src(), b.data());
text_mode = false;
new_val = std.fmt.parseFloat(f32, te_buf[0..te.len]) catch null;
}
if (e.evt == .key and e.evt.key.action == .down and e.evt.key.code == .escape) {
e.handle(@src(), b.data());
text_mode = false;
// don't set new_val, we are escaping
}
// don't want TextEntry to get focus
if (e.evt == .mouse and e.evt.mouse.action == .focus) {
e.handle(@src(), b.data());
focusWidget(b.data().id, null, e.num);
}
if (!e.handled) {
te.processEvent(e);
}
}
if (b.data().id == focusedWidgetId()) {
dvui.wantTextInput(b.data().borderRectScale().r.toNatural());
} else {
// we lost focus
text_mode = false;
new_val = std.fmt.parseFloat(f32, te_buf[0..te.len]) catch null;
}
if (!text_mode) {
refresh(null, @src(), b.data().id);
if (new_val) |*nv| {
if (init_opts.min) |min| nv.* = @max(min, nv.*);
if (init_opts.max) |max| nv.* = @min(max, nv.*);
init_opts.value.* = nv.*;
ret = true;
}
}
te.draw();
te.drawCursor();
te.deinit();
} else {
// show slider and label
const trackrs = b.widget().screenRectScale(.{ .x = knobsize / 2, .w = br.w - knobsize });
const min_x = trackrs.r.x;
const max_x = trackrs.r.x + trackrs.r.w;
const px_scale = trackrs.s;
const evts = events();
for (evts) |*e| {
if (!eventMatch(e, .{ .id = b.data().id, .r = rs.r }))
continue;
switch (e.evt) {
.mouse => |me| {
var p: ?Point.Physical = null;
if (me.action == .focus) {
e.handle(@src(), b.data());
focusWidget(b.data().id, null, e.num);
} else if (me.action == .press and me.button.pointer()) {
e.handle(@src(), b.data());
if (me.mod.matchBind("ctrl/cmd")) {
text_mode = true;
refresh(null, @src(), b.data().id);
} else {
captureMouse(b.data(), e.num);
dataSet(null, b.data().id, "_start_x", me.p.x);
dataSet(null, b.data().id, "_start_v", init_opts.value.*);
if (me.button.touch()) {
dvui.dragPreStart(me.button, me.p, .{});
} else {
// Only start tracking the position on press if this
// is not a touch to prevent the value from
// "jumping" when entering text mode on a
// touch-tap event
p = me.p;
}
}
} else if (me.action == .release and me.button.pointer()) {
if (me.button.touch() and dvui.dragging(me.p, null) == null) {
text_mode = true;
refresh(null, @src(), b.data().id);
}
e.handle(@src(), b.data());
captureMouse(null, e.num);
dragEnd();
dataRemove(null, b.data().id, "_start_x");
dataRemove(null, b.data().id, "_start_v");
} else if (me.action == .motion and captured(b.data().id)) {
e.handle(@src(), b.data());
// If this is a touch motion we need to make sure to
// only update the value if we are exceeding the
// drag threshold to prevent the value from jumping while
// entering text mode via a non-drag touch-tap
if (!me.button.touch() or dvui.dragging(me.p, null) != null) {
p = me.p;
}
} else if (me.action == .position) {
dvui.cursorSet(.arrow);
hover = true;
}
if (p) |pp| {
if (max_x > min_x) {
ret = true;
if (init_opts.min != null and init_opts.max != null) {
// lerp but make sure we can hit the max
if (pp.x > max_x) {
init_opts.value.* = init_opts.max.?;
} else {
const px_lerp = @max(0, @min(1, (pp.x - min_x) / (max_x - min_x)));
init_opts.value.* = init_opts.min.? + px_lerp * (init_opts.max.? - init_opts.min.?);
if (init_opts.interval) |ival| {
init_opts.value.* = init_opts.min.? + ival * @round((init_opts.value.* - init_opts.min.?) / ival);
}
}
} else if (init_opts.min != null) {
// only have min, go exponentially to the right
if (pp.x < min_x) {
init_opts.value.* = init_opts.min.?;
} else {
const base = if (init_opts.min.? == 0) exp_min_change else @exp(math.ln10 * @floor(@log10(@abs(init_opts.min.?)))) * exp_min_change;
const how_far = @max(0, (pp.x - min_x)) / px_scale;
const how_much = (@exp(how_far * exp_stretch) - 1) * base;
init_opts.value.* = init_opts.min.? + how_much;
if (init_opts.interval) |ival| {
init_opts.value.* = init_opts.min.? + ival * @round((init_opts.value.* - init_opts.min.?) / ival);
}
}
} else if (init_opts.max != null) {
// only have max, go exponentially to the left
if (pp.x > max_x) {
init_opts.value.* = init_opts.max.?;
} else {
const base = if (init_opts.max.? == 0) exp_min_change else @exp(math.ln10 * @floor(@log10(@abs(init_opts.max.?)))) * exp_min_change;
const how_far = @max(0, (max_x - pp.x)) / px_scale;
const how_much = (@exp(how_far * exp_stretch) - 1) * base;
init_opts.value.* = init_opts.max.? - how_much;
if (init_opts.interval) |ival| {
init_opts.value.* = init_opts.max.? - ival * @round((init_opts.max.? - init_opts.value.*) / ival);
}
}
} else {
// neither min nor max, go exponentially away from starting value
if (dataGet(null, b.data().id, "_start_x", f32)) |start_x| {
if (dataGet(null, b.data().id, "_start_v", f32)) |start_v| {
const base = if (start_v == 0) exp_min_change else @exp(math.ln10 * @floor(@log10(@abs(start_v)))) * exp_min_change;
const how_far = (pp.x - start_x) / px_scale;
const how_much = (@exp(@abs(how_far) * exp_stretch) - 1) * base;
init_opts.value.* = if (how_far < 0) start_v - how_much else start_v + how_much;
if (init_opts.interval) |ival| {
init_opts.value.* = start_v + ival * @round((init_opts.value.* - start_v) / ival);
}
}
}
}
}
}
},
.key => |ke| {
if (ke.code == .enter and ke.action == .down) {
text_mode = true;
} else if (ke.action == .down or ke.action == .repeat) {
switch (ke.code) {
.left, .right => {
e.handle(@src(), b.data());
ret = true;
if (init_opts.interval) |ival| {
init_opts.value.* = init_opts.value.* + (if (ke.code == .left) -ival else ival);
} else {
const how_much = @abs(init_opts.value.*) * key_percentage;
init_opts.value.* = if (ke.code == .left) init_opts.value.* - how_much else init_opts.value.* + how_much;
}
if (init_opts.min) |min| {
init_opts.value.* = @max(min, init_opts.value.*);
}
if (init_opts.max) |max| {
init_opts.value.* = @min(max, init_opts.value.*);
}
},
else => {},
}
}
},
.text => |te| {
switch (te.action) {
.value => |set| {
e.handle(@src(), b.data());
var value = std.fmt.parseFloat(f32, set.txt) catch init_opts.min orelse 0;
if (init_opts.min) |min| value = @max(min, value);
if (init_opts.max) |max| value = @min(max, value);
init_opts.value.* = value;
},
else => {},
}
},
else => {},
}
}
b.data().borderAndBackground(.{ .fill_color = if (hover) b.data().options.color(.fill_hover) else b.data().options.color(.fill) });
// only draw handle if we have a min and max
if (b.data().visible() and init_opts.min != null and init_opts.max != null) {
const how_far = (init_opts.value.* - init_opts.min.?) / (init_opts.max.? - init_opts.min.?);
const knobRect = Rect{ .x = (br.w - knobsize) * math.clamp(how_far, 0, 1), .w = knobsize, .h = knobsize };
const knobrs = b.widget().screenRectScale(knobRect);
knobrs.r.fill(options.cornersGet().scale(knobrs.s, CornerRect.Physical), .{ .color = options.color(.fill_press), .fade = 1.0 });
}
const label_opts = options.strip().override(.{ .gravity_x = 0.5, .gravity_y = 0.5 });
if (init_opts.label) |l| {
label(@src(), "{s}", .{l}, label_opts);
} else {
label(@src(), label_fmt orelse "{d:.3}", .{init_opts.value.*}, label_opts);
}
}
if (b.data().id == focusedWidgetId()) {
b.data().focusBorder();
}
dataSet(null, b.data().id, "_text_mode", text_mode);
if (ret) {
refresh(null, @src(), b.data().id);
}
return ret;
}