Browse Source

Added style class settings in components and using it to read a filter setting.

David Piuva 2 years ago
parent
commit
b18e2d92cc

+ 113 - 20
Source/DFPSR/gui/VisualTheme.cpp

@@ -1,6 +1,6 @@
 // zlib open source license
 //
-// Copyright (c) 2018 to 2022 David Forsgren Piuva
+// Copyright (c) 2018 to 2023 David Forsgren Piuva
 // 
 // This software is provided 'as-is', without any express or implied
 // warranty. In no event will the authors be held liable for any damages
@@ -191,6 +191,8 @@ UR"QUOTE(
 	; Fall back on the Button method if a component's class could not be recognized.
 	[Button]
 		rounding = 12
+		filter = 1
+		method = "Button"
 	[ListBox]
 		method = "HardRectangle"
 	[TextBox]
@@ -242,7 +244,7 @@ struct KeywordEntry {
 	MACRO_NAME(LOCATION strings)
 
 #define RETURN_TRUE_IF_SETTING_EXISTS(COLLECTION) \
-	for (int64_t i = 0; i < COLLECTION.length(); i++) { \
+	for (int i = 0; i < COLLECTION.length(); i++) { \
 		if (string_caseInsensitiveMatch(COLLECTION[i].key, key)) { \
 			return true; \
 		} \
@@ -281,7 +283,7 @@ struct ClassSettings {
 	// Post-condition: Returns true iff the key was found for the expected type.
 	// Side-effect: Writes the value of the found key iff found.
 	bool getString(String &target, const ReadableString &key) {
-		for (int64_t i = 0; i < this->strings.length(); i++) {
+		for (int i = 0; i < this->strings.length(); i++) {
 			if (string_caseInsensitiveMatch(this->strings[i].key, key)) {
 				target = this->strings[i].value;
 				return true;
@@ -289,6 +291,28 @@ struct ClassSettings {
 		}
 		return false;
 	}
+	// Post-condition: Returns true iff the key was found for the expected type.
+	// Side-effect: Writes the value of the found key iff found.
+	bool getImage(PersistentImage &target, const ReadableString &key) {
+		for (int i = 0; i < this->colorImages.length(); i++) {
+			if (string_caseInsensitiveMatch(this->colorImages[i].key, key)) {
+				target = this->colorImages[i].value;
+				return true;
+			}
+		}
+		return false;
+	}
+	// Post-condition: Returns true iff the key was found for the expected type.
+	// Side-effect: Writes the value of the found key iff found.
+	bool getScalar(FixedPoint &target, const ReadableString &key) {
+		for (int i = 0; i < this->scalars.length(); i++) {
+			if (string_caseInsensitiveMatch(this->scalars[i].key, key)) {
+				target = this->scalars[i].value;
+				return true;
+			}
+		}
+		return false;
+	}
 };
 
 // TODO: Make it easy for visual components to ask the theme for additional resources such as custom fonts, text offset from pressing buttons and fixed dimensions for scroll lists to match fixed-size images.
@@ -296,14 +320,14 @@ class VisualThemeImpl {
 public:
 	MediaMachine machine;
 	List<ClassSettings> settings;
-	int32_t getClassIndex(const ReadableString& className) {
-		for (int64_t i = 0; i < this->settings.length(); i++) { if (string_caseInsensitiveMatch(this->settings[i].className, className)) { return i; } }
+	int getClassIndex(const ReadableString& className) {
+		for (int i = 0; i < this->settings.length(); i++) { if (string_caseInsensitiveMatch(this->settings[i].className, className)) { return i; } }
 		return settings.pushConstructGetIndex(className);
 	}
 	VisualThemeImpl(const MediaMachine &machine, const ReadableString &styleSettings, const ReadableString &fromPath) : machine(machine) {
 		this->settings.pushConstruct(U"default");
 		config_parse_ini(styleSettings, [this, fromPath](const ReadableString& block, const ReadableString& key, const ReadableString& value) {
-			int32_t classIndex = (string_length(block) == 0) ? 0 : this->getClassIndex(block);
+			int classIndex = (string_length(block) == 0) ? 0 : this->getClassIndex(block);
 			this->settings[classIndex].setVariable(key, value, fromPath);
 		});
 	}
@@ -327,35 +351,104 @@ VisualTheme theme_createFromFile(const MediaMachine &machine, const ReadableStri
 	return theme_createFromText(machine, string_load(styleFilename), file_getRelativeParentFolder(styleFilename));
 }
 
-MediaMethod theme_getScalableImage(const VisualTheme &theme, const ReadableString &className) {
+bool theme_exists(const VisualTheme &theme) {
+	return theme.get() != nullptr;
+}
+
+int theme_getClassIndex(const VisualTheme &theme, const ReadableString &className) {
+	if (!theme_exists(theme)) {
+		return -1;
+	} else if (string_length(className) == 0) {
+		return 0;
+	} else {
+		int classIndex = theme->getClassIndex(className);
+		return (classIndex == -1) ? 0 : classIndex;
+	}
+}
+
+bool theme_class_exists(const VisualTheme &theme, const ReadableString &className) {
+	return theme_getClassIndex(theme, className) > 0;
+}
+
+String theme_selectClass(const VisualTheme &theme, const ReadableString &suggestedClassName, const ReadableString &fallbackClassName) {
+	return theme_class_exists(theme, suggestedClassName) ? suggestedClassName : fallbackClassName;
+}
+
+OrderedImageRgbaU8 theme_getImage(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName) {
+	if (!theme.get()) {
+		return OrderedImageRgbaU8();
+	}
+	int classIndex = theme->getClassIndex(className);
+	PersistentImage result;
+	if ((classIndex != -1 && theme->settings[classIndex].getImage(result, settingName))
+	                     || (theme->settings[0].getImage(result, settingName))) {
+		// If the class existed and it contained the setting or the setting could be found in the default class then return it.
+		return result.value;
+	} else {
+		return OrderedImageRgbaU8();
+	}
+}
+
+FixedPoint theme_getFixedPoint(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName, const FixedPoint &defaultValue) {
+	if (!theme.get()) {
+		return defaultValue;
+	}
+	int classIndex = theme->getClassIndex(className);
+	FixedPoint result;
+	if ((classIndex != -1 && theme->settings[classIndex].getScalar(result, settingName))
+	                     || (theme->settings[0].getScalar(result, settingName))) {
+		// If the class existed and it contained the setting or the setting could be found in the default class then return it.
+		return result;
+	} else {
+		return defaultValue;
+	}
+}
+
+int theme_getInteger(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName, const int &defaultValue) {
+	return fixedPoint_round(theme_getFixedPoint(theme, className, settingName, FixedPoint::fromWhole(defaultValue)));
+}
+
+ReadableString theme_getString(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName, const ReadableString &defaultValue) {
 	if (!theme.get()) {
-		throwError(U"theme_getScalableImage: Can't get scalable image from a non-existing theme!\n");
+		return defaultValue;
 	}
 	int classIndex = theme->getClassIndex(className);
-	if (classIndex == -1) {
-		throwError(U"theme_getScalableImage: Can't find any style class named ", className, U" in the given theme!\n");
+	String result;
+	if ((classIndex != -1 && theme->settings[classIndex].getString(result, settingName))
+	                     || (theme->settings[0].getString(result, settingName))) {
+		// If the class existed and it contained the setting or the setting could be found in the default class then return it.
+		return result;
+	} else {
+		return defaultValue;
 	}
-	// Try to get the method's name from the component's class settings,
-	// and fall back on the class name itself if not found in neither the class settings nor the common default settings.
+}
+
+MediaMethod theme_getScalableImage(const VisualTheme &theme, const ReadableString &className) {
+	if (!theme.get()) {
+		throwError(U"theme_getScalableImage: Can't get scalable image of class ", className, U" from a non-existing theme!\n");
+	}
+	int classIndex = theme->getClassIndex(className);
 	String methodName;
-	if (!theme->settings[classIndex].getString(methodName, U"method")) {
-		if (!theme->settings[0].getString(methodName, U"method")) {
-			throwError(U"The property \"method\" could not be found from the style class ", className, U", nor in the default settings!\n");
-		}
+	if ((classIndex != -1 && theme->settings[classIndex].getString(methodName, U"method"))
+	                     || (theme->settings[0].getString(methodName, U"method"))) {
+		// If the class existed and it contained the setting or the setting could be found in the default class then return it.
+		return machine_getMethod(theme->machine, methodName, theme->getClassIndex(className));
+	} else {
+		throwError(U"theme_getScalableImage: Can't get scalable image of class ", className, U" because the setting did not exist in neither the class nor the default settings!\n");
+		return MediaMethod();
 	}
-	return machine_getMethod(theme->machine, methodName, classIndex);
 }
 
 static bool assignMediaMachineArguments(ClassSettings settings, MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName) {
 	// Search for argumentName in colorImages.
-	for (int64_t i = 0; i < settings.colorImages.length(); i++) {
+	for (int i = 0; i < settings.colorImages.length(); i++) {
 		if (string_caseInsensitiveMatch(settings.colorImages[i].key, argumentName)) {
 			machine_setInputByIndex(machine, methodIndex, inputIndex, settings.colorImages[i].value.value);
 			return true;
 		}
 	}
 	// Search for argumentName in scalars.
-	for (int64_t i = 0; i < settings.scalars.length(); i++) {
+	for (int i = 0; i < settings.scalars.length(); i++) {
 		if (string_caseInsensitiveMatch(settings.scalars[i].key, argumentName)) {
 			machine_setInputByIndex(machine, methodIndex, inputIndex, settings.scalars[i].value);
 			return true;
@@ -365,7 +458,7 @@ static bool assignMediaMachineArguments(ClassSettings settings, MediaMachine &ma
 	return false;
 }
 
-bool theme_assignMediaMachineArguments(const VisualTheme &theme, int32_t contextIndex, MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName) {
+bool theme_assignMediaMachineArguments(const VisualTheme &theme, int contextIndex, MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName) {
 	if (!theme.get()) { return false; }
 	// Check in the context first, and then in the default settings.
 	return (contextIndex > 0 && assignMediaMachineArguments(theme->settings[contextIndex], machine, methodIndex, inputIndex, argumentName))

+ 37 - 2
Source/DFPSR/gui/VisualTheme.h

@@ -1,6 +1,6 @@
 // zlib open source license
 //
-// Copyright (c) 2018 to 2022 David Forsgren Piuva
+// Copyright (c) 2018 to 2023 David Forsgren Piuva
 // 
 // This software is provided 'as-is', without any express or implied
 // warranty. In no event will the authors be held liable for any damages
@@ -50,8 +50,43 @@ VisualTheme theme_createFromFile(const MediaMachine &machine, const ReadableStri
 // Get a handle to the default theme.
 VisualTheme theme_getDefault();
 
-// Get a scalable image by name from the theme.
+// Returns true iff theme exists, otherwise false.
+bool theme_exists(const VisualTheme &theme);
+// Returns the index of className in theme, 0 if it did not exist in theme or -1 if theme does not exist.
+int theme_getClassIndex(const VisualTheme &theme, const ReadableString &className);
+// Returns true iff className exists in theme.
+bool theme_class_exists(const VisualTheme &theme, const ReadableString &className);
+// Returns suggestedClassName if it exists in theme, fallbackClassName otherwise.
+String theme_selectClass(const VisualTheme &theme, const ReadableString &suggestedClassName, const ReadableString &fallbackClassName);
+
+// Getters for resources in the theme's *.ini configuration file.
+//   If className is not found in the theme, the settings declared before the first class are used.
+//     This allow unhandled components to have a generic look, get a pattern that signals unfinished work or leave it blank to terminate with an error when a class is not handled.
+//   Settings after the class name within [] until the end of the file or next class belong to that class.
+
+// Get the scalable image created using the media machine method name stored in the string setting "method" within className in theme.
+//   If there is no method name assigned to "method" after [className], the "method" before the first [] block is used instead.
+//   If no method is found at all, it means that the theme wants to throw an exception to alert the developer about a class that needs to be handled.
+//   The matches for className and "method" are both case insensitive.
 MediaMethod theme_getScalableImage(const VisualTheme &theme, const ReadableString &className);
+// TODO: Create syntax for naming sub-image regions within the atlas to be used as icons without needing to load separate files.
+// Get a fixed size image from className in theme using settingName, an empty handle if not found in theme or theme does not exist.
+//   Looks for image assignments to settingName after [className] first, then before the first [] block, and then returns an empty handle if not found at all.
+//   The matches for className and settingName are both case insensitive.
+//   Use theme_getClassIndex if you are unsure of why an empty handle was returned, because it will not throw exceptions.
+OrderedImageRgbaU8 theme_getImage(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName);
+// Get a fixed-point value from className in theme using settingName, the default value if not found, or throw an exception if theme does not exist.
+//   Looks for scalar assignments to settingName after [className] first, then before the first [] block, and then returns defaultValue if not found at all.
+//   The matches for className and settingName are both case insensitive.
+//   Use theme_getClassIndex if you are unsure of why defaultValue was returned, because it will not throw exceptions.
+FixedPoint theme_getFixedPoint(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName, const FixedPoint &defaultValue);
+// Get FixedPoint and truncate to an integer, which should leave 16 bits of useful range.
+int theme_getInteger(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName, const int &defaultValue);
+// Get a string from className in theme using settingName, the default value if not found, or throw an exception if theme does not exist.
+//   Looks for string assignments to settingName after [className] first, then before the first [] block, and then returns defaultValue if not found at all.
+//   The matches for className and settingName are both case insensitive.
+//   Use theme_getClassIndex if you are unsure of why defaultValue was returned, because it will not throw exceptions.
+ReadableString theme_getString(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName, const FixedPoint &defaultValue);
 
 // Called by VisualComponent to assign input arguments to functions in the media machine that were not given by the component itself.
 // Post-condition: Returns true if argumentName was identified and assigned as input to inputIndex of methodIndex in machine.

+ 20 - 4
Source/DFPSR/gui/components/Button.cpp

@@ -34,6 +34,7 @@ void Button::declareAttributes(StructureDefinition &target) const {
 	target.declareAttribute(U"ForeColor");
 	target.declareAttribute(U"Text");
 	target.declareAttribute(U"Padding");
+	target.declareAttribute(U"BackgroundClass");
 }
 
 Persistent* Button::findAttribute(const ReadableString &name) {
@@ -46,6 +47,8 @@ Persistent* Button::findAttribute(const ReadableString &name) {
 		return &(this->text);
 	} else if (string_caseInsensitiveMatch(name, U"Padding")) {
 		return &(this->padding);
+	} else if (string_caseInsensitiveMatch(name, U"Class") || string_caseInsensitiveMatch(name, U"BackgroundClass")) {
+		return &(this->backgroundClass);
 	} else {
 		return VisualComponent::findAttribute(name);
 	}
@@ -87,7 +90,11 @@ void Button::generateGraphics() {
 
 void Button::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
 	this->generateGraphics();
-	draw_alphaFilter(targetImage, (this->pressed && this->inside) ? this->imageDown : this->imageUp, relativeLocation.left(), relativeLocation.top());
+	if (this->background_filter == 1) {
+		draw_alphaFilter(targetImage, (this->pressed && this->inside) ? this->imageDown : this->imageUp, relativeLocation.left(), relativeLocation.top());
+	} else {
+		draw_copy(targetImage, (this->pressed && this->inside) ? this->imageDown : this->imageUp, relativeLocation.left(), relativeLocation.top());
+	}
 }
 
 void Button::receiveMouseEvent(const MouseEvent& event) {
@@ -111,14 +118,20 @@ bool Button::pointIsInside(const IVector2D& pixelPosition) {
 	return image_readPixel_border(this->imageUp, localPoint.x, localPoint.y).alpha > 127;
 }
 
+void Button::loadTheme(const VisualTheme &theme) {
+	this->finalBackgroundClass = theme_selectClass(theme, this->backgroundClass.value, U"Button");
+	this->button = theme_getScalableImage(theme, this->finalBackgroundClass);
+	this->background_filter = theme_getInteger(theme, this->finalBackgroundClass, U"Filter", 0);
+}
+
 void Button::changedTheme(VisualTheme newTheme) {
-	this->button = theme_getScalableImage(newTheme, U"Button");
+	this->loadTheme(newTheme);
 	this->hasImages = false;
 }
 
 void Button::completeAssets() {
 	if (this->button.methodIndex == -1) {
-		this->button = theme_getScalableImage(theme_getDefault(), U"Button");
+		this->loadTheme(theme_getDefault());
 	}
 	if (this->font.get() == nullptr) {
 		this->font = font_getDefault();
@@ -133,7 +146,10 @@ void Button::changedLocation(const IRect &oldLocation, const IRect &newLocation)
 }
 
 void Button::changedAttribute(const ReadableString &name) {
-	if (!string_caseInsensitiveMatch(name, U"Visible")) {
+	if (string_caseInsensitiveMatch(name, U"BackgroundClass")) {
+		// Update from the theme if the theme class has changed.
+		this->changedTheme(this->getTheme());
+	} else if (!string_caseInsensitiveMatch(name, U"Visible")) {
 		this->hasImages = false;
 	}
 	VisualComponent::changedAttribute(name);

+ 7 - 0
Source/DFPSR/gui/components/Button.h

@@ -36,6 +36,9 @@ public:
 	PersistentColor backColor = PersistentColor(130, 130, 130);
 	PersistentColor foreColor = PersistentColor(0, 0, 0);
 	PersistentString text;
+	// Name of theme class used to draw the background.
+	//   "Button" is used if backgroundClass is empty or not found.
+	PersistentString backgroundClass;
 	PersistentInteger padding = PersistentInteger(5); // How many pixels of padding are applied on each side of the text when calculating desired dimensions for placing in toolbars.
 	void declareAttributes(StructureDefinition &target) const override;
 	Persistent* findAttribute(const ReadableString &name) override;
@@ -46,8 +49,12 @@ private:
 	// Given from the style
 	MediaMethod button;
 	RasterFont font;
+	void loadTheme(const VisualTheme &theme);
 	void completeAssets();
 	void generateGraphics();
+	// Settings fetched from the theme
+	String finalBackgroundClass; // The selected BackgroundClass/Class from layout settings or the component's default theme class "Button".
+	int background_filter = 0; // 0 for solid, 1 for alpha filter.
 	// Generated
 	bool hasImages = false;
 	OrderedImageRgbaU8 imageUp;

+ 17 - 6
Source/DFPSR/gui/components/ListBox.cpp

@@ -33,6 +33,7 @@ void ListBox::declareAttributes(StructureDefinition &target) const {
 	target.declareAttribute(U"ForeColor");
 	target.declareAttribute(U"List");
 	target.declareAttribute(U"SelectedIndex");
+	target.declareAttribute(U"BackgroundClass");
 }
 
 Persistent* ListBox::findAttribute(const ReadableString &name) {
@@ -44,6 +45,8 @@ Persistent* ListBox::findAttribute(const ReadableString &name) {
 		return &(this->list);
 	} else if (string_caseInsensitiveMatch(name, U"SelectedIndex")) {
 		return &(this->selectedIndex);
+	} else if (string_caseInsensitiveMatch(name, U"Class") || string_caseInsensitiveMatch(name, U"BackgroundClass")) {
+		return &(this->backgroundClass);
 	} else {
 		return VisualComponent::findAttribute(name);
 	}
@@ -94,7 +97,11 @@ void ListBox::generateGraphics() {
 
 void ListBox::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
 	this->generateGraphics();
-	draw_copy(targetImage, this->image, relativeLocation.left(), relativeLocation.top());
+	if (this->background_filter == 1) {
+		draw_alphaFilter(targetImage, this->image, relativeLocation.left(), relativeLocation.top());
+	} else {
+		draw_copy(targetImage, this->image, relativeLocation.left(), relativeLocation.top());
+	}
 }
 
 void ListBox::updateScrollRange() {
@@ -168,9 +175,11 @@ void ListBox::receiveKeyboardEvent(const KeyboardEvent& event) {
 	VisualComponent::receiveKeyboardEvent(event);
 }
 
-void ListBox::loadTheme(VisualTheme theme) {
-	this->scalableImage_listBox = theme_getScalableImage(theme, U"ListBox");
+void ListBox::loadTheme(const VisualTheme &theme) {
+	this->finalBackgroundClass = theme_selectClass(theme, this->backgroundClass.value, U"ListBox");
+	this->scalableImage_listBox = theme_getScalableImage(theme, this->finalBackgroundClass);
 	this->verticalScrollBar.loadTheme(theme, this->backColor.value);
+	this->background_filter = theme_getInteger(theme, this->finalBackgroundClass, U"Filter", 0);
 }
 
 void ListBox::changedTheme(VisualTheme newTheme) {
@@ -203,12 +212,14 @@ void ListBox::changedLocation(const IRect &oldLocation, const IRect &newLocation
 }
 
 void ListBox::changedAttribute(const ReadableString &name) {
-	if (!string_caseInsensitiveMatch(name, U"Visible")) {
-		this->hasImages = false;
-	}
 	if (string_caseInsensitiveMatch(name, U"List")) {
 		// Reset selection on full list updates
 		this->setSelectedIndex(0, true);
+	} else if (string_caseInsensitiveMatch(name, U"BackgroundClass")) {
+		// Update from the theme if the theme class has changed.
+		this->changedTheme(this->getTheme());
+	} else if (!string_caseInsensitiveMatch(name, U"Visible")) {
+		this->hasImages = false;
 	}
 	this->limitSelection(false);
 	this->limitScrolling();

+ 7 - 1
Source/DFPSR/gui/components/ListBox.h

@@ -37,6 +37,9 @@ public:
 	PersistentColor backColor = PersistentColor(200, 200, 200);
 	PersistentColor foreColor = PersistentColor(0, 0, 0);
 	PersistentStringList list;
+	// Name of theme class used to draw the background.
+	//   "ListBox" is used if backgroundClass is empty or not found.
+	PersistentString backgroundClass;
 	PersistentInteger selectedIndex; // Should always be inside of the list's 0..length-1 bound or zero.
 	void declareAttributes(StructureDefinition &target) const override;
 	Persistent* findAttribute(const ReadableString &name) override;
@@ -52,6 +55,9 @@ private:
 	void loadFont();
 	void completeAssets();
 	void generateGraphics();
+	// Settings fetched from the theme
+	String finalBackgroundClass; // The selected BackgroundClass/Class from layout settings or the component's default theme class "ListBox".
+	int background_filter = 0; // 0 for solid, 1 for alpha filter.
 	// Generated
 	bool hasImages = false;
 	OrderedImageRgbaU8 image;
@@ -63,7 +69,7 @@ private:
 	void limitScrolling(bool keepSelectedVisible = false); // Clamp scrolling
 	int64_t getVisibleScrollRange(); // Return the number of items that are visible at once
 	void pressScrollBar(int64_t localY); // Press the scroll-bar at localY in pixels
-	void loadTheme(VisualTheme theme);
+	void loadTheme(const VisualTheme &theme);
 	// If a new selection inherited the old index, forceUpdate will send the select event anyway
 	void setSelectedIndex(int64_t index, bool forceUpdate);
 public:

+ 36 - 10
Source/DFPSR/gui/components/Menu.cpp

@@ -43,6 +43,8 @@ void Menu::declareAttributes(StructureDefinition &target) const {
 	target.declareAttribute(U"Text");
 	target.declareAttribute(U"Padding");
 	target.declareAttribute(U"Spacing");
+	target.declareAttribute(U"HeadClass");
+	target.declareAttribute(U"ListClass");
 }
 
 Persistent* Menu::findAttribute(const ReadableString &name) {
@@ -57,6 +59,11 @@ Persistent* Menu::findAttribute(const ReadableString &name) {
 		return &(this->padding);
 	} else if (string_caseInsensitiveMatch(name, U"Spacing")) {
 		return &(this->spacing);
+	} else if (string_caseInsensitiveMatch(name, U"HeadClass") || string_caseInsensitiveMatch(name, U"Class")) {
+		// Class is an alias for HeadClass.
+		return &(this->headClass);
+	} else if (string_caseInsensitiveMatch(name, U"ListClass")) {
+		return &(this->listClass);
 	} else {
 		return VisualComponent::findAttribute(name);
 	}
@@ -114,7 +121,11 @@ void Menu::generateGraphics() {
 // Fill the listBackgroundImageMethod with a solid color
 void Menu::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
 	this->generateGraphics();
-	draw_alphaFilter(targetImage, this->showingOverlay() ? this->imageDown : this->imageUp, relativeLocation.left(), relativeLocation.top());
+	if (this->menuHead_filter == 1) {
+		draw_alphaFilter(targetImage, this->showingOverlay() ? this->imageDown : this->imageUp, relativeLocation.left(), relativeLocation.top());
+	} else {
+		draw_copy(targetImage, this->showingOverlay() ? this->imageDown : this->imageUp, relativeLocation.left(), relativeLocation.top());
+	}
 }
 
 void Menu::generateBackground() {
@@ -150,26 +161,37 @@ bool Menu::pointIsInsideOfOverlay(const IVector2D& pixelPosition) {
 
 void Menu::drawOverlay(ImageRgbaU8& targetImage, const IVector2D &absoluteOffset) {
 	this->generateBackground();
-	// TODO: Let the theme select between solid and alpha filtered drawing.
 	IVector2D overlayOffset = absoluteOffset + this->overlayLocation.upperLeft();
-	draw_copy(targetImage, this->listBackgroundImage, overlayOffset.x, overlayOffset.y);
+	if (this->menuList_filter == 1) {
+		draw_alphaFilter(targetImage, this->listBackgroundImage, overlayOffset.x, overlayOffset.y);
+	} else {
+		draw_copy(targetImage, this->listBackgroundImage, overlayOffset.x, overlayOffset.y);
+	}
 	for (int i = 0; i < this->getChildCount(); i++) {
 		this->children[i]->draw(targetImage, absoluteOffset + this->location.upperLeft());
 	}
 }
 
+void Menu::loadTheme(const VisualTheme &theme) {
+	// Is it a sub-menu or top menu?
+	this->subMenu = this->parent != nullptr && dynamic_cast<Menu*>(this->parent) != nullptr;
+	this->finalHeadClass = theme_selectClass(theme, this->headClass.value, this->subMenu ? U"MenuSub" : U"MenuTop");
+	this->finalListClass = theme_selectClass(theme, this->listClass.value, U"MenuList");
+	this->headImageMethod = theme_getScalableImage(theme, this->finalHeadClass);
+	this->listBackgroundImageMethod = theme_getScalableImage(theme, this->finalListClass);
+	// Ask the theme which parts should be drawn using alpha filtering, and fall back on solid drawing.
+	this->menuHead_filter = theme_getInteger(theme, this->finalHeadClass, U"Filter", 0);
+	this->menuList_filter = theme_getInteger(theme, this->finalListClass, U"Filter", 0);
+}
+
 void Menu::changedTheme(VisualTheme newTheme) {
-	this->headImageMethod = theme_getScalableImage(newTheme, this->subMenu ? U"MenuSub" : U"MenuTop");
-	this->listBackgroundImageMethod = theme_getScalableImage(newTheme, U"MenuList");
+	this->loadTheme(newTheme);
 	this->hasImages = false;
 }
 
 void Menu::completeAssets() {
 	if (this->headImageMethod.methodIndex == -1) {
-		// Work as a sub-menu if the direct parent is also a menu.
-		this->subMenu = this->parent != nullptr && dynamic_cast<Menu*>(this->parent) != nullptr;
-		this->headImageMethod = theme_getScalableImage(theme_getDefault(), this->subMenu ? U"MenuSub" : U"MenuTop");
-		this->listBackgroundImageMethod = theme_getScalableImage(theme_getDefault(), U"MenuList");
+		this->loadTheme(theme_getDefault());
 	}
 	if (this->font.get() == nullptr) {
 		this->font = font_getDefault();
@@ -184,7 +206,11 @@ void Menu::changedLocation(const IRect &oldLocation, const IRect &newLocation) {
 }
 
 void Menu::changedAttribute(const ReadableString &name) {
-	if (!string_caseInsensitiveMatch(name, U"Visible")) {
+	if (string_caseInsensitiveMatch(name, U"HeadClass")
+	 || string_caseInsensitiveMatch(name, U"ListClass")) {
+		// Update from the theme if a theme class has changed.
+		this->changedTheme(this->getTheme());
+	} else if (!string_caseInsensitiveMatch(name, U"Visible")) {
 		this->hasImages = false;
 	}
 	VisualComponent::changedAttribute(name);

+ 14 - 0
Source/DFPSR/gui/components/Menu.h

@@ -35,12 +35,21 @@ public:
 	// Attributes
 	PersistentColor backColor = PersistentColor(130, 130, 130);
 	PersistentColor foreColor = PersistentColor(0, 0, 0);
+	// The text to display for the head that is directly interacted with.
 	PersistentString text;
+	// Name of theme class used to draw the background of the head.
+	//   "MenuTop" is used if listClass is empty or not found, and the menu is not directly within another menu.
+	//   "MenuSub" is used if listClass is empty or not found, and the menu is a direct child of another menu.
+	PersistentString headClass;
+	// Name of theme class used to draw the background of the drop-down list.
+	//   "MenuList" is used if listClass is empty or not found.
+	PersistentString listClass;
 	PersistentInteger padding = PersistentInteger(4); // Empty space around child components and its own text.
 	PersistentInteger spacing = PersistentInteger(2); // Empty space between child components.
 	void declareAttributes(StructureDefinition &target) const override;
 	Persistent* findAttribute(const ReadableString &name) override;
 private:
+	void loadTheme(const VisualTheme &theme);
 	void completeAssets();
 	void generateGraphics();
 	void generateBackground();
@@ -49,6 +58,11 @@ private:
 	RasterFont font;
 	bool subMenu = false;
 	IRect overlayLocation; // Relative to the parent's location, just like its own location
+	// Settings fetched from the theme
+	String finalHeadClass; // The selected HeadClass/Class from layout settings or the component's default theme class "MenuTop" or "MenuSub" based on ownership.
+	String finalListClass; // The selected ListClass from layout settings or the component's default theme class "MenuList".
+	int menuHead_filter = 0; // 0 for solid, 1 for alpha filter.
+	int menuList_filter = 0; // 0 for solid, 1 for alpha filter.
 	// Generated
 	bool hasImages = false;
 	OrderedImageRgbaU8 imageUp;

+ 21 - 5
Source/DFPSR/gui/components/Panel.cpp

@@ -32,6 +32,7 @@ void Panel::declareAttributes(StructureDefinition &target) const {
 	target.declareAttribute(U"Solid");
 	target.declareAttribute(U"Plain");
 	target.declareAttribute(U"Color");
+	target.declareAttribute(U"BackgroundClass");
 }
 
 Persistent* Panel::findAttribute(const ReadableString &name) {
@@ -42,6 +43,8 @@ Persistent* Panel::findAttribute(const ReadableString &name) {
 	} else if (string_caseInsensitiveMatch(name, U"Color") || string_caseInsensitiveMatch(name, U"BackColor")) {
 		// Both color and backcolor is accepted as names for the only color.
 		return &(this->color);
+	} else if (string_caseInsensitiveMatch(name, U"Class") || string_caseInsensitiveMatch(name, U"BackgroundClass")) {
+		return &(this->backgroundClass);
 	} else {
 		return VisualComponent::findAttribute(name);
 	}
@@ -71,20 +74,30 @@ void Panel::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
 		if (this->plain.value) {
 			draw_rectangle(targetImage, relativeLocation, ColorRgbaI32(this->color.value, 255));
 		} else {
-			this->generateGraphics();
-			draw_copy(targetImage, this->imageBackground, relativeLocation.left(), relativeLocation.top());
+			this->generateGraphics();			
+			if (this->background_filter == 1) {
+				draw_alphaFilter(targetImage, this->imageBackground, relativeLocation.left(), relativeLocation.top());
+			} else {
+				draw_copy(targetImage, this->imageBackground, relativeLocation.left(), relativeLocation.top());
+			}
 		}
 	}
 }
 
+void Panel::loadTheme(const VisualTheme &theme) {
+	this->finalBackgroundClass = theme_selectClass(theme, this->backgroundClass.value, U"Panel");
+	this->background = theme_getScalableImage(theme, this->finalBackgroundClass);
+	this->background_filter = theme_getInteger(theme, this->finalBackgroundClass, U"Filter", 0);
+}
+
 void Panel::changedTheme(VisualTheme newTheme) {
-	this->background = theme_getScalableImage(newTheme, U"Panel");
+	this->loadTheme(newTheme);
 	this->hasImages = false;
 }
 
 void Panel::completeAssets() {
 	if (this->background.methodIndex == -1) {
-		this->background = theme_getScalableImage(theme_getDefault(), U"Panel");
+		this->loadTheme(theme_getDefault());
 	}
 }
 
@@ -96,7 +109,10 @@ void Panel::changedLocation(const IRect &oldLocation, const IRect &newLocation)
 }
 
 void Panel::changedAttribute(const ReadableString &name) {
-	if (!string_caseInsensitiveMatch(name, U"Visible")) {
+	if (string_caseInsensitiveMatch(name, U"BackgroundClass")) {
+		// Update from the theme if the theme class has changed.
+		this->changedTheme(this->getTheme());
+	} else if (!string_caseInsensitiveMatch(name, U"Visible")) {
 		this->hasImages = false;
 	}
 	VisualComponent::changedAttribute(name);

+ 8 - 0
Source/DFPSR/gui/components/Panel.h

@@ -34,12 +34,20 @@ public:
 	// Attributes
 	PersistentBoolean solid; // If true, the panel itself will be drawn.
 	PersistentBoolean plain; // If true, a solid color will be drawn instead of a buffered image to save time and memory.
+	// Name of theme class used to draw the background.
+	//   "Panel" is used if backgroundClass is empty or not found.
+	PersistentString backgroundClass;
 	PersistentColor color = PersistentColor(130, 130, 130); // The color being used when solid is set to true.
 	void declareAttributes(StructureDefinition &target) const override;
 	Persistent* findAttribute(const ReadableString &name) override;
 private:
+	void loadTheme(const VisualTheme &theme);
 	void completeAssets();
 	void generateGraphics();
+	// Settings fetched from the theme
+	String finalBackgroundClass; // The selected BackgroundClass/Class from layout settings or the component's default theme class "Panel".
+	int background_filter = 0; // 0 for solid, 1 for alpha filter.
+	// Images
 	MediaMethod background;
 	OrderedImageRgbaU8 imageBackground; // Alpha is copied to the target and should be 255
 	// Generated

+ 22 - 9
Source/DFPSR/gui/components/TextBox.cpp

@@ -34,6 +34,7 @@ void TextBox::declareAttributes(StructureDefinition &target) const {
 	target.declareAttribute(U"ForeColor");
 	target.declareAttribute(U"Text");
 	target.declareAttribute(U"MultiLine");
+	target.declareAttribute(U"BackgroundClass");
 }
 
 Persistent* TextBox::findAttribute(const ReadableString &name) {
@@ -45,6 +46,8 @@ Persistent* TextBox::findAttribute(const ReadableString &name) {
 		return &(this->text);
 	} else if (string_caseInsensitiveMatch(name, U"MultiLine")) {
 		return &(this->multiLine);
+	} else if (string_caseInsensitiveMatch(name, U"Class") || string_caseInsensitiveMatch(name, U"BackgroundClass")) {
+		return &(this->backgroundClass);
 	} else {
 		return VisualComponent::findAttribute(name);
 	}
@@ -207,7 +210,11 @@ void TextBox::generateGraphics() {
 
 void TextBox::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
 	this->generateGraphics();
-	draw_copy(targetImage, this->image, relativeLocation.left(), relativeLocation.top());
+	if (this->background_filter == 1) {
+		draw_alphaFilter(targetImage, this->image, relativeLocation.left(), relativeLocation.top());
+	} else {
+		draw_copy(targetImage, this->image, relativeLocation.left(), relativeLocation.top());
+	}
 }
 
 int64_t TextBox::findBeamLocationInLine(int64_t rowIndex, int64_t pixelX) {
@@ -504,10 +511,16 @@ bool TextBox::pointIsInside(const IVector2D& pixelPosition) {
 	return dsr::image_readPixel_border(this->image, localPoint.x, localPoint.y).alpha > 127;
 }
 
+void TextBox::loadTheme(const VisualTheme &theme) {
+	this->finalBackgroundClass = theme_selectClass(theme, this->backgroundClass.value, U"TextBox");
+	this->textBox = theme_getScalableImage(theme, this->finalBackgroundClass);
+	this->verticalScrollBar.loadTheme(theme, this->backColor.value);
+	this->horizontalScrollBar.loadTheme(theme, this->backColor.value);
+	this->background_filter = theme_getInteger(theme, this->finalBackgroundClass, U"Filter", 0);
+}
+
 void TextBox::changedTheme(VisualTheme newTheme) {
-	this->textBox = theme_getScalableImage(newTheme, U"TextBox");
-	this->verticalScrollBar.loadTheme(newTheme, this->backColor.value);
-	this->horizontalScrollBar.loadTheme(newTheme, this->backColor.value);
+	this->loadTheme(newTheme);
 	this->hasImages = false;
 }
 
@@ -522,10 +535,7 @@ void TextBox::loadFont() {
 
 void TextBox::completeAssets() {
 	if (this->textBox.methodIndex == -1) {
-		VisualTheme newTheme = theme_getDefault();
-		this->textBox = theme_getScalableImage(newTheme, U"TextBox");
-		this->verticalScrollBar.loadTheme(newTheme, this->backColor.value);
-		this->horizontalScrollBar.loadTheme(newTheme, this->backColor.value);
+		this->loadTheme(theme_getDefault());
 	}
 	this->loadFont();
 }
@@ -539,7 +549,10 @@ void TextBox::changedLocation(const IRect &oldLocation, const IRect &newLocation
 }
 
 void TextBox::changedAttribute(const ReadableString &name) {
-	if (!string_caseInsensitiveMatch(name, U"Visible")) {
+	if (string_caseInsensitiveMatch(name, U"BackgroundClass")) {
+		// Update from the theme if the theme class has changed.
+		this->changedTheme(this->getTheme());
+	} else if (!string_caseInsensitiveMatch(name, U"Visible")) {
 		this->hasImages = false;
 		if (string_caseInsensitiveMatch(name, U"Text")) {
 			this->indexedAtLength = -1;

+ 7 - 0
Source/DFPSR/gui/components/TextBox.h

@@ -58,6 +58,9 @@ public:
 	PersistentColor foreColor = PersistentColor(0, 0, 0);
 	PersistentColor backColor = PersistentColor(200, 200, 200);
 	PersistentString text;
+	// Name of theme class used to draw the background.
+	//   "TextBox" is used if backgroundClass is empty or not found.
+	PersistentString backgroundClass;
 	PersistentBoolean multiLine;
 	int64_t borderX = 6; // Empty pixels left and right of text.
 	int64_t borderY = 4; // Empty pixels above and below text.
@@ -92,8 +95,12 @@ private:
 	MediaMethod textBox;
 	RasterFont font;
 	void loadFont();
+	void loadTheme(const VisualTheme &theme);
 	void completeAssets();
 	void generateGraphics();
+	// Settings fetched from the theme
+	String finalBackgroundClass; // The selected BackgroundClass/Class from layout settings or the component's default theme class "TextBox".
+	int background_filter = 0; // 0 for solid, 1 for alpha filter.
 	// Generated
 	bool hasImages = false;
 	bool drawnAsFocused = false;

+ 20 - 4
Source/DFPSR/gui/components/Toolbar.cpp

@@ -34,6 +34,7 @@ void Toolbar::declareAttributes(StructureDefinition &target) const {
 	target.declareAttribute(U"Color");
 	target.declareAttribute(U"Padding");
 	target.declareAttribute(U"Spacing");
+	target.declareAttribute(U"BackgroundClass");
 }
 
 Persistent* Toolbar::findAttribute(const ReadableString &name) {
@@ -48,6 +49,8 @@ Persistent* Toolbar::findAttribute(const ReadableString &name) {
 		return &(this->padding);
 	} else if (string_caseInsensitiveMatch(name, U"Spacing")) {
 		return &(this->spacing);
+	} else if (string_caseInsensitiveMatch(name, U"Class") || string_caseInsensitiveMatch(name, U"BackgroundClass")) {
+		return &(this->backgroundClass);
 	} else {
 		return VisualComponent::findAttribute(name);
 	}
@@ -78,19 +81,29 @@ void Toolbar::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation)
 			draw_rectangle(targetImage, relativeLocation, ColorRgbaI32(this->color.value, 255));
 		} else {
 			this->generateGraphics();
-			draw_copy(targetImage, this->imageBackground, relativeLocation.left(), relativeLocation.top());
+			if (this->background_filter == 1) {
+				draw_alphaFilter(targetImage, this->imageBackground, relativeLocation.left(), relativeLocation.top());
+			} else {
+				draw_copy(targetImage, this->imageBackground, relativeLocation.left(), relativeLocation.top());
+			}			
 		}
 	}
 }
 
+void Toolbar::loadTheme(const VisualTheme &theme) {
+	this->finalBackgroundClass = theme_selectClass(theme, this->backgroundClass.value, U"Toolbar");
+	this->background = theme_getScalableImage(theme, this->finalBackgroundClass);
+	this->background_filter = theme_getInteger(theme, this->finalBackgroundClass, U"Filter", 0);
+}
+
 void Toolbar::changedTheme(VisualTheme newTheme) {
-	this->background = theme_getScalableImage(newTheme, U"Toolbar");
+	this->loadTheme(newTheme);
 	this->hasImages = false;
 }
 
 void Toolbar::completeAssets() {
 	if (this->background.methodIndex == -1) {
-		this->background = theme_getScalableImage(theme_getDefault(), U"Toolbar");
+		this->loadTheme(theme_getDefault());
 	}
 }
 
@@ -102,7 +115,10 @@ void Toolbar::changedLocation(const IRect &oldLocation, const IRect &newLocation
 }
 
 void Toolbar::changedAttribute(const ReadableString &name) {
-	if (!string_caseInsensitiveMatch(name, U"Visible")) {
+	if (string_caseInsensitiveMatch(name, U"BackgroundClass")) {
+		// Update from the theme if the theme class has changed.
+		this->changedTheme(this->getTheme());
+	} else if (!string_caseInsensitiveMatch(name, U"Visible")) {
 		this->hasImages = false;
 	}
 	VisualComponent::changedAttribute(name);

+ 7 - 0
Source/DFPSR/gui/components/Toolbar.h

@@ -34,6 +34,9 @@ public:
 	// Attributes
 	PersistentBoolean solid; // If true, the panel itself will be drawn.
 	PersistentBoolean plain; // If true, a solid color will be drawn instead of a buffered image to save time and memory.
+	// Name of theme class used to draw the background.
+	//   "Toolbar" is used if backgroundClass is empty or not found.
+	PersistentString backgroundClass;
 	PersistentColor color = PersistentColor(130, 130, 130); // The color being used when drawn is set to true.
 	PersistentInteger padding = PersistentInteger(2); // Empty space around child components.
 	PersistentInteger spacing = PersistentInteger(3); // Empty space between child components.
@@ -41,10 +44,14 @@ public:
 	void declareAttributes(StructureDefinition &target) const override;
 	Persistent* findAttribute(const ReadableString &name) override;
 private:
+	void loadTheme(const VisualTheme &theme);
 	void completeAssets();
 	void generateGraphics();
 	MediaMethod background;
 	OrderedImageRgbaU8 imageBackground; // Alpha is copied to the target and should be 255
+	// Settings fetched from the theme
+	String finalBackgroundClass; // The selected BackgroundClass/Class from layout settings or the component's default theme class "Toolbar".
+	int background_filter = 0; // 0 for solid, 1 for alpha filter.
 	// Generated
 	bool hasImages = false;
 public:

+ 3 - 0
Source/DFPSR/math/FixedPoint.h

@@ -102,7 +102,10 @@ inline FixedPoint operator*(int64_t left, const FixedPoint &right) {
 	return FixedPoint(left * right.getMantissa());
 }
 
+// Returns value rounded to the closest integer.
 int32_t fixedPoint_round(const FixedPoint& value);
+
+// Returns a floating-point approximation of value.
 double fixedPoint_approximate(const FixedPoint& value);
 
 FixedPoint fixedPoint_min(const FixedPoint &left, const FixedPoint &right);

+ 6 - 0
Source/SDK/guiExample/main.cpp

@@ -52,6 +52,12 @@ void dsrMain(List<String> args) {
 	// Load an interface to the window
 	window_loadInterfaceFromFile(window, U"media/interface.lof");
 
+	// Create a virtual machine with reusable image generating functions.
+	//   The same Media Machine Code (*.mmc) can be used for multiple themes.
+	MediaMachine machine = machine_create(string_load(U"media/Drawing.mmc"));
+	// Use the virtual machine with a specific style referring to the functions in machine.
+	window_applyTheme(window, theme_createFromFile(machine, U"media/Theme.ini"));
+
 	// Bind methods to events
 	window_setCloseEvent(window, []() {
 		sendWarning(U"Ahhh, you killed me! But closing a window directly is okay, because the program can run logic for saving things before terminating.");

+ 272 - 0
Source/SDK/guiExample/media/Drawing.mmc

@@ -0,0 +1,272 @@
+# Helper function that can't be used as a theme.
+BEGIN: Resize3x3
+	INPUT: FixedPoint, targetWidth
+	INPUT: FixedPoint, targetHeight
+	INPUT: FixedPoint, sourceBorder
+	INPUT: ImageRgbaU8, sourceImage
+	INPUT: FixedPoint, sourceLeft
+	INPUT: FixedPoint, sourceTop
+	INPUT: FixedPoint, sourceWidth
+	INPUT: FixedPoint, sourceHeight
+	OUTPUT: ImageRgbaU8, targetImage
+	CREATE: targetImage, targetWidth, targetHeight
+	# Limit targetBorder
+	MIN: targetBorder<FixedPoint>, targetWidth, targetHeight
+	MUL: targetBorder, targetBorder, 0.3
+	MIN: targetBorder, targetBorder, sourceBorder
+	# Calculat dimensions
+	ADD: sourceRight<FixedPoint>, sourceLeft, sourceWidth
+	ADD: sourceBottom<FixedPoint>, sourceTop, sourceHeight
+	ADD: sb2<FixedPoint>, sourceBorder, sourceBorder
+	SUB: sbiw<FixedPoint>, sourceWidth, sb2
+	SUB: sbih<FixedPoint>, sourceHeight, sb2
+	#ADD: sbl<FixedPoint>, sourceLeft, sourceBorder
+	#ADD: sbt<FixedPoint>, sourceTop, sourceBorder
+	SUB: sbow<FixedPoint>, sourceRight, sourceBorder
+	SUB: sboh<FixedPoint>, sourceBottom, sourceBorder
+	ADD: pl<FixedPoint>, sourceLeft, sourceBorder
+	ADD: pt<FixedPoint>, sourceTop, sourceBorder
+	ADD: tb2<FixedPoint>, targetBorder, targetBorder
+	SUB: tbiw<FixedPoint>, targetWidth, tb2
+	SUB: tbih<FixedPoint>, targetHeight, tb2
+	SUB: tbow<FixedPoint>, targetWidth, targetBorder
+	SUB: tboh<FixedPoint>, targetHeight, targetBorder
+	# Upper
+	RESIZE_BILINEAR: scaled<ImageRgbaU8>, targetBorder, targetBorder, sourceImage, sourceLeft, sourceTop, sourceBorder, sourceBorder
+	COPY: targetImage, 0, 0, scaled
+	RESIZE_BILINEAR: scaled, tbiw, targetBorder, sourceImage, pl, sourceTop, sbiw, sourceBorder
+	COPY: targetImage, targetBorder, 0, scaled
+	RESIZE_BILINEAR: scaled, targetBorder, targetBorder, sourceImage, sbow, sourceTop, sourceBorder, sourceBorder
+	COPY: targetImage, tbow, 0, scaled
+	# Middle
+	RESIZE_BILINEAR: scaled, targetBorder, tbih, sourceImage, sourceLeft, pt, sourceBorder, sbih
+	COPY: targetImage, 0, targetBorder, scaled
+	RESIZE_BILINEAR: scaled, tbiw, tbih, sourceImage, pl, pt, sbiw, sbih
+	COPY: targetImage, targetBorder, targetBorder, scaled
+	RESIZE_BILINEAR: scaled, targetBorder, tbih, sourceImage, sbow, pt, sourceBorder, sbih
+	COPY: targetImage, tbow, targetBorder, scaled
+	# Lower
+	RESIZE_BILINEAR: scaled, targetBorder, targetBorder, sourceImage, sourceLeft, sboh, sourceBorder, sourceBorder
+	COPY: targetImage, 0, tboh, scaled
+	RESIZE_BILINEAR: scaled, tbiw, targetBorder, sourceImage, pl, sboh, sbiw, sourceBorder
+	COPY: targetImage, targetBorder, tboh, scaled
+	RESIZE_BILINEAR: scaled, targetBorder, targetBorder, sourceImage, sbow, sboh, sourceBorder, sourceBorder
+	COPY: targetImage, tbow, tboh, scaled
+END:
+
+BEGIN: Diffuse3x3
+	INPUT: FixedPoint, width
+	INPUT: FixedPoint, height
+	INPUT: FixedPoint, red
+	INPUT: FixedPoint, green
+	INPUT: FixedPoint, blue
+	INPUT: FixedPoint, sourceLeft
+	INPUT: FixedPoint, sourceTop
+	INPUT: FixedPoint, sourceWidth
+	INPUT: FixedPoint, sourceHeight
+	INPUT: FixedPoint, preserved
+	INPUT: ImageRgbaU8, atlas
+	OUTPUT: ImageRgbaU8, colorImage
+	# Scale by 1 / 255 so that 255 represents full intensity in atlas.
+	MUL: normRed<FixedPoint>, red, 0.00392156862745
+	MUL: normGreen<FixedPoint>, green, 0.00392156862745
+	MUL: normBlue<FixedPoint>, blue, 0.00392156862745
+	# Resize source region from the atlas.
+	CALL: Resize3x3, rescaledImage<ImageRgbaU8>, width, height, preserved, atlas, sourceLeft, sourceTop, sourceWidth, sourceHeight
+	GET_RED: diffuseMap<ImageU8>, rescaledImage
+	GET_ALPHA: visibilityMap<ImageU8>, rescaledImage
+	MUL: redImage<ImageU8>, diffuseMap, normRed
+	MUL: greenImage<ImageU8>, diffuseMap, normGreen
+	MUL: blueImage<ImageU8>, diffuseMap, normBlue
+	PACK_RGBA: colorImage, redImage, greenImage, blueImage, visibilityMap
+END:
+
+BEGIN: FocusableDiffuse3x3
+	INPUT: FixedPoint, width
+	INPUT: FixedPoint, height
+	INPUT: FixedPoint, red
+	INPUT: FixedPoint, green
+	INPUT: FixedPoint, blue
+	INPUT: FixedPoint, sourceLeft
+	INPUT: FixedPoint, sourceTop
+	INPUT: FixedPoint, sourceWidth
+	INPUT: FixedPoint, sourceHeight
+	INPUT: FixedPoint, focusOffsetX
+	INPUT: FixedPoint, focusOffsetY
+	INPUT: FixedPoint, focused
+	INPUT: FixedPoint, preserved
+	INPUT: ImageRgbaU8, atlas
+	OUTPUT: ImageRgbaU8, colorImage
+	# Select image using the focused state.
+	MUL: sourceOffsetX<FixedPoint>, focused, focusOffsetX
+	MUL: sourceOffsetY<FixedPoint>, focused, focusOffsetY
+	ADD: adjustedSourceLeft<FixedPoint>, sourceLeft, sourceOffsetX
+	ADD: adjustedSourceTop<FixedPoint>, sourceTop, sourceOffsetY
+	# Scale by 1 / 255 so that 255 represents full intensity in atlas.
+	MUL: normRed<FixedPoint>, red, 0.00392156862745
+	MUL: normGreen<FixedPoint>, green, 0.00392156862745
+	MUL: normBlue<FixedPoint>, blue, 0.00392156862745
+	# Resize source region from the atlas.
+	CALL: Resize3x3, rescaledImage<ImageRgbaU8>, width, height, preserved, atlas, adjustedSourceLeft, adjustedSourceTop, sourceWidth, sourceHeight
+	GET_RED: diffuseMap<ImageU8>, rescaledImage
+	GET_ALPHA: visibilityMap<ImageU8>, rescaledImage
+	MUL: redImage<ImageU8>, diffuseMap, normRed
+	MUL: greenImage<ImageU8>, diffuseMap, normGreen
+	MUL: blueImage<ImageU8>, diffuseMap, normBlue
+	PACK_RGBA: colorImage, redImage, greenImage, blueImage, visibilityMap
+END:
+
+BEGIN: DiffuseSpecular3x3
+	INPUT: FixedPoint, width
+	INPUT: FixedPoint, height
+	INPUT: FixedPoint, red
+	INPUT: FixedPoint, green
+	INPUT: FixedPoint, blue
+	INPUT: FixedPoint, sourceLeft
+	INPUT: FixedPoint, sourceTop
+	INPUT: FixedPoint, sourceWidth
+	INPUT: FixedPoint, sourceHeight
+	INPUT: FixedPoint, preserved
+	INPUT: ImageRgbaU8, atlas
+	OUTPUT: ImageRgbaU8, colorImage
+	# Scale by 1 / 255 so that 255 represents full intensity in atlas.
+	MUL: normRed<FixedPoint>, red, 0.00392156862745
+	MUL: normGreen<FixedPoint>, green, 0.00392156862745
+	MUL: normBlue<FixedPoint>, blue, 0.00392156862745
+	# Resize source region from the atlas.
+	CALL: Resize3x3, rescaledImage<ImageRgbaU8>, width, height, preserved, atlas, sourceLeft, sourceTop, sourceWidth, sourceHeight
+	GET_RED: diffuseMap<ImageU8>, rescaledImage
+	GET_GREEN: specularMap<ImageU8>, rescaledImage
+	GET_ALPHA: visibilityMap<ImageU8>, rescaledImage
+	MUL: redImage<ImageU8>, diffuseMap, normRed
+	MUL: greenImage<ImageU8>, diffuseMap, normGreen
+	MUL: blueImage<ImageU8>, diffuseMap, normBlue
+	ADD: redImage, redImage, specularMap
+	ADD: greenImage, greenImage, specularMap
+	ADD: blueImage, blueImage, specularMap
+	PACK_RGBA: colorImage, redImage, greenImage, blueImage, visibilityMap
+END:
+
+BEGIN: PressableDiffuseSpecular3x3
+	INPUT: FixedPoint, width
+	INPUT: FixedPoint, height
+	INPUT: FixedPoint, red
+	INPUT: FixedPoint, green
+	INPUT: FixedPoint, blue
+	INPUT: FixedPoint, pressed
+	INPUT: FixedPoint, sourceLeft
+	INPUT: FixedPoint, sourceTop
+	INPUT: FixedPoint, sourceWidth
+	INPUT: FixedPoint, sourceHeight
+	INPUT: FixedPoint, pressOffsetX
+	INPUT: FixedPoint, pressOffsetY
+	INPUT: FixedPoint, preserved
+	INPUT: ImageRgbaU8, atlas
+	OUTPUT: ImageRgbaU8, colorImage
+	# Select image using the pressed state.
+	MUL: sourceOffsetX<FixedPoint>, pressed, pressOffsetX
+	MUL: sourceOffsetY<FixedPoint>, pressed, pressOffsetY
+	ADD: adjustedSourceLeft<FixedPoint>, sourceLeft, sourceOffsetX
+	ADD: adjustedSourceTop<FixedPoint>, sourceTop, sourceOffsetY
+	# Rescale the source region to fit width and height, while applying the diffuse color and adding white shine.
+	CALL: DiffuseSpecular3x3, colorImage, width, height, red, green, blue, adjustedSourceLeft, adjustedSourceTop, sourceWidth, sourceHeight, preserved, atlas
+END:
+
+BEGIN: DiffuseSpecular1x1
+	INPUT: FixedPoint, width
+	INPUT: FixedPoint, height
+	INPUT: FixedPoint, red
+	INPUT: FixedPoint, green
+	INPUT: FixedPoint, blue
+	INPUT: FixedPoint, sourceLeft
+	INPUT: FixedPoint, sourceTop
+	INPUT: FixedPoint, sourceWidth
+	INPUT: FixedPoint, sourceHeight
+	INPUT: FixedPoint, preserved
+	INPUT: ImageRgbaU8, atlas
+	OUTPUT: ImageRgbaU8, colorImage
+	# Scale by 1 / 255 so that 255 represents full intensity in atlas.
+	MUL: normRed<FixedPoint>, red, 0.00392156862745
+	MUL: normGreen<FixedPoint>, green, 0.00392156862745
+	MUL: normBlue<FixedPoint>, blue, 0.00392156862745
+	# Resize source region from the atlas.
+	RESIZE_BILINEAR: rescaledImage<ImageRgbaU8>, width, height, atlas, sourceLeft, sourceTop, sourceWidth, sourceHeight
+	GET_RED: diffuseMap<ImageU8>, rescaledImage
+	GET_GREEN: specularMap<ImageU8>, rescaledImage
+	GET_ALPHA: visibilityMap<ImageU8>, rescaledImage
+	MUL: redImage<ImageU8>, diffuseMap, normRed
+	MUL: greenImage<ImageU8>, diffuseMap, normGreen
+	MUL: blueImage<ImageU8>, diffuseMap, normBlue
+	ADD: redImage, redImage, specularMap
+	ADD: greenImage, greenImage, specularMap
+	ADD: blueImage, blueImage, specularMap
+	PACK_RGBA: colorImage, redImage, greenImage, blueImage, visibilityMap
+END:
+
+BEGIN: PressableDiffuseSpecular1x1
+	INPUT: FixedPoint, width
+	INPUT: FixedPoint, height
+	INPUT: FixedPoint, red
+	INPUT: FixedPoint, green
+	INPUT: FixedPoint, blue
+	INPUT: FixedPoint, pressed
+	INPUT: FixedPoint, sourceLeft
+	INPUT: FixedPoint, sourceTop
+	INPUT: FixedPoint, sourceWidth
+	INPUT: FixedPoint, sourceHeight
+	INPUT: FixedPoint, pressOffsetX
+	INPUT: FixedPoint, pressOffsetY
+	INPUT: FixedPoint, preserved
+	INPUT: ImageRgbaU8, atlas
+	OUTPUT: ImageRgbaU8, colorImage
+	# Select image using the pressed state.
+	MUL: sourceOffsetX<FixedPoint>, pressed, pressOffsetX
+	MUL: sourceOffsetY<FixedPoint>, pressed, pressOffsetY
+	ADD: adjustedSourceLeft<FixedPoint>, sourceLeft, sourceOffsetX
+	ADD: adjustedSourceTop<FixedPoint>, sourceTop, sourceOffsetY
+	# Rescale the source region to fit width and height, while applying the diffuse color and adding white shine.
+	CALL: DiffuseSpecular1x1, colorImage, width, height, red, green, blue, adjustedSourceLeft, adjustedSourceTop, sourceWidth, sourceHeight, preserved, atlas
+END:
+
+BEGIN: ListBox
+	INPUT: FixedPoint, width
+	INPUT: FixedPoint, height
+	INPUT: FixedPoint, red
+	INPUT: FixedPoint, green
+	INPUT: FixedPoint, blue
+	INPUT: FixedPoint, sourceLeft
+	INPUT: FixedPoint, sourceTop
+	INPUT: FixedPoint, sourceWidth
+	INPUT: FixedPoint, sourceHeight
+	INPUT: FixedPoint, preserved
+	INPUT: ImageRgbaU8, atlas
+	OUTPUT: ImageRgbaU8, colorImage
+	# Rescale the source region to fit width and height, while applying the diffuse color.
+	CALL: Diffuse3x3, colorImage, width, height, red, green, blue, sourceLeft, sourceTop, sourceWidth, sourceHeight, preserved, atlas
+END:
+
+BEGIN: VerticalScrollList
+	INPUT: FixedPoint, width
+	INPUT: FixedPoint, height
+	INPUT: FixedPoint, red
+	INPUT: FixedPoint, green
+	INPUT: FixedPoint, blue
+	OUTPUT: ImageRgbaU8, colorImage
+	CREATE: visImage<ImageU8>, width, height
+	CREATE: lumaImage<ImageU8>, width, height
+	FADE_LINEAR: visImage, 0, 0, 128, width, 0, 0
+	PACK_RGBA: colorImage, 0, 0, 0, visImage
+END:
+
+BEGIN: HorizontalScrollList
+	INPUT: FixedPoint, width
+	INPUT: FixedPoint, height
+	INPUT: FixedPoint, red
+	INPUT: FixedPoint, green
+	INPUT: FixedPoint, blue
+	OUTPUT: ImageRgbaU8, colorImage
+	CREATE: visImage<ImageU8>, width, height
+	CREATE: lumaImage<ImageU8>, width, height
+	FADE_LINEAR: visImage, 0, 0, 128, 0, height, 0
+	PACK_RGBA: colorImage, 0, 0, 0, visImage
+END:

BIN
Source/SDK/guiExample/media/Style.png


BIN
Source/SDK/guiExample/media/Style.xcf


+ 127 - 0
Source/SDK/guiExample/media/Theme.ini

@@ -0,0 +1,127 @@
+atlas = File:Style.png
+preserved = 5
+
+[Button]
+method = "PressableDiffuseSpecular3x3"
+sourceLeft = 32
+sourceTop = 0
+sourceWidth = 16
+sourceHeight = 16
+pressOffsetX = 16
+pressOffsetY = 0
+
+[ListBox]
+method = "Diffuse3x3"
+sourceLeft = 0
+sourceTop = 0
+sourceWidth = 16
+sourceHeight = 16
+
+[TextBox]
+method = "FocusableDiffuse3x3"
+sourceLeft = 0
+sourceTop = 16
+sourceWidth = 16
+sourceHeight = 16
+focusOffsetX = 16
+focusOffsetY = 0
+
+[VerticalScrollKnob]
+method = "PressableDiffuseSpecular1x1"
+sourceLeft = 96
+sourceTop = 0
+sourceWidth = 16
+sourceHeight = 32
+pressOffsetX = 16
+pressOffsetY = 0
+
+[HorizontalScrollKnob]
+method = "PressableDiffuseSpecular1x1"
+sourceLeft = 96
+sourceTop = 32
+sourceWidth = 32
+sourceHeight = 16
+pressOffsetX = 0
+pressOffsetY = 16
+
+[VerticalScrollList]
+method = "VerticalScrollList"
+
+[HorizontalScrollList]
+method = "HorizontalScrollList"
+
+[ScrollUp]
+method = "PressableDiffuseSpecular1x1"
+sourceLeft = 64
+sourceTop = 0
+sourceWidth = 16
+sourceHeight = 16
+pressOffsetX = 16
+pressOffsetY = 0
+
+[ScrollDown]
+method = "PressableDiffuseSpecular1x1"
+sourceLeft = 64
+sourceTop = 16
+sourceWidth = 16
+sourceHeight = 16
+pressOffsetX = 16
+pressOffsetY = 0
+
+[ScrollLeft]
+method = "PressableDiffuseSpecular1x1"
+sourceLeft = 64
+sourceTop = 32
+sourceWidth = 16
+sourceHeight = 16
+pressOffsetX = 16
+pressOffsetY = 0
+
+[ScrollRight]
+method = "PressableDiffuseSpecular1x1"
+sourceLeft = 64
+sourceTop = 48
+sourceWidth = 16
+sourceHeight = 16
+pressOffsetX = 16
+pressOffsetY = 0
+
+[Panel]
+method = "Diffuse3x3"
+sourceLeft = 0
+sourceTop = 0
+sourceWidth = 16
+sourceHeight = 16
+
+[Toolbar]
+method = "Diffuse3x3"
+sourceLeft = 16
+sourceTop = 0
+sourceWidth = 16
+sourceHeight = 16
+
+[MenuTop]
+method = "Diffuse3x3"
+sourceLeft = 32
+sourceTop = 16
+sourceWidth = 16
+sourceHeight = 16
+preserved = 7
+filter = 1
+
+[MenuList]
+method = "Diffuse3x3"
+sourceLeft = 48
+sourceTop = 16
+sourceWidth = 16
+sourceHeight = 16
+filter = 1
+
+[MenuSub]
+method = "Diffuse3x3"
+sourceLeft = 32
+sourceTop = 16
+sourceWidth = 16
+sourceHeight = 16
+preserved = 7
+filter = 1

+ 6 - 0
Source/SDK/guiExample/media/interface.lof

@@ -8,21 +8,27 @@
 		Begin : Menu
 			Name = "menuFile"
 			Text = "File"
+			ListClass = "TextBox"
 			Begin : Menu
+				Color = 120,255,120
 				Name = "menuNew"
 				Text = "New"
 			End
 			Begin : Menu
+				Color = 120,120,255
 				Name = "menuLoad"
 				Text = "Load"
 			End
 			Begin : Menu
+				Color = 255,255,120
 				Name = "menuSave"
 				Text = "Save"
 			End
 			Begin : Menu
+				Color = 255,120,120
 				Name = "menuExit"
 				Text = "Exit"
+				Class = "Button"
 			End
 		End
 		Begin : Menu

+ 1 - 0
Source/tools/wizard/media/Theme.ini

@@ -10,6 +10,7 @@ sourceHeight = 64
 pressOffsetX = 64
 pressOffsetY = 0
 preserved = 16
+filter = 1
 
 [ListBox]
 method = "Diffuse3x3"