diff --git a/Nasal/canvas/gui.nas b/Nasal/canvas/gui.nas index bab83e092..e1847d993 100644 --- a/Nasal/canvas/gui.nas +++ b/Nasal/canvas/gui.nas @@ -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(); } }; diff --git a/Nasal/canvas/gui/Style.nas b/Nasal/canvas/gui/Style.nas new file mode 100644 index 000000000..244177c26 --- /dev/null +++ b/Nasal/canvas/gui/Style.nas @@ -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; + } +}; diff --git a/Nasal/canvas/gui/Widget.nas b/Nasal/canvas/gui/Widget.nas new file mode 100644 index 000000000..a6b37e707 --- /dev/null +++ b/Nasal/canvas/gui/Widget.nas @@ -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(); + }); + } +}; diff --git a/Nasal/canvas/gui/styles/DefaultStyle.nas b/Nasal/canvas/gui/styles/DefaultStyle.nas new file mode 100644 index 000000000..01799d147 --- /dev/null +++ b/Nasal/canvas/gui/styles/DefaultStyle.nas @@ -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"); + } +}; diff --git a/Nasal/canvas/gui/widgets/Button.nas b/Nasal/canvas/gui/widgets/Button.nas new file mode 100644 index 000000000..08e3ccb51 --- /dev/null +++ b/Nasal/canvas/gui/widgets/Button.nas @@ -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; diff --git a/gui/styles/AmbianceClassic/close_focused_normal.png b/gui/styles/AmbianceClassic/decoration/close_focused_normal.png similarity index 100% rename from gui/styles/AmbianceClassic/close_focused_normal.png rename to gui/styles/AmbianceClassic/decoration/close_focused_normal.png diff --git a/gui/styles/AmbianceClassic/close_focused_prelight.png b/gui/styles/AmbianceClassic/decoration/close_focused_prelight.png similarity index 100% rename from gui/styles/AmbianceClassic/close_focused_prelight.png rename to gui/styles/AmbianceClassic/decoration/close_focused_prelight.png diff --git a/gui/styles/AmbianceClassic/close_focused_pressed.png b/gui/styles/AmbianceClassic/decoration/close_focused_pressed.png similarity index 100% rename from gui/styles/AmbianceClassic/close_focused_pressed.png rename to gui/styles/AmbianceClassic/decoration/close_focused_pressed.png diff --git a/gui/styles/AmbianceClassic/decoration/close_unfocused.png b/gui/styles/AmbianceClassic/decoration/close_unfocused.png new file mode 100644 index 000000000..03eb5a695 Binary files /dev/null and b/gui/styles/AmbianceClassic/decoration/close_unfocused.png differ diff --git a/gui/styles/AmbianceClassic/decoration/close_unfocused_prelight.png b/gui/styles/AmbianceClassic/decoration/close_unfocused_prelight.png new file mode 100644 index 000000000..6e5ec3d9e Binary files /dev/null and b/gui/styles/AmbianceClassic/decoration/close_unfocused_prelight.png differ diff --git a/gui/styles/AmbianceClassic/style.xml b/gui/styles/AmbianceClassic/style.xml new file mode 100644 index 000000000..6ec09432d --- /dev/null +++ b/gui/styles/AmbianceClassic/style.xml @@ -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> diff --git a/gui/styles/AmbianceClassic/widgets/backdrop-button-active-hover.png b/gui/styles/AmbianceClassic/widgets/backdrop-button-active-hover.png new file mode 100644 index 000000000..7ff22c80b Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/backdrop-button-active-hover.png differ diff --git a/gui/styles/AmbianceClassic/widgets/backdrop-button-active.png b/gui/styles/AmbianceClassic/widgets/backdrop-button-active.png new file mode 100644 index 000000000..c71f92074 Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/backdrop-button-active.png differ diff --git a/gui/styles/AmbianceClassic/widgets/backdrop-button-hover.png b/gui/styles/AmbianceClassic/widgets/backdrop-button-hover.png new file mode 100644 index 000000000..7ff22c80b Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/backdrop-button-hover.png differ diff --git a/gui/styles/AmbianceClassic/widgets/backdrop-button.png b/gui/styles/AmbianceClassic/widgets/backdrop-button.png new file mode 100644 index 000000000..d357ed68c Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/backdrop-button.png differ diff --git a/gui/styles/AmbianceClassic/widgets/button-active-focused.png b/gui/styles/AmbianceClassic/widgets/button-active-focused.png new file mode 100644 index 000000000..71e24eb5b Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/button-active-focused.png differ diff --git a/gui/styles/AmbianceClassic/widgets/button-active.png b/gui/styles/AmbianceClassic/widgets/button-active.png new file mode 100644 index 000000000..d2d5a7f4f Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/button-active.png differ diff --git a/gui/styles/AmbianceClassic/widgets/button-focused-hover.png b/gui/styles/AmbianceClassic/widgets/button-focused-hover.png new file mode 100644 index 000000000..5d6e27690 Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/button-focused-hover.png differ diff --git a/gui/styles/AmbianceClassic/widgets/button-focused.png b/gui/styles/AmbianceClassic/widgets/button-focused.png new file mode 100644 index 000000000..f4e976f37 Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/button-focused.png differ diff --git a/gui/styles/AmbianceClassic/widgets/button-hover.png b/gui/styles/AmbianceClassic/widgets/button-hover.png new file mode 100644 index 000000000..abf23dc55 Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/button-hover.png differ diff --git a/gui/styles/AmbianceClassic/widgets/button.png b/gui/styles/AmbianceClassic/widgets/button.png new file mode 100644 index 000000000..c9090ae76 Binary files /dev/null and b/gui/styles/AmbianceClassic/widgets/button.png differ