Add additional widgets for PUI compat
Styling of these is not complete yet.
This commit is contained in:
parent
10a31e1f3c
commit
82cb79feb3
7 changed files with 542 additions and 1 deletions
|
@ -31,11 +31,17 @@ loadGUIFile("Config.nas");
|
|||
loadGUIFile("Style.nas");
|
||||
loadGUIFile("Widget.nas");
|
||||
loadGUIFile("styles/DefaultStyle.nas");
|
||||
|
||||
# widgets
|
||||
loadWidget("Button");
|
||||
loadWidget("CheckBox");
|
||||
loadWidget("Label");
|
||||
loadWidget("LineEdit");
|
||||
loadWidget("ScrollArea");
|
||||
loadWidget("Rule");
|
||||
loadWidget("Slider");
|
||||
|
||||
# standard dialogs
|
||||
loadDialog("InputDialog");
|
||||
loadDialog("MessageBox");
|
||||
|
||||
|
|
|
@ -196,7 +196,7 @@ DefaultStyle.widgets.label = {
|
|||
},
|
||||
setText: func(model, text)
|
||||
{
|
||||
if( text == nil or size(text) == 0 )
|
||||
if ( !isstr(text) or size(text) == 0 )
|
||||
{
|
||||
model.setHeightForWidthFunc(nil);
|
||||
return me._deleteElement('text');
|
||||
|
@ -451,3 +451,274 @@ DefaultStyle.widgets["scroll-area"] = {
|
|||
model._scroller_delta[dir] = model._size[dir] - model._scroller_size[dir];
|
||||
}
|
||||
};
|
||||
|
||||
# A horizontal or vertical rule line
|
||||
# possibly with a text label embedded
|
||||
DefaultStyle.widgets.rule = {
|
||||
new: func(parent, cfg)
|
||||
{
|
||||
me._root = parent.createChild("group", "rule");
|
||||
me._createElement("bg", "image");
|
||||
me._isVertical = cfg.get("isVertical");
|
||||
if (me._isVertical) {
|
||||
me._bg.set("slice", "0 20");
|
||||
me._baseFile = "vrule";
|
||||
} else {
|
||||
me._bg.set("slice", "10 0");
|
||||
me._baseFile = "hrule";
|
||||
}
|
||||
},
|
||||
setSize: func(model, w, h)
|
||||
{
|
||||
if( me['_text'] != nil )
|
||||
{
|
||||
# first 20 px
|
||||
me._bg.setTranslation(2, 0);
|
||||
me._bg.setSize(20, h);
|
||||
|
||||
# TODO handle eliding for translations?
|
||||
me._text.setTranslation(22, 2 + h / 2);
|
||||
var maxW = model._cfg.get("maxTextWidth", -1);
|
||||
if (maxW > 0) {
|
||||
me._text.set("max-width", maxW);
|
||||
}
|
||||
|
||||
var bg2Left = maxW > 0 ? maxW : me._text.maxWidth() + 22;
|
||||
me._bg2.setTranslation(bg2Left, 0);
|
||||
me._bg2.setSize(w - bg2Left, h);
|
||||
} else {
|
||||
me._bg.setSize(w, h);
|
||||
}
|
||||
return me;
|
||||
},
|
||||
setText: func(model, text)
|
||||
{
|
||||
if( text == nil or size(text) == 0 )
|
||||
{
|
||||
# force a resize?
|
||||
me._deleteElement('bg2');
|
||||
return me._deleteElement('text');
|
||||
}
|
||||
|
||||
if (me._isVertical) {
|
||||
logprint(LOG_DEVALERT, "Text label not supported for vertical rules, yet");
|
||||
return;
|
||||
}
|
||||
|
||||
me._createElement("text", "text")
|
||||
.setText(text);
|
||||
|
||||
var width_hint = me._text.maxWidth() + 40;
|
||||
me._createElement("bg2", "image")
|
||||
.set("slice", "10 0");
|
||||
|
||||
model.setLayoutMinimumSize([40, 14]);
|
||||
# TODO mark as expanding?
|
||||
model.setLayoutSizeHint([width_hint, 24]);
|
||||
|
||||
return me.update(model);
|
||||
},
|
||||
update: func(model)
|
||||
{
|
||||
var file = me._style._dir_widgets ~ "/";
|
||||
file ~= me._baseFile;
|
||||
|
||||
if( !model._enabled )
|
||||
file ~= "-disabled";
|
||||
|
||||
me._bg.set("src", file ~ ".png");
|
||||
if ( me['_bg2'] != nil)
|
||||
me._bg2.set("src", file ~ ".png");
|
||||
|
||||
# different color if disabled?
|
||||
if( me['_text'] != nil )
|
||||
{
|
||||
var color_name = model._windowFocus() ? "fg_color" : "backdrop_fg_color";
|
||||
me._text.set("fill", me._style.getColor(color_name));
|
||||
}
|
||||
},
|
||||
# protected:
|
||||
_createElement: func(name, type)
|
||||
{
|
||||
var mem = '_' ~ name;
|
||||
if( me[ mem ] == nil )
|
||||
{
|
||||
me[ mem ] = me._root.createChild(type, "rule-" ~ name);
|
||||
|
||||
if( type == "text" )
|
||||
{
|
||||
me[ mem ].set("font", "LiberationFonts/LiberationSans-Regular.ttf")
|
||||
.set("character-size", 14)
|
||||
.set("alignment", "left-center");
|
||||
}
|
||||
}
|
||||
return me[ mem ];
|
||||
},
|
||||
_deleteElement: func(name)
|
||||
{
|
||||
name = '_' ~ name;
|
||||
if( me[ name ] != nil )
|
||||
{
|
||||
me[ name ].del();
|
||||
me[ name ] = nil;
|
||||
}
|
||||
return me;
|
||||
}
|
||||
};
|
||||
|
||||
# a frame (sometimes called a group box), with optional label
|
||||
# and enable/disable checkbox
|
||||
DefaultStyle.widgets.frame = {
|
||||
new: func(parent, cfg)
|
||||
{
|
||||
me._root = parent.createChild("group", "frame-box");
|
||||
me._createElement("bg", "image")
|
||||
.set("slice", "10 10");
|
||||
me.content = me._root.createChild("group", "frame-content");
|
||||
|
||||
# handle label + checkable flag
|
||||
},
|
||||
|
||||
update: func(model)
|
||||
{
|
||||
var file = me._style._dir_widgets ~ "/";
|
||||
file ~= "backdrop-";
|
||||
|
||||
if( !model._enabled )
|
||||
file ~= "-disabled";
|
||||
|
||||
me._bg.set("src", file ~ ".png");
|
||||
|
||||
|
||||
},
|
||||
# protected:
|
||||
_createElement: func(name, type)
|
||||
{
|
||||
var mem = '_' ~ name;
|
||||
if( me[ mem ] == nil )
|
||||
{
|
||||
me[ mem ] = me._root.createChild(type, "frame-" ~ name);
|
||||
|
||||
if( type == "text" )
|
||||
{
|
||||
me[ mem ].set("font", "LiberationFonts/LiberationSans-Regular.ttf")
|
||||
.set("character-size", 14)
|
||||
.set("alignment", "left-center");
|
||||
}
|
||||
}
|
||||
return me[ mem ];
|
||||
},
|
||||
_deleteElement: func(name)
|
||||
{
|
||||
name = '_' ~ name;
|
||||
if( me[ name ] != nil )
|
||||
{
|
||||
me[ name ].del();
|
||||
me[ name ] = nil;
|
||||
}
|
||||
return me;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
# a horionztal or vertical slider, for selecting /
|
||||
# dragging over a numerical range
|
||||
DefaultStyle.widgets.slider = {
|
||||
new: func(parent, cfg)
|
||||
{
|
||||
me._root = parent.createChild("group", "slider");
|
||||
me._createElement("bg", "image")
|
||||
.set("slice", "10 10");
|
||||
|
||||
me._createElement("thumb", "image")
|
||||
.set("slice", "10 10");
|
||||
|
||||
me._ticks = 0;
|
||||
me._ticksPath = nil;
|
||||
},
|
||||
|
||||
setNormValue: func(model, normValue)
|
||||
{
|
||||
var (w, h) = model._size;
|
||||
var availWidthPos = w - h; # pixel range the thumb can move over
|
||||
me._thumb.setTranslation(round(availWidthPos * normValue), 0);
|
||||
},
|
||||
|
||||
update: func(model)
|
||||
{
|
||||
# set background state
|
||||
var file = me._style._dir_widgets ~ "/";
|
||||
file ~= "backdrop-";
|
||||
if( !model._enabled )
|
||||
file ~= "-disabled";
|
||||
|
||||
me._bg.set("src", file ~ ".png");
|
||||
|
||||
# set thumb state
|
||||
file = me._style._dir_widgets ~ "/";
|
||||
file ~= "button-"; # should we use a seperate thumb?
|
||||
if( !model._enabled )
|
||||
file ~= "-disabled";
|
||||
else if (model._down)
|
||||
file ~= "-down";
|
||||
elsif (model._hover)
|
||||
file ~= "-hovered";
|
||||
|
||||
me._thumb.set("src", file ~ ".png");
|
||||
|
||||
# set thumb size
|
||||
var (w, h) = model._size;
|
||||
# fixme assumes horizonal for now
|
||||
me._thumb.setSize(h, h);
|
||||
|
||||
# update the position as well, since other stuff
|
||||
# may have changed
|
||||
me.setNormValue(model, model._normValue());
|
||||
},
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
# protected:
|
||||
_createElement: func(name, type)
|
||||
{
|
||||
var mem = '_' ~ name;
|
||||
if( me[ mem ] == nil )
|
||||
{
|
||||
me[ mem ] = me._root.createChild(type, "slider-" ~ name);
|
||||
|
||||
if( type == "text" )
|
||||
{
|
||||
me[ mem ].set("font", "LiberationFonts/LiberationSans-Regular.ttf")
|
||||
.set("character-size", 14)
|
||||
.set("alignment", "left-center");
|
||||
}
|
||||
}
|
||||
return me[ mem ];
|
||||
},
|
||||
_deleteElement: func(name)
|
||||
{
|
||||
name = '_' ~ name;
|
||||
if( me[ name ] != nil )
|
||||
{
|
||||
me[ name ].del();
|
||||
me[ name ] = nil;
|
||||
}
|
||||
return me;
|
||||
}
|
||||
}
|
68
Nasal/canvas/gui/widgets/Frame.nas
Normal file
68
Nasal/canvas/gui/widgets/Frame.nas
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Frame.nas: container with a visual frame around it,
|
||||
# and optional checkbox / label (usuallt at the top / left)
|
||||
# to enable / disable it
|
||||
|
||||
# Copyright (C) 2022 James Turner
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
||||
gui.widgets.Frame = {
|
||||
new: func(parent, style, cfg)
|
||||
{
|
||||
var m = gui.Widget.new(gui.widgets.Frame);
|
||||
m._cfg = Config.new(cfg);
|
||||
# m._focus_policy = m.NoFocus; maybe?
|
||||
m._setView( style.createWidget(parent, "frame", m._cfg) );
|
||||
m._checkable = cfg.get("checkable", 0);
|
||||
m._label = cfg.get("label");
|
||||
m._layout = nil;
|
||||
|
||||
m.setLayoutSizeHint([200, 200]);
|
||||
m.setLayoutMaximumSize([m._MAX_SIZE, m._MAX_SIZE]);
|
||||
return m;
|
||||
},
|
||||
getContent: func()
|
||||
{
|
||||
return me._view.content;
|
||||
},
|
||||
setLabel: func(text)
|
||||
{
|
||||
me._label = text;
|
||||
me._view.update(me);
|
||||
return me;
|
||||
},
|
||||
setCheckable: func(e)
|
||||
{
|
||||
me._checkable = e;
|
||||
me._view.update(me);
|
||||
return me;
|
||||
},
|
||||
setLayout: func(;)
|
||||
{
|
||||
me._layout = l;
|
||||
l.setParent(me);
|
||||
return me.update();
|
||||
},
|
||||
setSize: func
|
||||
{
|
||||
if( size(arg) == 1 )
|
||||
var arg = arg[0];
|
||||
var (x,y) = arg;
|
||||
me._size = [x,y];
|
||||
return me.update();
|
||||
},
|
||||
# Needs to be called when the size of the content changes.
|
||||
update: func()
|
||||
{
|
||||
# var offset = [ me._content_offset[0] - me._content_pos[0],
|
||||
# me._content_offset[1] - me._content_pos[1] ];
|
||||
|
||||
me.getContent().setTranslation(10, 10);
|
||||
me.getContent().setSize([me._size[0] - 20, me._size[1] - 20]);
|
||||
|
||||
me._view.update(me);
|
||||
me.getContent().update();
|
||||
|
||||
return me;
|
||||
},
|
||||
};
|
95
Nasal/canvas/gui/widgets/RadioButton.nas
Normal file
95
Nasal/canvas/gui/widgets/RadioButton.nas
Normal file
|
@ -0,0 +1,95 @@
|
|||
# RadioButton.nas : radio button, and group helper
|
||||
# to manage updating checked state conherently
|
||||
# Copyright (C) 2022 James Turner
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
gui.widgets.RadioButton = {
|
||||
new: func(parent, style, cfg)
|
||||
{
|
||||
cfg["type"] = "radio";
|
||||
var m = gui.widgets.Button.new(parent, style, cfg);
|
||||
m._checkable = 1;
|
||||
|
||||
append(m.parents, gui.widgets.RadioButton);
|
||||
|
||||
if (contains(pr, "radioGroup") {
|
||||
pr.radioGroup.addRadio(m);
|
||||
}
|
||||
|
||||
return m;
|
||||
},
|
||||
|
||||
del: func()
|
||||
{
|
||||
var pr = getParent();
|
||||
if (contains(pr, "radioGroup") {
|
||||
pr.radioGroup.removeRadio(me);
|
||||
}
|
||||
},
|
||||
|
||||
setCheckable: nil,
|
||||
setChecked: func(checked = 1)
|
||||
{
|
||||
# call our base version
|
||||
me.parents[0].setChecked(checked);
|
||||
if (checked) {
|
||||
me._setRadioGroupSiblingsUnchecked();
|
||||
}
|
||||
},
|
||||
# protected members
|
||||
_setRadioGroupSiblingsUnchecked: func
|
||||
{
|
||||
var pr = getParent();
|
||||
if (contains(pr, "radioGroup") {
|
||||
pr.radioGroup._updateChecked(me);
|
||||
} else {
|
||||
# todo, remove me, it's okay to manually manage RadioButtons
|
||||
logprint(LOG_DEV, "No radio group defined");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
# auto manage radio-button checked state
|
||||
gui.widgets.RadioGroup = {
|
||||
new: func(nm = 'unnamed')
|
||||
{
|
||||
var m = {parents:[gui.widgets.RadioGroup, name:nm, radios:[]]};
|
||||
return m;
|
||||
},
|
||||
|
||||
addRadio: func(r)
|
||||
{
|
||||
if (r.parents[0] != RadioButton) {
|
||||
logprint(LOG_ALERT, "Adding non-RadioButton to RadioGroup")
|
||||
return;
|
||||
}
|
||||
append(me.radios, r);
|
||||
},
|
||||
|
||||
removeRadio: func(r)
|
||||
{
|
||||
if (r.parents[0] != RadioButton) {
|
||||
logprint(LOG_ALERT, "Remove non-RadioButton from RadioGroup")
|
||||
return;
|
||||
}
|
||||
|
||||
# should we update some other item to be checked?
|
||||
me.radios.remove(r);
|
||||
},
|
||||
|
||||
setEnabled: func(doEnable = 1)
|
||||
{
|
||||
foreach (var r : me.radios) {
|
||||
r.setEnabled(doEnable);
|
||||
}
|
||||
},
|
||||
|
||||
# protected methods
|
||||
# update check state of all radios in the group
|
||||
_updateChecked: func(active)
|
||||
{
|
||||
foreach (var r : me.radios) {
|
||||
r.setChecked(r == active);
|
||||
}
|
||||
}
|
||||
};
|
47
Nasal/canvas/gui/widgets/Rule.nas
Normal file
47
Nasal/canvas/gui/widgets/Rule.nas
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Rule.nas : horizontal or vertical dividing line,
|
||||
# optionally with a text label, eg to name a section
|
||||
# Copyright (C) 2022 James Turner
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
||||
gui.widgets.HorizontalRule = {
|
||||
new: func(parent, style, cfg)
|
||||
{
|
||||
var m = gui.Widget.new(gui.widgets.HorizontalRule);
|
||||
m._cfg = Config.new(cfg);
|
||||
m._focus_policy = m.NoFocus;
|
||||
m._setView( style.createWidget(parent, "rule", m._cfg) );
|
||||
|
||||
# should ask Style the rule height, not hard-code 1px
|
||||
m.setLayoutMinimumSize([16, 1]);
|
||||
m.setLayoutSizeHint([m._MAX_SIZE, 1]); # expand to fill
|
||||
m.setLayoutMaximumSize([m._MAX_SIZE, 1]);
|
||||
return m;
|
||||
},
|
||||
setText: func(text)
|
||||
{
|
||||
me._view.setText(me, text);
|
||||
return me;
|
||||
}
|
||||
};
|
||||
|
||||
gui.widgets.VerticalRule = {
|
||||
new: func(parent, style, cfg)
|
||||
{
|
||||
var m = gui.Widget.new(gui.widgets.VerticalRule);
|
||||
m._cfg = Config.new(cfg);
|
||||
m._focus_policy = m.NoFocus;
|
||||
m._setView( style.createWidget(parent, "rule", m._cfg) );
|
||||
|
||||
# should ask Style the rule height, not hard-code 1px
|
||||
m.setLayoutMinimumSize([1, 16]);
|
||||
m.setLayoutSizeHint([1, m._MAX_SIZE]); # expand to fill
|
||||
m.setLayoutMaximumSize([1, m._MAX_SIZE]);
|
||||
return m;
|
||||
},
|
||||
setText: func(text)
|
||||
{
|
||||
me._view.setText(me, text);
|
||||
return me;
|
||||
}
|
||||
};
|
54
Nasal/canvas/gui/widgets/Slider.nas
Normal file
54
Nasal/canvas/gui/widgets/Slider.nas
Normal file
|
@ -0,0 +1,54 @@
|
|||
gui.widgets.Slider = {
|
||||
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._maxValue = 100;
|
||||
m._value = 50;
|
||||
m._pageStep = 10;
|
||||
m._numTicks = 10;
|
||||
|
||||
|
||||
if( style != nil ) {
|
||||
m._setView( style.createWidget(parent, cfg.get("type", "slider"), cfg) );
|
||||
m._view.updateRanges(m._minValue, m._maxValue, m._numTicks);
|
||||
}
|
||||
|
||||
return m;
|
||||
},
|
||||
|
||||
setValue: func(val)
|
||||
{
|
||||
if( me._view != nil ) {
|
||||
me._view.setNormValue(me._normValue());
|
||||
}
|
||||
return me;
|
||||
},
|
||||
|
||||
|
||||
|
||||
# protected:
|
||||
_setView: func(view)
|
||||
{
|
||||
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("drag", func(e) e.stopPropagation());
|
||||
},
|
||||
|
||||
# return value as its normalised equivalent
|
||||
_normValue: func
|
||||
{
|
||||
var range = me._maxValue - me._minValue;
|
||||
var v = math.clamp(me._value, me._minValue, me._maxValue) - me._minValue;
|
||||
return v / range;
|
||||
}
|
||||
};
|
BIN
gui/styles/AmbianceClassic/widgets/hrule.png
Normal file
BIN
gui/styles/AmbianceClassic/widgets/hrule.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 858 B |
Loading…
Add table
Reference in a new issue