From 372d68a775c66a91ee7d7a5535e2129f06179129 Mon Sep 17 00:00:00 2001 From: TheFGFSEagle Date: Wed, 15 Feb 2023 00:46:18 +0100 Subject: [PATCH] Canvas slider widget: Added value display Add ticks, round mouse dragging result to nearest multiple of step size, implement scroll handler Add keybindings for adjusting slider value --- Nasal/canvas/gui/Widget.nas | 5 - .../gui/dialogs/WidgetsFactoryDialog.nas | 34 +++-- Nasal/canvas/gui/styles/DefaultStyle.nas | 140 ++++++++++++++---- Nasal/canvas/gui/widgets/Slider.nas | 119 +++++++++++---- gui/styles/AmbianceClassic/style.xml | 6 + 5 files changed, 237 insertions(+), 67 deletions(-) diff --git a/Nasal/canvas/gui/Widget.nas b/Nasal/canvas/gui/Widget.nas index 56798b4e8..1651be137 100644 --- a/Nasal/canvas/gui/Widget.nas +++ b/Nasal/canvas/gui/Widget.nas @@ -200,11 +200,6 @@ gui.Widget = { me._trigger("mouse-leave"); me._onStateChange(); }); - root.addEventListener("keypress", func(e) { - if (me._focused) { - root.onKeyPressed(e); - } - }); # if we have keyboard bindings defined, add the listener for them if (size(me._bindings)) { diff --git a/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas b/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas index 433032985..f763764c1 100644 --- a/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas +++ b/Nasal/canvas/gui/dialogs/WidgetsFactoryDialog.nas @@ -29,6 +29,15 @@ var WidgetsFactoryDialog = { w.setText("Label " ~ i); }); }); + m.widgetsMenu.createItem(text: "Benchmark slider", cb: func { + m.benchmark_widget(widget: canvas.gui.widgets.Slider, proc_func: func(w, i) { + w.setValue(i); + }, cfg: { + "value-position": canvas.gui.widgets.Slider.ValuePosition.Below, + "value-style": canvas.gui.widgets.Slider.ValueStyle.Moving, + "ticks-position": gui.widgets.Slider.TicksPosition.Below, + }); + }); m.widgetsMenu.createItem(text: "Benchmark radio button", cb: func { m.benchmark_radio_button(func(w, i) { w.setText("Radio button " ~ i); @@ -201,11 +210,15 @@ var WidgetsFactoryDialog = { m.benchmark_tab.addItem(m.benchmark_statistics); m.numericControlsTab = VBoxLayout.new(); - m.tabs.addTab("ncTab", "Numeric Controls", m.numericControlsTab); - m.slider = gui.widgets.Slider.new(m.tabsContent, style, - {"max-value" : 100, - "page-step" : 20, - "tick-count" : 10}) + m.tabs.addTab("numeric-controls", "Numeric controls", m.numericControlsTab); + m.slider = gui.widgets.Slider.new(m.tabsContent, style, { + "max-value" : 100, + "page-size" : 20, + "tick-step" : 10, + "value-style": gui.widgets.Slider.ValueStyle.Moving, + "value-position": gui.widgets.Slider.ValuePosition.Above, + "ticks-position": gui.widgets.Slider.TicksPosition.Above, + }) .setValue(42); m.numericControlsTab.addItem(m.slider); @@ -213,11 +226,12 @@ var WidgetsFactoryDialog = { return m; }, - benchmark_widget: func(widget, proc_func=nil, amount=50) { + benchmark_widget: func(widget, proc_func=nil, amount=50, cfg=nil) { + cfg = cfg or {}; var start = systime(); me.benchmark_tab_scroll_layout.clear(); for (var i = 0; i < amount; i += 1) { - var w = widget.new(me.benchmark_tab_scroll.getContent(), canvas.style, {}); + var w = widget.new(me.benchmark_tab_scroll.getContent(), canvas.style, cfg); if (proc_func != nil) { proc_func(w, i); } @@ -227,12 +241,14 @@ var WidgetsFactoryDialog = { me.benchmark_statistics.setText("Took " ~ time ~ " seconds to add " ~ amount ~ " widgets."); }, - benchmark_radio_button: func(proc_func=nil, amount=50) { + benchmark_radio_button: func(proc_func=nil, amount=50, cfg= nil) { + cfg = cfg or {}; var start = systime(); me.benchmark_tab_scroll_layout.clear(); var r = canvas.gui.widgets.RadioButton.new(me.benchmark_tab_scroll.getContent()); + cfg["parentRadio"] = r; for (var i = 1; i < amount; i += 1) { - var w = canvas.gui.widgets.RadioButton.new(me.benchmark_tab_scroll.getContent(), canvas.style, {parentRadio: r}); + var w = canvas.gui.widgets.RadioButton.new(me.benchmark_tab_scroll.getContent(), canvas.style, cfg); if (proc_func != nil) { proc_func(w, i); } diff --git a/Nasal/canvas/gui/styles/DefaultStyle.nas b/Nasal/canvas/gui/styles/DefaultStyle.nas index 4775ea1d3..cef100c24 100644 --- a/Nasal/canvas/gui/styles/DefaultStyle.nas +++ b/Nasal/canvas/gui/styles/DefaultStyle.nas @@ -934,23 +934,55 @@ DefaultStyle.widgets.slider = { me._createElement("fill", "image") .set("slice", "2 6"); + me._ticks = me._root.createChild("path") + .set("stroke-width", me._style.getSize("slider-ticks-width", 1)); + me._fillHeight = me._fill.imageSize()[1]; me._createElement("thumb", "image"); me._thumbSize = me._thumb.imageSize(); - me._ticks = 0; - me._ticksPath = nil; + me._value = me._root.createChild("text") + .set("font", "LiberationFonts/LiberationSans-Regular.ttf") + .set("character-size", me._style.getSize("slider-value-font-size", me._style.getSize("base-font-size"))) + .set("alignment", "center-top"); + }, + + _updateLayoutSizes: func(model) { + me.update(model); + var h = me._thumb.imageSize()[1] + 6; + if (model._valueDisplayPosition != model.ValuePosition.None) { + h += me._style.getSize("slider-value-font-size", me._style.getSize("base-font-size")) + + me._style.getSize("slider-thumb-value-margin", 8); + } + if (model._ticksPosition != model.TicksPosition.None and model._valueDisplayPosition != model._ticksPosition) { + h += me._style.getSize("slider-fill-ticks-margin", 3) + me._style.getSize("slider-tick-length", 10); + } + model.setLayoutMinimumSize([50, h]); + model.setLayoutSizeHint([(model._maxValue - model._minValue) / (model._stepSize or 1), h]); + model.setLayoutMaximumSize([model._MAX_SIZE, h]); }, setNormValue: func(model, normValue) { - var (w, h) = model._size; + var w = model._size[0]; var halfThumbWidth = me._thumbSize[0] * 0.5; var availWidthPos = w - me._thumbSize[0]; var thumbX = math.round(availWidthPos * normValue); - var thumbY = (h - me._thumbSize[1]) * 0.5; - me._thumb.setTranslation(thumbX - halfThumbWidth, thumbY); + + var valueX = 0; + if (model._valueDisplayStyle == model.ValueStyle.Moving) { + var startPos = me._value.maxWidth() / 2; + var thumbPos = thumbX + me._thumbSize[0] * 0.5; + var endPos = w - (me._value.maxWidth() / 2); + valueX = math.clamp(thumbPos, startPos, endPos); + } elsif (model._valueDisplayStyle == model.ValueStyle.Fixed) { + valueX = w / 2; + } + me._value.setTranslation(valueX, me._value.getTranslation()[1]); + me._thumb.setTranslation(thumbX, me._thumb.getTranslation()[1]); me._fill.setSize(thumbX, me._fillHeight); + me._fill.setTranslation(halfThumbWidth, me._fill.getTranslation()[1]); + me._value.setText(model._value); }, update: func(model) @@ -974,6 +1006,7 @@ DefaultStyle.widgets.slider = { } me._fill.set("src", file ~ ".png"); + me._fillHeight = me._fill.imageSize()[1]; # set thumb state file = me._style._dir_widgets ~ "/"; @@ -981,14 +1014,30 @@ DefaultStyle.widgets.slider = { if( !model._enabled ) { file ~= "-disabled"; } else { - if (model._down) + if (model._thumbDown) file ~= "-focused"; if (model._hover) file ~= "-hover"; } me._thumb.set("src", file ~ ".png"); + me._thumbSize = me._thumb.imageSize(); + var color_name = model._windowFocus() ? "fg_color" : "backdrop_fg_color"; + me._value.set("fill", me._style.getColor(color_name)); + if (model._valueDisplayPosition != model.ValuePosition.None) { + me._value.show(); + } else { + me._value.hide(); + } + + me._ticks.set("stroke", me._style.getColor("slider_ticks")); + if (model._ticksPosition != model.TicksPosition.None) { + me._ticks.show(); + } else { + me._ticks.hide(); + } + # update the position as well, since other stuff # may have changed me.setNormValue(model, model._normValue()); @@ -996,28 +1045,67 @@ DefaultStyle.widgets.slider = { setSize: func(model, w, h) { - var fillTop = (h - me._fillHeight) * 0.5; - me._bg.setTranslation(0, fillTop); - me._fill.setTranslation(0, fillTop); - me._bg.setSize(w, me._fillHeight); + var valueFontSize = me._style.getSize("slider-value-font-size", me._style.getSize("base-font-size")); + var thumbValueMargin = me._style.getSize("slider-thumb-value-margin", 8); + var fillTicksMargin = me._style.getSize("slider-fill-ticks-margin", 3); + var ticksOffset = fillTicksMargin + me._style.getSize("slider-tick-length", 10); + + var thumbY = (h - me._thumbSize[1]) * 0.5; + if (model._valueDisplayPosition != model.ValuePosition.None) { + thumbY -= (valueFontSize + thumbValueMargin) * 0.5; + } + if (model._ticksPosition != model.TicksPosition.None and model._ticksPosition != model._valueDisplayPosition) { + thumbY -= ticksOffset * 0.5; + } + + var valueY = thumbY; + if (model._valueDisplayPosition == model.ValuePosition.Above) { + thumbY += valueFontSize + thumbValueMargin; + } elsif (model._valueDisplayPosition == model.ValuePosition.Below) { + valueY += me._thumbSize[1] + thumbValueMargin; + } + + var fillY = thumbY + (me._thumbSize[1] - me._fillHeight) * 0.5; + + var ticksY = fillY; + if (model._ticksPosition == model.TicksPosition.Below) { + ticksY += me._fillHeight + fillTicksMargin; + } elsif (model._ticksPosition == model.TicksPosition.Above) { + if (model._valueDisplayPosition == model.ValuePosition.Below) { + thumbY += ticksOffset; + fillY += ticksOffset; + valueY += ticksOffset; + } else { + ticksY -= ticksOffset; + } + } + + me._bg.setTranslation(me._thumbSize[0] / 2, fillY); + me._fill.setTranslation(me._fill.getTranslation()[0], fillY); + me._ticks.setTranslation(me._thumbSize[0] / 2, ticksY); + me._thumb.setTranslation(me._thumb.getTranslation()[0], thumbY); + me._value.setTranslation(me._value.getTranslation()[0], valueY); + me._bg.setSize(w - me._thumbSize[0], me._fillHeight); me.setNormValue(model, model._normValue()); + me._drawTicks(model); }, - updateRanges: func(minValue, maxValue, numTicks = 0) - { - if (me._ticks != numTicks) { - # update tick marks - if (numTicks == 0) { - me._ticks = 0; - me._deleteElement('ticksPath'); - } else { - me._createElement('ticksPath', 'path'); - me._ticks = numTicks; - - # set style - # loop adding ticks - - } + _drawTicks: func(model) { + me._ticks.reset(); + var range = model._maxValue - model._minValue; + if (range <= 0 or model._tickStep <= 0) { + return; + } + var availWidthPos = model._size[0] - me._thumbSize[0]; + var pixelsPerUnit = availWidthPos / range; + var remainder = math.mod(range, model._tickStep); + var numTicks = int((range - remainder) / model._tickStep); + if (remainder == 0) { + numTicks -= 1; + } + for (var i = 1; i <= numTicks; i += 1) { + me._ticks.moveTo(i * pixelsPerUnit * model._tickStep, 0) + .vert(me._style.getSize("slider-tick-length", 8)); } }, @@ -1032,7 +1120,7 @@ DefaultStyle.widgets.slider = { if( type == "text" ) { me[ mem ].set("font", "LiberationFonts/LiberationSans-Regular.ttf") - .set("character-size", 14) + .set("character-size", me._style.getSize("slider-value-font-size", me._style.getSize("base-font-size"))) .set("alignment", "left-center"); } } diff --git a/Nasal/canvas/gui/widgets/Slider.nas b/Nasal/canvas/gui/widgets/Slider.nas index 44536959d..f167e90e4 100644 --- a/Nasal/canvas/gui/widgets/Slider.nas +++ b/Nasal/canvas/gui/widgets/Slider.nas @@ -4,47 +4,65 @@ # SPDX-License-Identifier: GPL-2.0-or-later gui.widgets.Slider = { + ValueStyle: { + Fixed: 0, + Moving: 1, + }, + ValuePosition: { + None: 0, + Above: 1, + Below: 2, + }, + TicksPosition: { + None: 0, + Above: 1, + Below: 2, + }, new: func(parent, style, cfg) { var cfg = Config.new(cfg); var m = gui.Widget.new(gui.widgets.Slider); m._focus_policy = m.StrongFocus; - m._down = 0; - m._minValue = 0; + m._thumbDown = 0; + m._minValue = cfg.get("min-value", 0); m._maxValue = cfg.get("max-value", 100); - m._value = 50; - m._pageStep = cfg.get("page-step", 0); - m._numTicks = cfg.get("tick-count", 0); + m._value = cfg.get("value", 50); + m._stepSize = cfg.get("step-size", 1); + m._pageSize = cfg.get("page-size", 10); + m._tickStep = cfg.get("tick-step", 10); - m._tickStyle = cfg.get("ticks-style", 0); - m._valueDisplayStyle = cfg.get("value-style", 0); + m._ticksPosition = cfg.get("ticks-position", m.TicksPosition.None); + m._valueDisplayStyle = cfg.get("value-style", m.ValueStyle.Moving); + m._valueDisplayPosition = cfg.get("value-position", m.ValuePosition.None); - # TODO : select where value is shown - # TODO : select where tick marks are shown - - m._setView( style.createWidget(parent, cfg.get("type", "slider"), cfg) ); - m._view.updateRanges(m._minValue, m._maxValue, m._numTicks); + m._setView(style.createWidget(parent, cfg.get("type", "slider"), cfg)); + m._view._updateLayoutSizes(m); return m; }, setValue: func(val) { - me._value = val; - if( me._view != nil ) { + me._value = math.clamp(val, me._minValue, me._maxValue); + if (me._view != nil) { me._view.setNormValue(me, me._normValue()); } return me; }, - setDown: func(down = 1) - { - if (me._down == down ) - return me; + setValuePosition: func(pos) { + me._valueDisplayPosition = pos; + me._view._updateLayoutSizes(me); + }, - me._down = down; - me._onStateChange(); - return me; + setValueStyle: func(style) { + me._valueDisplayStyle = style; + me._view._updateLayoutSizes(me); + }, + + setTicksPositon: func(pos) { + me._ticksPosition = pos; + me._view._updateLayoutSizes(me); }, # protected: @@ -53,19 +71,62 @@ gui.widgets.Slider = { call(gui.Widget._setView, [view], me); var el = view._root; - el.addEventListener("mousedown", func if( me._enabled ) me.setDown(1)); - el.addEventListener("mouseup", func if( me._enabled ) me.setDown(0)); - # el.addEventListener("click", func if( me._enabled ) me.toggle()); - el.addEventListener("mouseleave",func me.setDown(0)); + el.addEventListener("click", func(e) { + me._dragThumb(e); + }); view._thumb.addEventListener("drag", func(e) { me._dragThumb(e); e.stopPropagation(); }); + view._thumb.addEventListener("mousedown", func(e) { + me._thumbDown = 1; + me._onStateChange(); + }); + view._thumb.addEventListener("mouseup", func(e) { + me._thumbDown = 0; + me._onStateChange(); + }); + view._root.addEventListener("wheel", func(e) { + if (!me._enabled) { + return; + } + + me.setValue(me._value + e.deltaY * me._stepSize); + e.stopPropagation(); + }); + view._root.addEventListener("keydown", func(e) { + var value = me._value; + if (contains([ + keyboard.FunctionKeys.Left, keyboard.FunctionKeys.KP_Left, + keyboard.FunctionKeys.Down, keyboard.FunctionKeys.KP_Down, + keyboard.PrintableKeys.Minus, keyboard.FunctionKeys.KP_Subtract, + ], e.keyCode)) { + value -= me._stepSize; + } elsif (contains([ + keyboard.FunctionKeys.Right, keyboard.FunctionKeys.KP_Right, + keyboard.FunctionKeys.Up, keyboard.FunctionKeys.KP_Up, + keyboard.PrintableKeys.Plus, keyboard.FunctionKeys.KP_Add, + ], e.keyCode)) { + value += me._stepSize; + } elsif (contains([keyboard.FunctionKeys.Page_Down, keyboard.FunctionKeys.KP_Page_Down], e.keyCode)) { + value -= me._pageSize; + } elsif (contains([keyboard.FunctionKeys.Page_Up, keyboard.FunctionKeys.KP_Page_Up], e.keyCode)) { + value += me._pageSize; + } elsif (contains([keyboard.FunctionKeys.Home, keyboard.FunctionKeys.KP_Home], e.keyCode)) { + value = me._minValue; + } elsif (contains([keyboard.FunctionKeys.End, keyboard.FunctionKeys.KP_End], e.keyCode)) { + value = me._maxValue; + } + me.setValue(value); + }); }, _dragThumb: func(event) - { + { + if (!me._enabled) { + return; + } var vr = me._view._root; var viewPosX = vr.canvasToLocal([event.clientX, event.clientY])[0]; var width = me._size[0]; @@ -76,7 +137,11 @@ gui.widgets.Slider = { me.setValue(me._maxValue); } else { var norm = viewPosX / width; - me.setValue(norm * ( me._maxValue - me._minValue)); + var mouseValue = me._minValue + norm * ( me._maxValue - me._minValue); + if (me._stepSize != 0) { + mouseValue = math.round(mouseValue / me._stepSize) * me._stepSize; + } + me.setValue(mouseValue); } }, diff --git a/gui/styles/AmbianceClassic/style.xml b/gui/styles/AmbianceClassic/style.xml index 571a840d3..1dc6d83de 100644 --- a/gui/styles/AmbianceClassic/style.xml +++ b/gui/styles/AmbianceClassic/style.xml @@ -311,6 +311,12 @@ 0.5 0.5 + + + 0.5 + 0.5 + 0.5 +