Canvas GUI: Basic widget, focus and theming support.
|
@ -1,3 +1,46 @@
|
|||
var gui = {
|
||||
widgets: {},
|
||||
focused_window: nil
|
||||
};
|
||||
|
||||
var gui_dir = getprop("/sim/fg-root") ~ "/Nasal/canvas/gui/";
|
||||
var loadGUIFile = func(file) io.load_nasal(gui_dir ~ file, "canvas");
|
||||
var loadWidget = func(name) loadGUIFile("widgets/" ~ name ~ ".nas");
|
||||
|
||||
loadGUIFile("Style.nas");
|
||||
loadGUIFile("Widget.nas");
|
||||
loadGUIFile("styles/DefaultStyle.nas");
|
||||
loadWidget("Button");
|
||||
|
||||
var style = DefaultStyle.new("AmbianceClassic");
|
||||
var WindowButton = {
|
||||
new: func(parent, name)
|
||||
{
|
||||
var m = {
|
||||
parents: [WindowButton, gui.widgets.Button.new(parent, nil, {"flat": 1})],
|
||||
_name: name
|
||||
};
|
||||
m._focus_policy = m.NoFocus;
|
||||
m._setRoot( parent.createChild("image", "WindowButton-" ~ name) );
|
||||
return m;
|
||||
},
|
||||
# protected:
|
||||
_onStateChange: func
|
||||
{
|
||||
var file = style._dir_decoration ~ "/" ~ me._name;
|
||||
file ~= me._window._focused ? "_focused" : "_unfocused";
|
||||
|
||||
if( me._active )
|
||||
file ~= "_pressed";
|
||||
else if( me._hover )
|
||||
file ~= "_prelight";
|
||||
else if( me._window._focused )
|
||||
file ~= "_normal";
|
||||
|
||||
me._root.set("file", file ~ ".png");
|
||||
}
|
||||
};
|
||||
|
||||
var Window = {
|
||||
# Constructor
|
||||
#
|
||||
|
@ -7,7 +50,10 @@ var Window = {
|
|||
var ghost = _newWindowGhost(id);
|
||||
var m = {
|
||||
parents: [Window, PropertyElement, ghost],
|
||||
_node: props.wrapNode(ghost._node_ghost)
|
||||
_node: props.wrapNode(ghost._node_ghost),
|
||||
_focused: 0,
|
||||
_focused_widget: nil,
|
||||
_widgets: []
|
||||
};
|
||||
|
||||
m.setInt("content-size[0]", size[0]);
|
||||
|
@ -15,6 +61,7 @@ var Window = {
|
|||
|
||||
# TODO better default position
|
||||
m.move(0,0);
|
||||
m.setFocus();
|
||||
|
||||
# arg = [child, listener_node, mode, is_child_event]
|
||||
setlistener(m._node, func m._propCallback(arg[0], arg[2]), 0, 2);
|
||||
|
@ -26,6 +73,8 @@ var Window = {
|
|||
# Destructor
|
||||
del: func
|
||||
{
|
||||
me.clearFocus();
|
||||
|
||||
if( me["_canvas"] != nil )
|
||||
{
|
||||
var placements = me._canvas.texture.getChildren("placement");
|
||||
|
@ -93,6 +142,42 @@ var Window = {
|
|||
{
|
||||
return wrapCanvas(me._getCanvasDecoration());
|
||||
},
|
||||
addWidget: func(w)
|
||||
{
|
||||
append(me._widgets, w);
|
||||
w._window = me;
|
||||
if( size(me._widgets) == 2 )
|
||||
w.setFocus();
|
||||
w._onStateChange();
|
||||
return me;
|
||||
},
|
||||
#
|
||||
setFocus: func
|
||||
{
|
||||
if( me._focused )
|
||||
return me;
|
||||
|
||||
if( gui.focused_window != nil )
|
||||
gui.focused_window.clearFocus();
|
||||
|
||||
me._focused = 1;
|
||||
# me.onFocusIn();
|
||||
me._onStateChange();
|
||||
gui.focused_window = me;
|
||||
return me;
|
||||
},
|
||||
#
|
||||
clearFocus: func
|
||||
{
|
||||
if( !me._focused )
|
||||
return me;
|
||||
|
||||
me._focused = 0;
|
||||
# me.onFocusOut();
|
||||
me._onStateChange();
|
||||
gui.focused_window = nil;
|
||||
return me;
|
||||
},
|
||||
setPosition: func(x, y)
|
||||
{
|
||||
me.setInt("tf/t[0]", x);
|
||||
|
@ -114,6 +199,26 @@ var Window = {
|
|||
# on writing the z-index the window always is moved to the top of all other
|
||||
# windows with the same z-index.
|
||||
me.setInt("z-index", me.get("z-index", 0));
|
||||
|
||||
me.setFocus();
|
||||
},
|
||||
# protected:
|
||||
_onStateChange: func
|
||||
{
|
||||
if( me._getCanvasDecoration() != nil )
|
||||
{
|
||||
# Stronger shadow for focused windows
|
||||
me.getCanvasDecoration()
|
||||
.set("image[1]/fill", me._focused ? "#000000" : "rgba(0,0,0,0.5)");
|
||||
|
||||
var suffix = me._focused ? "" : "-unfocused";
|
||||
me._title_bar_bg.set("fill", style.getColor("title" ~ suffix));
|
||||
me._title.set( "fill", style.getColor("title-text" ~ suffix));
|
||||
me._top_line.set( "stroke", style.getColor("title-highlight" ~ suffix));
|
||||
}
|
||||
|
||||
foreach(var w; me._widgets)
|
||||
w._onStateChange();
|
||||
},
|
||||
# private:
|
||||
_propCallback: func(child, mode)
|
||||
|
@ -188,28 +293,25 @@ var Window = {
|
|||
|
||||
var group_deco = canvas_deco.getGroup("decoration");
|
||||
var title_bar = group_deco.createChild("group", "title_bar");
|
||||
title_bar
|
||||
.rect( 0, 0,
|
||||
me.get("size[0]"),
|
||||
me.get("size[1]"), #25,
|
||||
{"border-top-radius": border_radius} )
|
||||
.setColorFill(0.25,0.24,0.22)
|
||||
.setStrokeLineWidth(0);
|
||||
|
||||
var style_dir = "gui/styles/AmbianceClassic/";
|
||||
me._title_bar_bg =
|
||||
title_bar.rect( 0, 0,
|
||||
me.get("size[0]"),
|
||||
me.get("size[1]"),
|
||||
{"border-top-radius": border_radius} );
|
||||
me._top_line = title_bar.createChild("path", "top-line")
|
||||
.moveTo(border_radius - 2, 2)
|
||||
.lineTo(me.get("size[0]") - border_radius + 2, 2);
|
||||
|
||||
# close icon
|
||||
var x = 10;
|
||||
var y = 3;
|
||||
var w = 19;
|
||||
var h = 19;
|
||||
var ico = title_bar.createChild("image", "icon-close")
|
||||
.set("file", style_dir ~ "close_focused_normal.png")
|
||||
.setTranslation(x,y);
|
||||
ico.addEventListener("click", func me.del());
|
||||
ico.addEventListener("mouseover", func ico.set("file", style_dir ~ "close_focused_prelight.png"));
|
||||
ico.addEventListener("mousedown", func ico.set("file", style_dir ~ "close_focused_pressed.png"));
|
||||
ico.addEventListener("mouseout", func ico.set("file", style_dir ~ "close_focused_normal.png"));
|
||||
|
||||
var button_close = WindowButton.new(title_bar, "close")
|
||||
.move(x, y);
|
||||
button_close.onClick = func me.del();
|
||||
me.addWidget(button_close);
|
||||
|
||||
# title
|
||||
me._title = title_bar.createChild("text", "title")
|
||||
|
@ -223,10 +325,8 @@ var Window = {
|
|||
me._node.getNode("title", 1).alias(me._title._node.getPath() ~ "/text");
|
||||
me.set("title", title);
|
||||
|
||||
title_bar.addEventListener("drag", func(e) {
|
||||
if( !ico.equals(e.target) )
|
||||
me.move(e.deltaX, e.deltaY);
|
||||
});
|
||||
title_bar.addEventListener("drag", func(e) me.move(e.deltaX, e.deltaY));
|
||||
me._onStateChange();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
50
Nasal/canvas/gui/Style.nas
Normal file
|
@ -0,0 +1,50 @@
|
|||
gui.Style = {
|
||||
new: func(name)
|
||||
{
|
||||
var root_node = props.globals.getNode("/sim/gui/canvas", 1)
|
||||
.addChild("style");
|
||||
var path = getprop("/sim/fg-root") ~ "/gui/styles/" ~ name;
|
||||
|
||||
var m = {
|
||||
parents: [gui.Style],
|
||||
_path: path,
|
||||
_node: io.read_properties(path ~ "/style.xml", root_node),
|
||||
_colors: {}
|
||||
};
|
||||
|
||||
# parse theme colors
|
||||
var comp_names = ["red", "green", "blue", "alpha"];
|
||||
var colors = m._node.getChild("colors");
|
||||
if( colors != nil )
|
||||
{
|
||||
foreach(var color; colors.getChildren())
|
||||
{
|
||||
var str = "rgba(";
|
||||
for(var i = 0; i < size(comp_names); i += 1)
|
||||
{
|
||||
if( i > 0 )
|
||||
str ~= ",";
|
||||
var val = color.getValue(comp_names[i]);
|
||||
if( val == nil )
|
||||
val = 1;
|
||||
if( i < 3 )
|
||||
str ~= int(val * 255 + 0.5);
|
||||
else
|
||||
str ~= int(val * 100) / 100;
|
||||
}
|
||||
m._colors[ color.getName() ] = str ~ ")";
|
||||
}
|
||||
}
|
||||
|
||||
m._dir_decoration =
|
||||
m._path ~ "/" ~ (m._node.getValue("folders/decoration") or "decoration");
|
||||
m._dir_widgets =
|
||||
m._path ~ "/" ~ (m._node.getValue("folders/widgets") or "widgets");
|
||||
|
||||
return m;
|
||||
},
|
||||
getColor: func(name, def = "#00ffff")
|
||||
{
|
||||
return me._colors[name] or def;
|
||||
}
|
||||
};
|
83
Nasal/canvas/gui/Widget.nas
Normal file
|
@ -0,0 +1,83 @@
|
|||
gui.Widget = {
|
||||
# enum FocusPolicy:
|
||||
NoFocus: 0,
|
||||
TabFocus: 1,
|
||||
ClickFocus: 2,
|
||||
StrongFocus: 1 + 2,
|
||||
|
||||
#
|
||||
new: func(derived)
|
||||
{
|
||||
return {
|
||||
parents: [derived, gui.Widget],
|
||||
_focused: 0,
|
||||
_focus_policy: gui.Widget.NoFocus,
|
||||
_hover: 0,
|
||||
_root: nil
|
||||
};
|
||||
},
|
||||
# Move the widget to the given position (relative to its parent)
|
||||
move: func(x, y)
|
||||
{
|
||||
me._root.setTranslation(x, y);
|
||||
return me;
|
||||
},
|
||||
#
|
||||
setFocus: func
|
||||
{
|
||||
if( me._focused )
|
||||
return me;
|
||||
|
||||
if( me._window._focused_widget != nil )
|
||||
me._window._focused_widget.clearFocus();
|
||||
|
||||
me._focused = 1;
|
||||
me._window._focused_widget = me;
|
||||
|
||||
me.onFocusIn();
|
||||
me._onStateChange();
|
||||
|
||||
return me;
|
||||
},
|
||||
#
|
||||
clearFocus: func
|
||||
{
|
||||
if( !me._focused )
|
||||
return me;
|
||||
|
||||
me._focused = 0;
|
||||
me._window._focused_widget = nil;
|
||||
|
||||
me.onFocusOut();
|
||||
me._onStateChange();
|
||||
|
||||
return me;
|
||||
},
|
||||
onFocusIn: func {},
|
||||
onFocusOut: func {},
|
||||
onMouseEnter: func {},
|
||||
onMouseLeave: func {},
|
||||
# protected:
|
||||
_onStateChange: func {},
|
||||
_setRoot: func(el)
|
||||
{
|
||||
me._root = el;
|
||||
el.addEventListener("mouseenter", func {
|
||||
me._hover = 1;
|
||||
me.onMouseEnter();
|
||||
me._onStateChange();
|
||||
});
|
||||
el.addEventListener("mousedown", func {
|
||||
if( bits.test(me._focus_policy, me.ClickFocus / 2) )
|
||||
{
|
||||
me.setFocus();
|
||||
me._window.setFocus();
|
||||
}
|
||||
});
|
||||
el.addEventListener("mouseleave", func {
|
||||
me._hover = 0;
|
||||
me.onMouseLeave();
|
||||
me._onStateChange();
|
||||
});
|
||||
}
|
||||
};
|
87
Nasal/canvas/gui/styles/DefaultStyle.nas
Normal file
|
@ -0,0 +1,87 @@
|
|||
var DefaultStyle = {
|
||||
new: func(name)
|
||||
{
|
||||
return { parents: [gui.Style.new(name), DefaultStyle] };
|
||||
},
|
||||
createWidget: func(parent, type, cfg)
|
||||
{
|
||||
var factory = me.widgets[type];
|
||||
if( factory == nil )
|
||||
{
|
||||
debug.warn("DefaultStyle: unknown widget type (" ~ type ~ ")");
|
||||
return nil;
|
||||
}
|
||||
|
||||
return factory.new(parent, me, cfg);
|
||||
},
|
||||
widgets: {}
|
||||
};
|
||||
|
||||
# A button
|
||||
DefaultStyle.widgets.button = {
|
||||
padding: [6, 8, 6, 8],
|
||||
new: func(parent, style, cfg)
|
||||
{
|
||||
var button = {
|
||||
parents: [DefaultStyle.widgets.button],
|
||||
element: parent.createChild("group", "button"),
|
||||
size: cfg.get("size", [26, 26]),
|
||||
_style: style
|
||||
};
|
||||
|
||||
button._bg =
|
||||
button.element.rect( 3,
|
||||
3,
|
||||
button.size[0] - 6,
|
||||
button.size[1] - 6,
|
||||
{"border-radius": 5} );
|
||||
button._border =
|
||||
button.element.createChild("image", "button")
|
||||
.set("slice", "10 12") #"7")
|
||||
.setSize(button.size);
|
||||
button._label =
|
||||
button.element.createChild("text")
|
||||
.setFont("LiberationFonts/LiberationSans-Regular.ttf")
|
||||
.set("character-size", 14)
|
||||
.set("alignment", "center-baseline");
|
||||
return button;
|
||||
},
|
||||
setText: func(text)
|
||||
{
|
||||
me._label.set("text", text);
|
||||
},
|
||||
update: func(active, focused, hover, backdrop)
|
||||
{
|
||||
var file = me._style._dir_widgets ~ "/";
|
||||
if( backdrop )
|
||||
{
|
||||
file ~= "backdrop-";
|
||||
me._label.set("fill", me._style.getColor("backdrop_fg_color"));
|
||||
}
|
||||
else
|
||||
me._label.set("fill", me._style.getColor("fg_color"));
|
||||
file ~= "button";
|
||||
|
||||
if( active )
|
||||
{
|
||||
file ~= "-active";
|
||||
me._label.setTranslation(me.size[0] / 2 + 1, me.size[1] / 2 + 6);
|
||||
}
|
||||
else
|
||||
me._label.setTranslation(me.size[0] / 2, me.size[1] / 2 + 5);
|
||||
|
||||
|
||||
if( focused and !backdrop )
|
||||
file ~= "-focused";
|
||||
|
||||
if( hover and !active )
|
||||
{
|
||||
file ~= "-hover";
|
||||
me._bg.set("fill", me._style.getColor("button_bg_color_hover"));
|
||||
}
|
||||
else
|
||||
me._bg.set("fill", me._style.getColor("button_bg_color"));
|
||||
|
||||
me._border.set("file", file ~ ".png");
|
||||
}
|
||||
};
|
86
Nasal/canvas/gui/widgets/Button.nas
Normal file
|
@ -0,0 +1,86 @@
|
|||
var Config = {
|
||||
new: func(cfg)
|
||||
{
|
||||
var m = {
|
||||
parents: [Config],
|
||||
_cfg: cfg
|
||||
};
|
||||
if( typeof(m._cfg) != "hash" )
|
||||
m._cfg = {};
|
||||
|
||||
return m;
|
||||
},
|
||||
get: func(key, default = nil)
|
||||
{
|
||||
var val = me._cfg[key];
|
||||
if( val != nil )
|
||||
return val;
|
||||
|
||||
return default;
|
||||
}
|
||||
};
|
||||
|
||||
gui.widgets.Button = {
|
||||
new: func(parent, style, cfg)
|
||||
{
|
||||
var cfg = Config.new(cfg);
|
||||
var m = gui.Widget.new(gui.widgets.Button);
|
||||
m._focus_policy = m.StrongFocus;
|
||||
m._active = 0;
|
||||
m._flat = cfg.get("flat", 0);
|
||||
|
||||
if( style != nil and !m._flat )
|
||||
{
|
||||
m._button = style.createWidget(parent, "button", cfg);
|
||||
m._setRoot(m._button.element);
|
||||
}
|
||||
|
||||
return m;
|
||||
},
|
||||
setText: func(text)
|
||||
{
|
||||
me._button.setText(text);
|
||||
return me;
|
||||
},
|
||||
setActive: func
|
||||
{
|
||||
if( me._active )
|
||||
return me;
|
||||
|
||||
me._active = 1;
|
||||
me._onStateChange();
|
||||
return me;
|
||||
},
|
||||
clearActive: func
|
||||
{
|
||||
if( !me._active )
|
||||
return me;
|
||||
|
||||
me._active = 0;
|
||||
me._onStateChange();
|
||||
return me;
|
||||
},
|
||||
onClick: func {},
|
||||
# protected:
|
||||
_onStateChange: func
|
||||
{
|
||||
if( me._button != nil )
|
||||
me._button.update(me._active, me._focused, me._hover, !me._window._focused);
|
||||
},
|
||||
_setRoot: func(el)
|
||||
{
|
||||
el.addEventListener("mousedown", func me.setActive());
|
||||
el.addEventListener("mouseup", func me.clearActive());
|
||||
|
||||
# Use 'call' to ensure 'me' is not set and can be used in the closure of
|
||||
# custom callbacks. TODO pass 'me' as argument?
|
||||
el.addEventListener("click", func call(me.onClick));
|
||||
|
||||
el.addEventListener("mouseleave",func me.clearActive());
|
||||
el.addEventListener("drag", func(e) e.stopPropagation());
|
||||
|
||||
call(gui.Widget._setRoot, [el], me);
|
||||
}
|
||||
};
|
||||
|
||||
return;
|
Before Width: | Height: | Size: 833 B After Width: | Height: | Size: 833 B |
Before Width: | Height: | Size: 858 B After Width: | Height: | Size: 858 B |
Before Width: | Height: | Size: 589 B After Width: | Height: | Size: 589 B |
BIN
gui/styles/AmbianceClassic/decoration/close_unfocused.png
Normal file
After Width: | Height: | Size: 528 B |
After Width: | Height: | Size: 882 B |
83
gui/styles/AmbianceClassic/style.xml
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0"?>
|
||||
|
||||
<PropertyList>
|
||||
<!--
|
||||
<folders>
|
||||
<decoration type="string">decoration</decoration>
|
||||
<widgets type="string">widgets</widgets>
|
||||
</folders>
|
||||
-->
|
||||
<colors>
|
||||
|
||||
<!-- Window decoration colors -->
|
||||
<title>
|
||||
<red type="float">0.275</red>
|
||||
<green type="float">0.271</green>
|
||||
<blue type="float">0.251</blue>
|
||||
</title>
|
||||
<title-unfocused>
|
||||
<red type="float">0.235</red>
|
||||
<green type="float">0.231</green>
|
||||
<blue type="float">0.216</blue>
|
||||
</title-unfocused>
|
||||
<title-text>
|
||||
<red type="float">0.875</red>
|
||||
<green type="float">0.859</green>
|
||||
<blue type="float">0.824</blue>
|
||||
</title-text>
|
||||
<title-text-unfocused>
|
||||
<red type="float">0.502</red>
|
||||
<green type="float">0.490</green>
|
||||
<blue type="float">0.471</blue>
|
||||
</title-text-unfocused>
|
||||
<title-highlight>
|
||||
<red type="float">0.365</red>
|
||||
<green type="float">0.361</green>
|
||||
<blue type="float">0.341</blue>
|
||||
</title-highlight>
|
||||
<title-highlight-unfocused>
|
||||
<red type="float">0.278</red>
|
||||
<green type="float">0.275</green>
|
||||
<blue type="float">0.259</blue>
|
||||
</title-highlight-unfocused>
|
||||
|
||||
<!-- default colors for all GUI objects -->
|
||||
<bg_color>
|
||||
<red type="float">0.949</red>
|
||||
<green type="float">0.945</green>
|
||||
<blue type="float">0.941</blue>
|
||||
</bg_color>
|
||||
|
||||
<fg_color>
|
||||
<red type="float">0.298</red>
|
||||
<green type="float">0.298</green>
|
||||
<blue type="float">0.298</blue>
|
||||
</fg_color>
|
||||
|
||||
<text_color>
|
||||
<red type="float">0.235</red>
|
||||
<green type="float">0.235</green>
|
||||
<blue type="float">0.235</blue>
|
||||
</text_color>
|
||||
|
||||
<backdrop_fg_color>
|
||||
<red type="float">0.428</red>
|
||||
<green type="float">0.427</green>
|
||||
<blue type="float">0.427</blue>
|
||||
</backdrop_fg_color>
|
||||
|
||||
<button_bg_color>
|
||||
<red type="float">0.949</red>
|
||||
<green type="float">0.945</green>
|
||||
<blue type="float">0.941</blue>
|
||||
</button_bg_color>
|
||||
|
||||
<button_bg_color_hover>
|
||||
<red type="float">0.996</red>
|
||||
<green type="float">0.992</green>
|
||||
<blue type="float">0.988</blue>
|
||||
</button_bg_color_hover>
|
||||
|
||||
</colors>
|
||||
|
||||
</PropertyList>
|
After Width: | Height: | Size: 909 B |
BIN
gui/styles/AmbianceClassic/widgets/backdrop-button-active.png
Normal file
After Width: | Height: | Size: 799 B |
BIN
gui/styles/AmbianceClassic/widgets/backdrop-button-hover.png
Normal file
After Width: | Height: | Size: 909 B |
BIN
gui/styles/AmbianceClassic/widgets/backdrop-button.png
Normal file
After Width: | Height: | Size: 848 B |
BIN
gui/styles/AmbianceClassic/widgets/button-active-focused.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
gui/styles/AmbianceClassic/widgets/button-active.png
Normal file
After Width: | Height: | Size: 848 B |
BIN
gui/styles/AmbianceClassic/widgets/button-focused-hover.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
gui/styles/AmbianceClassic/widgets/button-focused.png
Normal file
After Width: | Height: | Size: 977 B |
BIN
gui/styles/AmbianceClassic/widgets/button-hover.png
Normal file
After Width: | Height: | Size: 934 B |
BIN
gui/styles/AmbianceClassic/widgets/button.png
Normal file
After Width: | Height: | Size: 858 B |