SimpleUI: An input-agnostic, skinless UI

Monkey Programming Forums/User Modules/SimpleUI: An input-agnostic, skinless UI

Nobuyuki(Posted 2013) [#1]
Since there appears to be more and more GUI toolkits for monkey coming out lately, and I'm sure a lot of people are getting ready to add more to the pile when they show off theirs, I figured I might as well open source the library I've been working on / using for the past year or so.



It's been used in at least one commercially-released project already, and internally, I've been using it (and still working on it) for at least 2 other projects. It's not done yet! In a bit of a state of flux, method signatures might change to improve consistency (especially with newer components such as WidgetManager/CircularProgressbar/TextBox), but a lot of it has been stable for a year now, so heck, why not.

I imagine the main users of this library (if that's anyone other than me), would be using it because it's designed to be extra modular, and let you design the components you want. Originally, it only included a PushButton to extend from, and that's it. There is no "default skin" or graphical motif you have to follow, you just plop it in your game and draw the controls the way you like.

The main functionality is offered through a polling-based system that handles input for you, and some basic "Events", tied to a generic input interface. There's only one "Complete" end-to-end example widget included, the forementioned PushButton, which you can extend to do whatever you need. Everything else is a basic shell type object which you can expand to do other things.

For example, in the example picture, 3 types of Scroller objects are shown: A typical numbered listbox, an endless looping scroller (both of which are trivial to make with SimpleUI), and a more complex example where the click/scroll functionality's hijacked to spin a ring of vectorballs. All of these are optimized for mobile, and work how you expect scrolling lists of content there to work.

SimpleUI respects your global scissor and matrix. It won't screw with automatic scaling, and it even has functions to push and pop a localized scissor, if you need to localize your widget's blitting area, or start making nested widgets.

SimpleUI also doesn't force you to go with a single input paradigm. The input and the interface are separated, so you can input with a controller or a keyboard if you wanted to on nearly all the widgets. (This is still a WIP for the textbox control! Soft keyboard is hard-coded to mobile platforms, etc.)

So if you don't need an entire library, why not give it a shot?

https://github.com/nobuyukinyuu/SimpleUI/

(coming soon: more widgets)


Amon(Posted 2013) [#2]
Cool, will give it a try in a bit.

You're quite a productive Monkey Coder and it's good to have people like you here.


Sammy(Posted 2013) [#3]
Well done Nobuyuki, thank you! :D


Skn3(Posted 2013) [#4]
Very nice :D


RENGAC(Posted 2013) [#5]
Cool!


Nobuyuki(Posted 2014) [#6]
I've added a 2d Panel object to the collection of widgets available. The panel may provide an easier way to serve content compared to the Scroller classes -- the main disadvantage being that the panel can't loop endlessly. However for many purposes, it is probably a better widget to use. It's simpler to add children (you Attach them like you would to a WidgetManager), and input is "captured" from child widgets in a way that's transparent to the child widget -- the panel employs a special PanelPointer which overrides the child widget's Input and only passes input down when scrolling is not in progress. It still passes down hover events at all times, so your controls won't appear to "Freeze" on desktop targets when hovering.

As usual, there's no default skinning. You can add your own by overriding RenderContent() -- Render() controls the matrix operations, so if you don't want to mess with that, override RenderContent(), and call Super in the place you want the child widgets to render. I've provided PercentX and PercentY properties for your own position indicator drawing convenience.



I've also added a new ScaleAwarePointer for those of you who are too lazy to implement your own. It's compatible with both AutoScale and Autofit.

Finally.... In the process of making my game, I'll be needing to update SimpleUI to better support Multitouch. As a result, I started a MultiTouchPointer class; however, it doesn't implement InputPointer (yet) and I haven't decided if it will or won't. Breaking input-agostity is something I hope to avoid, but it's either that or try to change the simple assumptions InputPointer makes about the input device. It may be enough to simply hack-on backwards-compatibility with InputPointer to MultiTouchPointer, but it would force multitouch widgets (such as the future MultiTouchPanel, which will have zoom, and possibly rotate support) to be aware of this and take a MultiTouchPointer instead, and might not be able to share property names with InputPointer. There's no use for MultiTouchPointer yet, so consider the code a preview of what's to come.


John Galt(Posted 2014) [#7]
Hey thanks man, this looks very nice.


Sammy(Posted 2014) [#8]
Much appreciated, thank you!


Sledge(Posted 2014) [#9]
This looks great, thanks for sharing! Have you got an example of using the new ScrollablePanel class as I'm finding that when I do this...

format_code('
scroll2 = New ScrollablePanel(120,10, 190,300, Cursor)
scroll2.__endlessX = False
scroll2.__endlessY = false
For Local i:Int = 1 To 32
Local newButton:PushButton = New PushButton(0, (32*i)-32, 190, 32, Cursor)
newButton.Text = "Panel Button "+i
scroll2.Attach newButton
Next

widgets.Attach(scroll2)
')

...it won't scroll along the y axis despite the content being taller than the container. If I set __endlessY to True then it scrolls but, well, endlessly(!), which is typically not what one wants. Any guidance appreciated.

Also, if you're interested in feedback, I notice that when you drag a scroller up and down it looks fine (lovely even) regardless of speed, but when you release it and it slows down by itself it uses sub-pixel rendering, which looks awful. An option to have the widgets positioned in integers regardless of whether they are being dragged or animated would be good.


Nobuyuki(Posted 2014) [#10]
@Sledge

Check example.monkey for an example. Make sure you're correctly polling first your InputPointer, and then the widget (or the parent the widget's attached to). There shouldn't be any reason for the control not to scroll if the content size is set to be larger than the dimensions of the panel. (I even tested this on both axes before releasing, so I'm not sure where the problem may lie)


Sledge(Posted 2014) [#11]
Cheers, I'll take another look. Note that the code I posted there is what I added to example.monkey to test it out (just under where the original scroller is defined) so I dunno if there's anything obviously wrong with how the polling would be set up there?

EDIT: Full source of the edited example.monkey
format_codebox('
Strict

Import mojo

'This imports the most basic things needed to get started.
Import SimpleUI.common
'The following are not included in the common init, add them as necessary.
Import SimpleUI.widgetManager
Import SimpleUI.Scrollers
Import SimpleUI.panel

Function Main:Int()
New Game()
Return 0
End Function

Class Game Extends App
Field status:String = "Better example coming soon..."

'This is the cursor we use for the example. It -should- support scaled screens...
Field Cursor:= New ScaledTouchPointer()
'This is a widget manager. For lazy people, you can poll/render batches of widgets at once!
Field widgets:WidgetManager
'Some pushbuttons. You'll want to extend these to provide your own functionality.
Field button:PushButton[3]
'A scroller.
Field scroll:EndlessScroller
Field scroll2:ScrollablePanel

Method OnCreate:Int()
SetUpdateRate 60

'Set up the widget manager to utilize our global InputPointer.
widgets = New WidgetManager(Cursor)

'Initialize the buttons.
For Local i:Int = 0 Until 3
button[i] = New PushButton(16, 32 + i * 48, 96, 32, Cursor)
button[i].Text = "Button " + i

widgets.Attach(button[i])
Next

'Set up the scroller.
scroll = New EndlessScroller(320, 32, 256, 320, 10, Cursor, 48)
scroll.Items = scroll.Items.Resize(10)
For Local i:Int = 0 Until 10
Local c:= New ExampleCell()
c.w = 256
c.h = 48
c.text = "Cell " + i
c.r = Rnd(255)
c.g = Rnd(255)
c.b = Rnd(255)

scroll.Items[i] = c
Next
widgets.Attach(scroll)

scroll2 = New ScrollablePanel(120,10, 190,300, Cursor)
scroll2.__endlessX = False
scroll2.__endlessY = False
For Local i:Int = 1 To 32
Local newButton:PushButton = New PushButton(0, (32*i)-32, 190, 32, Cursor)
newButton.Text = "Panel Button "+i
scroll2.Attach newButton
Next

widgets.Attach(scroll2)

Return 0
End Method

Method OnUpdate:Int()
If KeyHit(KEY_ESCAPE) or KeyHit(KEY_CLOSE) or KeyHit(KEY_BACK) Then Error("")

'In order for anything to detect, we must poll the InputPointer at the beginning of each frame.
Cursor.Poll()

'Tell our widget manager "Okay, let's poll our widgets for input."
widgets.PollAll()

'Now let's check that input.
For Local i:Int = 0 Until 3
If button[i].hit
status = "Button " + i + " hit."
End If
Next

Local i:Int = 1
For Local currentWidget:Widget = Eachin scroll2.Widgets
Local currentButton:PushButton = PushButton(currentWidget)
If currentButton.hit status = "Scroll2 Button " + i + " hit."
i=i+1
next

Return 0
End Method

Method OnRender:Int()
Cls(0, 16, 64)

widgets.RenderAll()

SetAlpha(0.4)
DrawCircle(Cursor.x, Cursor.y, 8)
SetAlpha(1)

Local m:Float[] = GetMatrix()
DrawText(status, 0, 0)
Return 0
End Method
End Class

'Summary: Class providing a SimpleUI InputPointer for an AutoScaled touchscreen. No multitouch.
Class ScaledTouchPointer Extends MousePointer
Method x:Float() Property
Return dTouchX()
End Method
Method y:Float() Property
Return dTouchY()
End Method

'Derived multitouch positions
Function dTouchX:Int(index:Int = 0)
Local m:Float[] = GetMatrix()
Return TouchX(index) / m[0] - (m[4] / m[0])
End Function

Function dTouchY:Int(index:Int=0)
Local m:Float[] = GetMatrix()
Return TouchY(index) / m[3] - (m[5] / m[3])
End Function
End Class
')

EDIT 2: Oh, I'm looking at panel.monkey and the maffs for the __endlessY clause is off... I wrote a stack scroller (sans touch input) just prior to finding your lib and did exactly the same thing I think, so I can probably just look at what I did to fix that and do the same here (if you don't beat me to it with an official fix). I should be good, ta :)

EDIT 3: Okay, I was suspicious of the maffs because when I took 'em out the thing went back to scrolling BUT it turns out the problem was that ch for scroll2 was always 0.0 so it threw your (perfectly fine) maths awry. This is easily fixed by updating ch as one adds elements to the ScrollablePanel widget, for example:
format_codebox('
Strict

Import mojo

'This imports the most basic things needed to get started.
Import SimpleUI.common
'The following are not included in the common init, add them as necessary.
Import SimpleUI.widgetManager
Import SimpleUI.Scrollers
Import SimpleUI.panel

Function Main:Int()
New Game()
Return 0
End Function

Class Game Extends App
Field status:String = "Better example coming soon..."

'This is the cursor we use for the example. It -should- support scaled screens...
Field Cursor:= New ScaledTouchPointer()
'This is a widget manager. For lazy people, you can poll/render batches of widgets at once!
Field widgets:WidgetManager
'Some pushbuttons. You'll want to extend these to provide your own functionality.
Field button:PushButton[3]
'A scroller.
Field scroll:EndlessScroller
Field scroll2:ScrollablePanel

Method OnCreate:Int()
SetUpdateRate 60

'Set up the widget manager to utilize our global InputPointer.
widgets = New WidgetManager(Cursor)

'Initialize the buttons.
For Local i:Int = 0 Until 3
button[i] = New PushButton(16, 32 + i * 48, 96, 32, Cursor)
button[i].Text = "Button " + i

widgets.Attach(button[i])
Next

'Set up the scroller.
scroll = New EndlessScroller(320, 32, 256, 320, 10, Cursor, 48)
scroll.Items = scroll.Items.Resize(10)
For Local i:Int = 0 Until 10
Local c:= New ExampleCell()
c.w = 256
c.h = 48
c.text = "Cell " + i
c.r = Rnd(255)
c.g = Rnd(255)
c.b = Rnd(255)

scroll.Items[i] = c
Next
widgets.Attach(scroll)

scroll2 = New ScrollablePanel(120,10, 190,300, Cursor)
scroll2.__endlessX = False
scroll2.__endlessY = False
For Local i:Int = 1 To 32
Local newButton:PushButton = New PushButton(0, (32*i)-32, 190, 32, Cursor)
newButton.Text = "Panel Button "+i
scroll2.Attach newButton
scroll2.ch = scroll2.ch + 32 ' <<<<<< HERE!!!!!!!
Next

widgets.Attach(scroll2)

Return 0
End Method

Method OnUpdate:Int()
If KeyHit(KEY_ESCAPE) or KeyHit(KEY_CLOSE) or KeyHit(KEY_BACK) Then Error("")

'In order for anything to detect, we must poll the InputPointer at the beginning of each frame.
Cursor.Poll()

'Tell our widget manager "Okay, let's poll our widgets for input."
widgets.PollAll()

'Now let's check that input.
For Local i:Int = 0 Until 3
If button[i].hit
status = "Button " + i + " hit."
End If
Next

Local i:Int = 1
For Local currentWidget:Widget = Eachin scroll2.Widgets
Local currentButton:PushButton = PushButton(currentWidget)
If currentButton.hit status = "Scroll2 Button " + i + " hit."
i=i+1
next

Return 0
End Method

Method OnRender:Int()
Cls(0, 16, 64)

widgets.RenderAll()

SetAlpha(0.4)
DrawCircle(Cursor.x, Cursor.y, 8)
SetAlpha(1)

Local m:Float[] = GetMatrix()
DrawText(status, 0, 0)
Return 0
End Method
End Class

'Summary: Class providing a SimpleUI InputPointer for an AutoScaled touchscreen. No multitouch.
Class ScaledTouchPointer Extends MousePointer
Method x:Float() Property
Return dTouchX()
End Method
Method y:Float() Property
Return dTouchY()
End Method

'Derived multitouch positions
Function dTouchX:Int(index:Int = 0)
Local m:Float[] = GetMatrix()
Return TouchX(index) / m[0] - (m[4] / m[0])
End Function

Function dTouchY:Int(index:Int=0)
Local m:Float[] = GetMatrix()
Return TouchY(index) / m[3] - (m[5] / m[3])
End Function
End Class
')

If there's something else I'm supposed to be doing that means ch gets updated automagically then please let me know as I've obviously missed it. Like I said, I'd been working on a scroller but was kinda dreading having to add touch control to it and make it carry clickable objects properly (i.e. responding appropriately to something that was potentially a click turning into a swipe) because I just wanted to get on with the actual application -- you've no idea what a godsend SimpleUI is.


Nobuyuki(Posted 2014) [#12]
No, the panel's content dimensions aren't updated automatically at all. That's up to you! And, as you've found out, the content size defaults to 0, which basically means that it won't scroll and will act like a simple scissor container for the objects attached to it. You can define a larger content size in either the extended constructor, or by setting cw/ch manually.


monkeyteets(Posted 2014) [#13]
Hey Nobuyuki, Just tried out SimpleUI example, looks really nice!
Scrolling is liquidy-smooth!

Do you have any plans for 9 slice scaling?

Edit: I just saw you created NinePatch already? Will these projects get merged?

Would be super helpful!


Nobuyuki(Posted 2014) [#14]
@monkeyteets

I like to keep my projects modular, so that people don't get "locked-in" to a framework or certain set of code if it can be avoided -- since I always want to improve my code but also have it be adopted, I try to avoid big integrated solutions that would break existing code which relies on the implementation when upgraded.

Perhaps the UI class could stand to have a NinePatch function? But it would simply provide the same functionality as before.... In any case, there is no built-in use of Image anywhere in the lib so I think maybe SimpleUI's not the best place for it in any case!

Thanks for reminding me about NinePatch, though; I was thinking about coming back to it and rewriting a nDrawExts2 version so that it's both seamless and supports image alpha

Edit: I lied; CircularProgressBars use images. But still!


monkeyteets(Posted 2014) [#15]
I like your style of keeping your modules.. modular :P
the nDrawExts2 idea would be really cool!

In any case, I imagine most ui elements would be skinned for production release, you know best though.


Nobuyuki(Posted 2014) [#16]
SimpleUI now supports a preliminary way of loading widgets from JSON metadata(!)



This works automatically with some of the basic widgets included with the library (Widget, PushButton, Textbox). I will look to try adding other widgets later, since I kinda got lazy and don't want to write the deep inspection stuff now (particularly for Panel, which will require its own unpacking routines). Custom widgets derived from the base class are of course also supported by the json unpacker. To implement unpacking for your own widgets, you need to do a few things:

1. Override the Spawn() method of your widget so that the output returns the type of your widget (Stupid hack, what I wouldn't give for a TypeOf operator!)
2. (If necessary) Override the UnpackJSON() method and call Super if you want to unpack the base class fields. Then, load your custom fields from the supplied json argument.
3. Update Unpacker.ValidTypes with a new copy of your widget prototype. This allows the Unpacker to call your widget type's custom overrides and spawn new ones.
4. Call Unpacker.UnpackForm(json:String), it returns a new WidgetManager containing all the widgets defined in the json! Magic :o

Obviously this is a bit preliminary and my first attempt at this kinda generic de-serialization of objects. However, the advantages of being able to load from metadata are pretty obvious: You can now write your own form editors! Automatic serialization is not currently implemented for SimpleUI, however, I doubt that's going to be a problem since the module was originally written for games, not form designer apps. You can serialize your own data externally, or use the new formdesigner.monkey example included with the module to see one way how to do it. The data structure's really simple and can be incorporated into your level format: Simply add a "Widgets" keypair, with the data being an array of objects. UnpackForm() does the rest.


CopperCircle(Posted 2014) [#17]
Great, thanks for the update.


maltic(Posted 2014) [#18]
Just thought I would let you know I am using this with great success in a project of mine. Thank you for making it, and I hope you keep adding features!


Nobuyuki(Posted 2014) [#19]
Thanks guys, I'll be sure to keep working on it. I make new widgets all the time for projects I'm paid for, but they're usually little things to go with my personal framework and thus aren't fit for inclusion in there. Things like atlas buttons and special toggles and such. I really want to genericize a few of these things (like toggles, progress bars, sliders and option lists), but they're imperfect to various degrees and I don't want them to start being used everywhere should I decide to rewrite them later and force people to refactor.

A little bird told me that any serious new features I have had plans for in the near future may have to be delayed from a public release until a few more projects have been published. That means that you'll probably see certain widgets/features being used in a commercial project first before being "handed down" to SimpleUI for the time being. I made json unpacking on a whim as a request for someone, and got a little bit of hell for sharing the code publicly the same day I wrote it from someone else (lol) <:)

In particular, I was planning on making a zoomable version of the ScrollablePanel with support for MultiTouchPointer. I probably won't be allowed to put it up on GitHub though until we've got at least 1 or 2 games out that use it. There's nothing that'll stop you from making your own multitouch controls now, though, since on my last update I fixed up the multitouch pointers which are now backwards-compatible with InputPointer. You can make your global game cursor a ScaleAwareMultitouchPointer with no ill effects, and extend widgets yourself to take advantage of it right now! But sadly will have to wait for perhaps the most obvious way to use it.

Watch this space.....


Landon(Posted 2014) [#20]
This UI is slick, i think what i love about it the most is it's barebones. I can just extend a widget and change the rending a bit and bam it's fully skinned the way i want. I don't have to hassle with some external xml or JSON file to make it look right.

Great job!


Nobuyuki(Posted 2016) [#21]
SimpleUI now has support for a color picker.



I still haven't added the color sliders I made many years ago, but maybe those will be next? :)

This wheel was designed for a palette editor that I hope to use in combination with a simple tile and animation editor to make doing 8-bit style games in Monkey a bit easier. However it's also a great addition to any graphics editor.


CopperCircle(Posted 2016) [#22]
Great, yeh sliders and knobs would be good.


Amon(Posted 2016) [#23]
Cool. This just keeps getting better and better.

Thank, Nobuyuki!