diff --git a/examples/Interface/Palette.html b/examples/Interface/Palette.html
new file mode 100644
index 00000000..34d14f88
--- /dev/null
+++ b/examples/Interface/Palette.html
@@ -0,0 +1,71 @@
+
+
+
+
+ Paelette
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/paper.js b/src/paper.js
index cccc226d..f745adb9 100644
--- a/src/paper.js
+++ b/src/paper.js
@@ -123,6 +123,9 @@ var paper = new function() {
/*#*/ include('ui/Key.js');
/*#*/ include('ui/MouseEvent.js');
+/*#*/ include('ui/Palette.js');
+/*#*/ include('ui/Component.js');
+
/*#*/ include('tool/ToolEvent.js');
/*#*/ include('tool/Tool.js');
/*#*/ } // options.browser
diff --git a/src/ui/Component.js b/src/ui/Component.js
new file mode 100644
index 00000000..49c314a1
--- /dev/null
+++ b/src/ui/Component.js
@@ -0,0 +1,165 @@
+/*
+ * Paper.js
+ *
+ * This file is part of Paper.js, a JavaScript Vector Graphics Library,
+ * based on Scriptographer.org and designed to be largely API compatible.
+ * http://paperjs.org/
+ * http://scriptographer.org/
+ *
+ * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
+ * http://lehni.org/ & http://jonathanpuckey.com/
+ *
+ * Distributed under the MIT license. See LICENSE file for details.
+ *
+ * All rights reserved.
+ */
+
+var Component = this.Component = Base.extend(Callback, /** @lends Component# */{
+ _events: [ 'onChange', 'onClick' ],
+
+ _types: {
+ 'boolean': {
+ type: 'checkbox',
+ value: 'checked'
+ },
+
+ string: {
+ type: 'text'
+ },
+
+ number: {
+ type: 'number'
+ },
+
+ button: {
+ type: 'button'
+ },
+
+ text: {
+ tag: 'div',
+ value: 'text'
+ },
+
+ slider: {
+ type: 'range'
+ },
+
+ list: {
+ tag: 'select',
+ options: function() {
+ DomElement.removeChildren(this.element);
+ DomElement.create(Base.each(this._options, function(option) {
+ this.push('option', { value: option, text: option });
+ }, []), this.element);
+ },
+
+ value: function(value) {
+ DomElement.set(
+ DomElement.find('option[value="' + value + '"]', this.element),
+ 'selected', true);
+ }
+ }
+ },
+
+ initialize: function(obj) {
+ this._type = obj.type
+ || ('options' in obj
+ ? 'list'
+ : 'onClick' in obj
+ ? 'button'
+ : typeof value);
+ this._info = this._types[this._type] || { type: this._type };
+ var that = this,
+ fireChange = false;
+ this.element = DomElement.create(this._info.tag || 'input', {
+ type: this._info.type,
+ events: {
+ change: function() {
+ var key = that._info.value;
+ if (typeof key === 'function')
+ key = null;
+ var value = DomElement.get(that.element, key || 'value');
+ if (fireChange) {
+ that.palette.fire('change', that, that.name, value);
+ that.fire('change', value);
+ }
+ },
+ click: function() {
+ that.fire('click');
+ }
+ }
+ });
+ Base.each(obj, function(value, key) {
+ this[key] = value;
+ }, this);
+ this._defaultValue = this._value;
+ // Only fire change events after we have initalized
+ fireChange = true;
+ },
+
+ getType: function() {
+ return this._type;
+ },
+
+ getOptions: function() {
+ return this._options;
+ },
+
+ setOptions: function(options) {
+ this._options = options;
+ if (this._info.options)
+ this._info.options.call(this);
+ },
+
+ getValue: function() {
+ return this._value;
+ },
+
+ setValue: function(value) {
+ var key = this._info.value;
+ if (typeof key === 'function')
+ key.call(this, value);
+ else
+ DomElement.set(this.element, key || 'value', value);
+ this._value = value;
+ },
+
+ getRange: function() {
+ return [toFloat(DomElement.get(this.element, 'min')),
+ toFloat(DomElement.get(this.element, 'max'))];
+ },
+
+ setRange: function(arg0, arg1) {
+ if (!Array.isArray(arg0))
+ arg0 = [arg0, arg1];
+ DomElement.set(this.element, { min: arg0[0], max: arg0[1] });
+ },
+
+ getMin: function() {
+ return getRange()[0];
+ },
+
+ setMin: function(min) {
+ this.setRange(min, getMax());
+ },
+
+ getMax: function() {
+ return getRange()[1];
+ },
+
+ setMax: function(max) {
+ this.setRange(getMin(), max);
+ },
+
+ getStep: function() {
+ return toFloat(DomElement.get(this.element, 'step'));
+ },
+
+ setStep: function(step) {
+ DomElement.set(this.element, 'step', step);
+ },
+
+ reset: function() {
+ this.setValue(this._defaultValue);
+ }
+});
\ No newline at end of file
diff --git a/src/ui/Palette.js b/src/ui/Palette.js
new file mode 100644
index 00000000..4c3fde64
--- /dev/null
+++ b/src/ui/Palette.js
@@ -0,0 +1,70 @@
+/*
+ * Paper.js
+ *
+ * This file is part of Paper.js, a JavaScript Vector Graphics Library,
+ * based on Scriptographer.org and designed to be largely API compatible.
+ * http://paperjs.org/
+ * http://scriptographer.org/
+ *
+ * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
+ * http://lehni.org/ & http://jonathanpuckey.com/
+ *
+ * Distributed under the MIT license. See LICENSE file for details.
+ *
+ * All rights reserved.
+ */
+
+var Palette = this.Palette = Base.extend(Callback, /** @lends Palette# */{
+ _events: [ 'onChange' ],
+
+ initialize: function(title, components, values) {
+ var parent = DomElement.find('.paperjs-palettes')
+ || DomElement.find('body').appendChild(
+ DomElement.create('div', { 'class': 'paperjs-palettes' }));
+ var table = parent.appendChild(
+ DomElement.create('table', { 'class': 'paperjs-palette' }));
+ this._title = title;
+ if (!values)
+ values = {};
+ this._components = Base.each(components, function(component, name) {
+ if (!(component instanceof Component)) {
+ if (component.value == null)
+ component.value = values[name];
+ component.name = name;
+ component = components[name] = new Component(component);
+ }
+ component.palette = this;
+ // Make sure each component has an entry in values, so observers get
+ // installed further down.
+ if (values[name] === undefined)
+ values[name] = null;
+ var row = table.appendChild(
+ DomElement.create('tr', [
+ 'td', { text: (component.label || name) + ':' },
+ 'td', component.element
+ ])
+ );
+ }, this);
+ this._values = Base.each(values, function(value, name) {
+ // Replace each entry with an getter / setters so we can observe
+ // change.
+ Base.define(values, name, {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ get: function() {
+ return value;
+ },
+ set: function(val) {
+ value = val;
+ components[name].setValue(val);
+ }
+ });
+ });
+ },
+
+ reset: function() {
+ for (var i in this._components)
+ this._components[i].reset();
+ }
+});