├── .gitignore ├── SimpleUI ├── common.monkey ├── pushbutton.monkey ├── unpacker.monkey ├── circularProgressbar.monkey ├── widget.monkey ├── textbox.monkey ├── ui.monkey ├── widgetManager.monkey ├── InputPointers.monkey ├── colorPicker.monkey ├── panel.monkey └── Scrollers.monkey ├── README.md ├── formdesigner.data ├── testOutput.json └── testOutput2.json ├── formdesigner.monkey └── example.monkey /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the build folder 2 | /*.build*/ 3 | 4 | # Ignore the docs folder 5 | /*.docs/ 6 | 7 | # Ignore Jungle IDE files 8 | *.jungle* 9 | *.jiproj* 10 | *.jipo 11 | *.JILayout 12 | 13 | # Ignore thumbs 14 | Thumbs.db 15 | 16 | # Ignore compiled APKs 17 | *.apk 18 | 19 | # Ignore debug log 20 | debug.log 21 | -------------------------------------------------------------------------------- /SimpleUI/common.monkey: -------------------------------------------------------------------------------- 1 | 'Copyright © 2013 Nobuyuki (nobu@subsoap.com) 2 | ' 3 | 'Permission is hereby granted, free of charge, To any person obtaining a copy 4 | 'of this software and associated documentation files (the "Software"), to deal 5 | 'in the Software without restriction, including without limitation the rights 6 | 'to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | 'copies of the Software, and to permit persons to whom the Software is 8 | 'furnished to do so, subject to the following conditions: 9 | ' 10 | 'The above copyright notice and this permission notice shall be included in 11 | 'all copies or substantial portions of the Software. 12 | ' 13 | 'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | 'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | 'FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | 'AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | 'LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | 'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | 'THE SOFTWARE. 20 | 21 | Import ui 22 | Import InputPointers 23 | Import widget 24 | Import pushbutton -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SimpleUI is a basic, portable, and modular UI library for Monkey. It serves as a UI base for projects that don't need a full library. 2 | 3 | ![](http://i.imgur.com/acs0W74.png) 4 | 5 | ## Features 6 | * Input and interface are separate, allowing (in theory) any control scheme. 7 | * Skinnable (actually, most widgets have no default look!) 8 | * Poll-based refreshing 9 | * Batch widget management 10 | * Plays nice global matrix scaling 11 | 12 | ## Widgets 13 | * Push button 14 | * Text box 15 | * Circular ProgressBar 16 | * Scroller ListBoxes with click/slide support 17 | * Looping Scroller panels (EndlessScroller) 18 | * 2d Scrolling panels (ScrollablePanel) 19 | * Color wheel picker (CircularPicker) **(New)** 20 | 21 | ## Planned improvements 22 | ### Coded, but not included yet 23 | * OptionList 24 | * Linear ProgressBar 25 | * Slider 26 | * RGB Slider 27 | * Toggle button (Checkbox) 28 | * Knob (circular progressbar + toggle) 29 | 30 | ### Not yet coded 31 | * Multitouch support 32 | * Widget: OptionTable (2d optionlist) 33 | * WidgetManager: automatic reflow support 34 | 35 | ## Donate 36 | If you like this project, please consider giving a small donation to support further development. 37 | 38 | [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=RHZMPB4RL3T82&lc=US&item_name=Nobu%27s%20Monkey%2dX%20projects¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted) 39 | 40 | -------------------------------------------------------------------------------- /SimpleUI/pushbutton.monkey: -------------------------------------------------------------------------------- 1 | Import mojo 2 | Import InputPointers 3 | Import widget 4 | 5 | 'Summary: A basic button class which many controls derive from. 6 | Class PushButton Extends Widget 7 | Method type:String() Property 'The type of this widget. 8 | Return "pushbutton" 9 | End Method 10 | 11 | Field hit:Bool 'Click event indicator 12 | Field down:Bool 'MouseDown event indicator 13 | Field up:Bool 'MouseUp event indicator 14 | Field over:Bool 'MouseOver event indicator 15 | 16 | Method New(x:Float, y:Float, w:Float, h:Float, Input:InputPointer) 17 | Super.New(x, y, w, h, Input) 18 | End Method 19 | 20 | Method Render:Void(xOffset:Float = 0, yOffset:Float = 0) 21 | If Visible 22 | Local b:Int; If over Then b = 16 23 | SetColor(128, 128 + b, 192 + b) 24 | If down Then SetColor(128, 192, 192) 25 | 26 | DrawRect(x + xOffset, y + yOffset, w, h) 27 | SetColor(255, 255, 255) 28 | DrawText(Self.Text, (x + w / 2) + xOffset, (y + h / 2) + yOffset, 0.5, 0.5) 29 | End If 30 | End Method 31 | 32 | Method Poll:Void(xOffset:Float = 0, yOffset:Float = 0) 33 | hit = False 34 | down = False 35 | up = False 36 | over = False 37 | Super.Poll(xOffset, yOffset) 38 | End Method 39 | 40 | Method MouseClick:Void() 41 | Super.MouseClick() 42 | If Visible And Enabled Then hit = True 43 | End Method 44 | 45 | Method MouseDown:Void() 46 | Super.MouseDown() 47 | If Visible And Enabled Then down = True 48 | End Method 49 | 50 | Method MouseUp:Void() 51 | Super.MouseUp() 52 | If Visible and Enabled Then up = True 53 | End Method 54 | 55 | Method MouseOver:Void() 56 | If Visible and Enabled Then over = True 57 | End Method 58 | 59 | 'Summary: Spawns a new PushButton for the unpacker. 60 | Method Spawn:Widget(json:String) 61 | 'Override this method to allow spawning of derived types. 62 | Local out:= New PushButton() 63 | out.UnpackJSON(json) 64 | Return out 65 | End Method 66 | End Class 67 | -------------------------------------------------------------------------------- /SimpleUI/unpacker.monkey: -------------------------------------------------------------------------------- 1 | 'The unpacker requires access to all the common widgets. Therefore, if you don't want to import all widgets' 2 | 'namespaces to your project, you don't need to include unpacker if you don't use it. -nobu (6 Feb 2014) 3 | Import common 4 | Import widgetManager 5 | Import panel 6 | Import Scrollers 7 | Import textbox 8 | 9 | 'Summary: Singleton class providing a way to easily unpack widgets from a JSON file to a WidgetManager. 10 | Class Unpacker Final 11 | Global ValidTypes:StringMap 12 | Global defaultType:String = "widget" 13 | 14 | 'Summary: Initializes the ValidTypes map with the built-in widgets. 15 | Function Init:Void() 16 | ValidTypes = New StringMap 17 | 18 | ValidTypes.Add("widget", New Widget()) 19 | ValidTypes.Add("pushbutton", New PushButton()) 20 | ' ValidTypes.Add("panel", New ScrollablePanel()) 21 | ' ValidTypes.Add("scroller"), New Scroller()) 22 | ' ValidTypes.Add("endless_scroller", New EndlessScroller()) 23 | ValidTypes.Add("textbox", New TextBox()) 24 | End Function 25 | 26 | 'Summary: Unpacks a series of objects in a SimpleUI Form to a WidgetManager. 27 | Function UnpackForm:WidgetManager(json:String, Input:InputPointer = Null) 28 | Try 29 | Local j:= New JsonObject(json) 30 | Local ws:JsonArray = JsonArray(j.Get("Widgets")) 31 | 32 | Local out:= New WidgetManager(Input) 33 | If ws = Null Then Return out 34 | 35 | For Local i:Int = 0 Until ws.Length() 36 | Local obj:= Unpack(JsonObject(ws.Get(i))) 37 | 38 | If obj <> Null Then out.Attach(obj) 39 | Next 40 | Return out 41 | Catch ex:JsonError 42 | Print "Unpacker: Warning, data corrupt. UnpackForm failed." 43 | Return New WidgetManager(Input) 44 | End Try 45 | End Function 46 | 47 | 'Summary: Returns a single unpacked widget from a JSON object. 48 | Function Unpack:Widget(j:JsonObject) 49 | If ValidTypes = Null Then Error("Unpacker not initialized") 50 | 51 | Local unpackType:String = j.GetString("type") 52 | If unpackType = "" Then 53 | Print("Unpacker: '" + unpackType + "' is not part of Unpacker.ValidTypes.") 54 | Print("Unpacker: Defaulting to '" + defaultType + "'...") 55 | unpackType = defaultType 56 | End If 57 | 58 | Local widget:= ValidTypes.Get(unpackType) 59 | 60 | If widget = Null Then 61 | Print("Unpacker: Warning, unpacking widget as '" + unpackType + "' failed.") 62 | Return Null 63 | End If 64 | 65 | Return widget.Spawn(j.ToJson()) 66 | End Function 67 | End Class -------------------------------------------------------------------------------- /SimpleUI/circularProgressbar.monkey: -------------------------------------------------------------------------------- 1 | 'The circular progressBar works by displaying 4 masked quadrants and rotating them to the proper orientation. 2 | 3 | Import mojo 4 | Import ui 5 | 6 | Class CircularProgressBar 7 | Private 8 | Field quad:Image[4] 'Quadrant image 9 | 10 | Public 11 | Field r:Float 'Radius 12 | Field CloverLeaf:Bool 'Display as cloverleaf instead of full circle 13 | 14 | Field Percent:Float 'Amount full to display this progress bar 15 | 16 | Method New(img:Image, CloverLeaf:Bool = False) 17 | Self.CloverLeaf = CloverLeaf 18 | Self.r = img.Width / 2 19 | SetImages(img) 20 | End Method 21 | 22 | Method SetImages(img:Image) 23 | quad[0] = img.GrabImage(0, r, r, r) 24 | quad[1] = img.GrabImage(0, 0, r, r) 25 | quad[2] = img.GrabImage(r, 0, r, r) 26 | quad[3] = img.GrabImage(r, r, r, r) 27 | 28 | 'Set every corner to virtual center. 29 | quad[0].SetHandle(r, 0) 30 | quad[1].SetHandle(r, r) 31 | quad[2].SetHandle(0, r) 32 | quad[3].SetHandle(0, 0) 33 | End Method 34 | 35 | Method Render:Void(x:Float, y:Float, scl:Float = 1.0) 36 | If CloverLeaf 37 | DrawQuad(0, x, y, Percent, scl) 38 | DrawQuad(1, x, y, Percent, scl) 39 | DrawQuad(2, x, y, Percent, scl) 40 | DrawQuad(3, x, y, Percent, scl) 41 | Else 42 | 43 | Local quadrant:Int = Min(3.0, Percent * 4) 'returns 0-3 44 | Select quadrant 45 | Case 0 46 | DrawQuad(0, x, y, Percent / 0.25, scl) 47 | Case 1 48 | DrawQuad(0, x, y, scl) 49 | DrawQuad(1, x, y, (Percent - 0.25) / 0.25, scl) 50 | Case 2 51 | DrawQuad(0, x, y, scl) 52 | DrawQuad(1, x, y, scl) 53 | DrawQuad(2, x, y, (Percent - 0.5) / 0.25, scl) 54 | Case 3 55 | DrawQuad(0, x, y, scl) 56 | DrawQuad(1, x, y, scl) 57 | DrawQuad(2, x, y, scl) 58 | DrawQuad(3, x, y, (Percent - 0.75) / 0.25, scl) 59 | End Select 60 | 61 | 62 | End If 63 | End Method 64 | 65 | 'Summary: Draw full quadrants 66 | Method DrawQuad:Void(quadrant:Int, x:Float, y:Float, scl:Float) 67 | DrawImage(quad[quadrant], x, y, 0, scl, scl) 68 | End Method 69 | 70 | 'Summary: Draw partial quadrant 71 | Method DrawQuad(quadrant:Int, x:Float, y:Float, Percent:Float, scl:Float) 72 | UI.PushScissor() 73 | Select quadrant 74 | Case 0 75 | xfScissor(x - r, y, r, r) 'LL Quadrant 0 76 | Case 1 77 | xfScissor(x - r, y - r, r, r) 'UL Quadrant 1 78 | Case 2 79 | xfScissor(x, y - r, r, r) 'UR Quadrant 2 80 | Case 3 81 | xfScissor(x, y, r, r) 'LR Quadrant 3 82 | End Select 83 | 84 | DrawImage(quad[quadrant], x, y, 90 - (90 * Percent), scl, scl) 85 | UI.PopScissor() 86 | End Method 87 | 88 | 'Summary: Transformed scissor 89 | Function xfScissor:Void(x:Float, y:Float, w:Float, h:Float) 90 | Local matrix:Float[] = GetMatrix() 91 | SetScissor(x * matrix[0] + matrix[4], y * matrix[3] + matrix[5], w * matrix[0], h * matrix[3]) 92 | End Function 93 | End Class 94 | -------------------------------------------------------------------------------- /formdesigner.data/testOutput.json: -------------------------------------------------------------------------------- 1 | { 2 | "Widgets" : [{ 3 | "Enabled" : true, 4 | "Visible" : true, 5 | "h" : 97.0, 6 | "id" : 4325376, 7 | "name" : "nira", 8 | "type" : "pushbutton", 9 | "w" : 99.0, 10 | "x" : 107.0, 11 | "y" : 64.0 12 | }, { 13 | "Enabled" : true, 14 | "Visible" : true, 15 | "h" : 163.0, 16 | "id" : 1687552001, 17 | "name" : "tohara", 18 | "type" : "pushbutton", 19 | "w" : 118.0, 20 | "x" : 294.0, 21 | "y" : 189.0 22 | }, { 23 | "Enabled" : true, 24 | "Visible" : true, 25 | "h" : 62.0, 26 | "id" : 1328283650, 27 | "name" : "judunoijee", 28 | "type" : "pushbutton", 29 | "w" : 196.0, 30 | "x" : 46.0, 31 | "y" : 198.0 32 | }, { 33 | "Enabled" : true, 34 | "Visible" : true, 35 | "h" : 63.0, 36 | "id" : 222298115, 37 | "name" : "tigaturi", 38 | "type" : "pushbutton", 39 | "w" : 224.0, 40 | "x" : 312.0, 41 | "y" : 75.0 42 | }, { 43 | "Enabled" : true, 44 | "Visible" : true, 45 | "h" : 37.0, 46 | "id" : 2005794820, 47 | "name" : "riajune", 48 | "type" : "pushbutton", 49 | "w" : 153.0, 50 | "x" : 95.0, 51 | "y" : 409.0 52 | }, { 53 | "Enabled" : true, 54 | "Visible" : true, 55 | "h" : 88.0, 56 | "id" : 1677328389, 57 | "name" : "reerii", 58 | "type" : "pushbutton", 59 | "w" : 131.0, 60 | "x" : 477.0, 61 | "y" : 334.0 62 | }, { 63 | "Enabled" : true, 64 | "Visible" : true, 65 | "h" : 81.0, 66 | "id" : 400424966, 67 | "name" : "pionahihe", 68 | "type" : "pushbutton", 69 | "w" : 200.0, 70 | "x" : 266.0, 71 | "y" : 371.0 72 | }, { 73 | "Enabled" : true, 74 | "Visible" : true, 75 | "h" : 125.0, 76 | "id" : 815398919, 77 | "name" : "kura", 78 | "type" : "pushbutton", 79 | "w" : 224.0, 80 | "x" : 28.0, 81 | "y" : 268.0 82 | }, { 83 | "Enabled" : true, 84 | "Visible" : true, 85 | "h" : 75.0, 86 | "id" : 1597767688, 87 | "name" : "boujeog", 88 | "type" : "pushbutton", 89 | "w" : 189.0, 90 | "x" : 419.0, 91 | "y" : 157.0 92 | }, { 93 | "Enabled" : true, 94 | "Visible" : true, 95 | "h" : 86.0, 96 | "id" : 1647050761, 97 | "name" : "hije", 98 | "type" : "pushbutton", 99 | "w" : 189.0, 100 | "x" : 418.0, 101 | "y" : 237.0 102 | }, { 103 | "Enabled" : true, 104 | "Visible" : true, 105 | "h" : 37.0, 106 | "id" : 499515402, 107 | "name" : "rehitita", 108 | "type" : "pushbutton", 109 | "w" : 177.0, 110 | "x" : 229.0, 111 | "y" : 145.0 112 | }, { 113 | "Enabled" : true, 114 | "Visible" : true, 115 | "h" : 71.0, 116 | "id" : 1588461579, 117 | "name" : "jutousio", 118 | "type" : "pushbutton", 119 | "w" : 90.0, 120 | "x" : 212.0, 121 | "y" : 62.0 122 | }, { 123 | "Enabled" : true, 124 | "Visible" : true, 125 | "h" : 65.0, 126 | "id" : 1630666764, 127 | "name" : "herorujog", 128 | "type" : "pushbutton", 129 | "w" : 44.0, 130 | "x" : 244.0, 131 | "y" : 193.0 132 | }, { 133 | "Enabled" : true, 134 | "Visible" : true, 135 | "h" : 99.0, 136 | "id" : 328073229, 137 | "name" : "jujeojikea", 138 | "type" : "pushbutton", 139 | "w" : 33.0, 140 | "x" : 255.0, 141 | "y" : 265.0 142 | }, { 143 | "Enabled" : true, 144 | "Visible" : true, 145 | "h" : 25.0, 146 | "id" : 1865023502, 147 | "name" : "jikuru", 148 | "type" : "pushbutton", 149 | "w" : 190.0, 150 | "x" : 32.0, 151 | "y" : 166.0 152 | }, { 153 | "Enabled" : true, 154 | "Visible" : true, 155 | "h" : 97.0, 156 | "id" : 1035993103, 157 | "name" : "togi", 158 | "type" : "pushbutton", 159 | "w" : 74.0, 160 | "x" : 24.0, 161 | "y" : 61.0 162 | } 163 | ] 164 | } -------------------------------------------------------------------------------- /formdesigner.data/testOutput2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Widgets" : [{ 3 | "Enabled" : true, 4 | "Visible" : true, 5 | "h" : 97.0, 6 | "id" : 4325376, 7 | "Text" : "nira", 8 | "type" : "pushbutton", 9 | "w" : 99.0, 10 | "x" : 107.0, 11 | "y" : 64.0 12 | }, { 13 | "Enabled" : true, 14 | "Visible" : true, 15 | "h" : 163.0, 16 | "id" : 1687552001, 17 | "Text" : "tohara", 18 | "type" : "pushbutton", 19 | "w" : 118.0, 20 | "x" : 294.0, 21 | "y" : 189.0 22 | }, { 23 | "Enabled" : true, 24 | "Visible" : true, 25 | "h" : 62.0, 26 | "id" : 1328283650, 27 | "Text" : "judunoijee", 28 | "type" : "pushbutton", 29 | "w" : 196.0, 30 | "x" : 46.0, 31 | "y" : 198.0 32 | }, { 33 | "Enabled" : true, 34 | "Visible" : true, 35 | "h" : 63.0, 36 | "id" : 222298115, 37 | "Text" : "tigaturi", 38 | "type" : "pushbutton", 39 | "w" : 224.0, 40 | "x" : 312.0, 41 | "y" : 75.0 42 | }, { 43 | "Enabled" : true, 44 | "Visible" : true, 45 | "h" : 37.0, 46 | "id" : 2005794820, 47 | "Text" : "riajune", 48 | "type" : "pushbutton", 49 | "w" : 153.0, 50 | "x" : 95.0, 51 | "y" : 409.0 52 | }, { 53 | "Enabled" : true, 54 | "Visible" : true, 55 | "h" : 88.0, 56 | "id" : 1677328389, 57 | "Text" : "reerii", 58 | "type" : "pushbutton", 59 | "w" : 131.0, 60 | "x" : 477.0, 61 | "y" : 334.0 62 | }, { 63 | "Enabled" : true, 64 | "Visible" : true, 65 | "h" : 81.0, 66 | "id" : 400424966, 67 | "Text" : "pionahihe", 68 | "type" : "pushbutton", 69 | "w" : 200.0, 70 | "x" : 266.0, 71 | "y" : 371.0 72 | }, { 73 | "Enabled" : true, 74 | "Visible" : true, 75 | "h" : 125.0, 76 | "id" : 815398919, 77 | "Text" : "kura", 78 | "type" : "pushbutton", 79 | "w" : 224.0, 80 | "x" : 28.0, 81 | "y" : 268.0 82 | }, { 83 | "Enabled" : true, 84 | "Visible" : true, 85 | "h" : 75.0, 86 | "id" : 1597767688, 87 | "Text" : "boujeog", 88 | "type" : "pushbutton", 89 | "w" : 189.0, 90 | "x" : 419.0, 91 | "y" : 157.0 92 | }, { 93 | "Enabled" : true, 94 | "Visible" : true, 95 | "h" : 86.0, 96 | "id" : 1647050761, 97 | "Text" : "hije", 98 | "type" : "pushbutton", 99 | "w" : 189.0, 100 | "x" : 418.0, 101 | "y" : 237.0 102 | }, { 103 | "Enabled" : true, 104 | "Visible" : true, 105 | "h" : 37.0, 106 | "id" : 499515402, 107 | "Text" : "rehitita", 108 | "type" : "pushbutton", 109 | "w" : 177.0, 110 | "x" : 229.0, 111 | "y" : 145.0 112 | }, { 113 | "Enabled" : true, 114 | "Visible" : true, 115 | "h" : 71.0, 116 | "id" : 1588461579, 117 | "Text" : "jutousio", 118 | "type" : "pushbutton", 119 | "w" : 90.0, 120 | "x" : 212.0, 121 | "y" : 62.0 122 | }, { 123 | "Enabled" : true, 124 | "Visible" : true, 125 | "h" : 65.0, 126 | "id" : 1630666764, 127 | "Text" : "herorujog", 128 | "type" : "pushbutton", 129 | "w" : 44.0, 130 | "x" : 244.0, 131 | "y" : 193.0 132 | }, { 133 | "Enabled" : true, 134 | "Visible" : true, 135 | "h" : 99.0, 136 | "id" : 328073229, 137 | "Text" : "jujeojikea", 138 | "type" : "pushbutton", 139 | "w" : 33.0, 140 | "x" : 255.0, 141 | "y" : 265.0 142 | }, { 143 | "Enabled" : true, 144 | "Visible" : true, 145 | "h" : 25.0, 146 | "id" : 1865023502, 147 | "Text" : "jikuru", 148 | "type" : "pushbutton", 149 | "w" : 190.0, 150 | "x" : 32.0, 151 | "y" : 166.0 152 | }, { 153 | "Enabled" : true, 154 | "Visible" : true, 155 | "h" : 97.0, 156 | "id" : 1035993103, 157 | "Text" : "togi", 158 | "type" : "pushbutton", 159 | "w" : 74.0, 160 | "x" : 24.0, 161 | "y" : 61.0 162 | } 163 | ] 164 | } -------------------------------------------------------------------------------- /SimpleUI/widget.monkey: -------------------------------------------------------------------------------- 1 | Import mojo 2 | Import InputPointers 3 | Import ui 4 | 5 | 'Summary: This is the base class which all SimpleUI components derive from. 6 | Class Widget 7 | Private 8 | Field Holding:Bool 'If holding down in the widget 9 | 10 | Public 11 | Method type:String() Property 'The type of this widget. 12 | Return "widget" 13 | End Method 14 | 15 | Field id:Int = -1 'For tagging a widget for the widget manager. Optional. 16 | Field name:String 'For tagging a widget for the widget manager. Optional. 17 | 18 | Field x:Float, y:Float 'Location 19 | Field w:Float=32, h:Float=32 'Size 20 | Field Input:InputPointer 21 | Field Text:String 22 | Field Visible:Bool = True 23 | Field Enabled:Bool = True 24 | 25 | Method New(x:Float, y:Float, w:Float, h:Float) 26 | Self.x=x ; Self.y=y ; Self.w=w ; Self.h=h 27 | 28 | Input = New MousePointer() 29 | End Method 30 | 31 | Method New(x:Float, y:Float, w:Float, h:Float, input:InputPointer) 32 | Self.x=x ; Self.y=y ; Self.w=w ; Self.h=h 33 | 34 | Self.Input = input 35 | End Method 36 | 37 | 'Summary: This function polls the widget to see if it should execute any methods. Don't call if not Visible. 38 | Method Poll:Void(xOffset:Float = 0, yOffset:Float = 0) 39 | Local inWidget:Bool = UI.WithinRect(Input.x, Input.y, Self.x + xOffset, Self.y + yOffset, Self.w, Self.h) 40 | 41 | 'Check input to do things. In derived classes, you can poll the InputPointer before calling Super here. 42 | If inWidget Then 43 | Self.MouseOver() 44 | 45 | If Input.Hit Then 46 | Holding = True 47 | Self.MouseHit() 48 | End If 49 | 50 | If Input.Down Then Self.MouseDown() 51 | 52 | If Input.Up Then 'User releasing input over this control. 53 | Self.MouseUp() 54 | 'Check to see if the widget was tapped earlier. If so, activate the Click event. 55 | If Holding = True Then 56 | Self.MouseClick() 57 | Holding = False 58 | End If 59 | End If 60 | 61 | End If 62 | 63 | If Not Input.Down Then Holding = False 'Deactivate Holding whether or not we're in the control 64 | End Method 65 | 66 | Method MouseOver:Void() 67 | 'Put Hover event code in here when extending the class 68 | End Method 69 | 70 | Method MouseDown:Void() 71 | 'Put MouseDown event code in here when extending the class 72 | End Method 73 | 74 | Method MouseHit:Void() 75 | 'Put MouseHit event code in here when extending the class 76 | End Method 77 | 78 | Method MouseUp:Void() 79 | 'Put MouseUp event code in here when extending the class 80 | End Method 81 | 82 | Method MouseClick:Void() 83 | 'Put event code in here when extending the class for when a control was clicked 84 | End Method 85 | 86 | Method Render:Void(xOffset:Float = 0, yOffset:Float = 0) 87 | 'Overload me! 88 | End Method 89 | 90 | 91 | 'Note: incomplete..... 06 feb 2014 92 | 'Summary: Unpacks a JSON string to the fields of this Widget. 93 | Method UnpackJSON:Void(json:String) 94 | 'Override me in a subclass and call Super to automatically deal with this stuff. 95 | Local j:= New JsonObject(json) 96 | 97 | Self.id = j.GetInt("id", -1) 98 | Self.name = j.GetString("name", "") 99 | 100 | Self.x = j.GetFloat("x") 101 | Self.y = j.GetFloat("y") 102 | Self.w = j.GetFloat("w") 103 | Self.h = j.GetFloat("h") 104 | 105 | Self.Text = j.GetString("Text", "") 106 | Self.Visible = j.GetBool("Visible", True) 107 | Self.Enabled = j.GetBool("Enabled", True) 108 | End Method 109 | 110 | 'Summary: Spawns a new Widget. 111 | Method Spawn:Widget(json:String) 112 | 'Override this method to allow spawning of derived types. 113 | Local out:= New Widget() 114 | out.UnpackJSON(json) 115 | Return out 116 | End Method 117 | End Class 118 | -------------------------------------------------------------------------------- /SimpleUI/textbox.monkey: -------------------------------------------------------------------------------- 1 | Import ui 2 | Import InputPointers 3 | Import widget 4 | 5 | Class TextBox Extends Widget 6 | Method type:String() Property 'The type of this widget. 7 | Return "textbox" 8 | End Method 9 | 10 | Field HasFocus:Bool 11 | Field skin:TextBoxSkin = New DefaultTextboxSkin() 12 | Field chars:= New Stack 'Character input queue on last Poll() 13 | 14 | Field hit:Bool 'Activated for one frame when the control receives focus. 15 | 16 | Method New(x:Float, y:Float, w:Float, h:Float, Input:InputPointer) 17 | Super.New(x, y, w, h, Input) 18 | End Method 19 | 20 | Method New(x:Float, y:Float, w:Float, h:Float, skin:TextBoxSkin, Input:InputPointer) 21 | Super.New(x, y, w, h, Input) 22 | Self.skin = skin 23 | End Method 24 | 25 | 26 | Method Poll:Void(xOffset:Float = 0, yOffset:Float = 0) 27 | Super.Poll(xOffset, yOffset) 28 | 29 | Self.hit = False 30 | 31 | Local inWidget:Bool = UI.WithinRect(Input.x, Input.y, Self.x + xOffset, Self.y + yOffset, Self.w, Self.h) 32 | 33 | If Input.Hit And inWidget Then 34 | If HasFocus = False Then GotFocus() 35 | HasFocus = True 36 | ElseIf Input.Hit And inWidget = False Then 37 | If HasFocus = True Then LostFocus() 38 | HasFocus = False 39 | ElseIf KeyHit(KEY_BACK) And HasFocus 40 | LostFocus() 41 | HasFocus = False 42 | End If 43 | 44 | 45 | If HasFocus 46 | 'Capture input events. First, clear the char queue 47 | chars.Clear() 48 | 49 | 'Repopulate the char queue. 50 | Local char:Int 51 | Repeat 52 | char = GetChar() 53 | chars.Push(char) 54 | Until char = 0 55 | 56 | 'Now, process input events. 57 | For Local c:Int = EachIn chars 58 | If c > 16 And c < 256 And c <> 8 Then 59 | Text += String.FromChar(c) 60 | ElseIf c = 8 'Backspace 61 | Text = Text[0 .. - 1] 62 | End If 63 | If c > 0 Then KeyPress(c) 64 | Next 65 | 66 | End If 67 | End Method 68 | 69 | Method KeyPress:Void(char:Int) 70 | 'Put key press event stuff here when extending the class. 71 | End Method 72 | 73 | Method GotFocus:Void() 74 | 'Put specific stuff here when extending the class 75 | 76 | #If TARGET="android" or TARGET="ios" 77 | EnableKeyboard() 78 | #End 79 | 80 | Self.hit = True 81 | End Method 82 | 83 | Method LostFocus:Void() 84 | 'Put specific stuff here when extending the class 85 | 86 | #If TARGET="android" or TARGET="ios" 87 | DisableKeyboard() 88 | #End 89 | End Method 90 | 91 | Method Render:Void(xOffset:Float = 0, yOffset:Float = 0) 92 | skin.Render(Self, xOffset, yOffset) 93 | End Method 94 | 95 | 96 | 'Summary: Spawns a new TextBox for the unpacker. 97 | Method Spawn:Widget(json:String) 98 | 'Override this method to allow spawning of derived types. 99 | Local out:= New TextBox() 100 | out.UnpackJSON(json) 101 | Return out 102 | End Method 103 | End Class 104 | 105 | Interface TextBoxSkin 106 | Method Render:Void(caller:TextBox, x:Float = 0, y:Float = 0) 107 | End Interface 108 | 109 | Class DefaultTextboxSkin Implements TextBoxSkin 110 | Method Render:Void(caller:TextBox, x:Float = 0, y:Float = 0) 111 | 'Draw bg 112 | If caller.HasFocus Then 113 | SetAlpha(0.1) 114 | DrawRect(caller.x + x, caller.y + y, caller.w, caller.h) 115 | SetAlpha(1) 116 | End If 117 | 118 | 'Draw text 119 | DrawText(caller.Text, caller.x + x + 2, caller.y + y + 2) 120 | 121 | 'Draw box 122 | DrawLine(caller.x + x, caller.y + y, caller.x + x, caller.y + y + caller.h) 123 | DrawLine(caller.x + x, caller.y + y, caller.x + x + caller.w, caller.y + y) 124 | DrawLine(caller.x + x + caller.w + y, caller.y + caller.h, caller.x + x, caller.y + y + caller.h) 125 | DrawLine(caller.x + x + caller.w + y, caller.y + caller.h, caller.x + x + caller.w, caller.y + y) 126 | End Method 127 | End Class -------------------------------------------------------------------------------- /SimpleUI/ui.monkey: -------------------------------------------------------------------------------- 1 | Import mojo 2 | Import brl.json 3 | 4 | 'Summary: Generic namespace for various SimpleUI functions. 5 | Class UI Final 6 | 'Summary: Returns True if (px, py) are within rect (x,y,w,h). 7 | Function WithinRect:Bool(px:Float, py:Float, x:Float, y:Float, w:Float, h:Float) 8 | If px >= x And px <= x+w And py >= y And py < y+h Then Return true Else Return False 9 | End Function 10 | 11 | 'Summary: Sets a scissor which respects the current matrix's scale and translation values. 12 | Function SetXformedScissor:Void(x:Float, y:Float, w:Float, h:Float) 13 | Local matrix:Float[] = GetMatrix() 14 | SetScissor(x * matrix[0] + matrix[4], y * matrix[3] + matrix[5], w * matrix[0], h * matrix[3]) 15 | End Function 16 | 17 | Global scissorStack:= New Float[4 * 32], scissorSp 18 | 'Summary: Saves the current Scissor to the stack for later restoration. 19 | Function PushScissor:Void() 20 | Local sp:Int = scissorSp 21 | Local m:Float[] = GetScissor() 22 | scissorStack[sp + 0] = m[0] 23 | scissorStack[sp + 1] = m[1] 24 | scissorStack[sp + 2] = m[2] 25 | scissorStack[sp + 3] = m[3] 26 | scissorSp = sp + 4 27 | End Function 28 | 'Summary: Saves an arbitrary Scissor to the stack for later restoration. 29 | Function PushScissor:Void(scissor:Float[]) 30 | Local sp:Int = scissorSp 31 | Local m:Float[] = scissor 32 | scissorStack[sp + 0] = m[0] 33 | scissorStack[sp + 1] = m[1] 34 | scissorStack[sp + 2] = m[2] 35 | scissorStack[sp + 3] = m[3] 36 | scissorSp = sp + 4 37 | End Function 38 | 39 | 'Summary: Restores the last saved scissor from the stack. 40 | Function PopScissor:Void() 41 | Local sp = scissorSp - 4 42 | SetScissor(scissorStack[sp + 0], scissorStack[sp + 1], scissorStack[sp + 2], scissorStack[sp + 3]) 43 | scissorSp = sp 44 | End Function 45 | 46 | Function GetLastPushedScissor:Float[] (logToDebug:Bool = False) 47 | Local out:Float[4] 48 | Local sp = scissorSp - 4 49 | out[0] = scissorStack[sp + 0] 50 | out[1] = scissorStack[sp + 1] 51 | out[2] = scissorStack[sp + 2] 52 | out[3] = scissorStack[sp + 3] 53 | 54 | If logToDebug Then DebugLog("(" + out[0] + "," + out[1] + "," + out[2] + "," + out[3] + ") Stack position " + int(sp / 4)) 55 | Return out 56 | End Function 57 | 58 | 'Summary: Returns the distance between two points, squared 59 | Function DistSq:Float(x:Float, y:Float, x2:Float, y2:Float) 60 | Return (x2 - x) * (x2 - x) + (y2 - y) * (y2 - y) 61 | End Function 62 | 63 | 'Summary: Returns a value between startValue and endValue by precentage 0-1. 64 | Function Lerp:Float(startValue:Float, endValue:Float, percent:Float) 65 | Return startValue + (endValue - startValue) * percent 66 | End 67 | 68 | 'Summary: Returns a value between 0-1 based on testValue. 69 | Function Normalize:Float(testValue:Float, startValue:Float, endValue:Float, clamp:Bool = True) 70 | If clamp Then Return Clamp( (testValue - startValue) / (endValue - startValue), 0.0, 1.0) 71 | Return (testValue - startValue) / (endValue - startValue) 72 | End Function 73 | 74 | 'Summary: Generates a unique ID for widgets. 75 | Function GenerateID:Int(salt:Int) 76 | 'To generate a unique ID, we must find a number that's unlikely to be repeated no matter how many widgets we add here. 77 | 'Doing this in int32 space is difficult, so we comprimise by attempting to fill most of the values in the upper limit 78 | 'of this space. 14 bits of data (not counting the sign bit) are allocated high and assigned to a random number, then 79 | 'masked against the current stack size. This should provide unique values for at least 100,000 widgets, while giving 80 | 'only a 1-in-16382 chance of an auto ID collision if the stack size shrinks and stack length is repeated again. 81 | 82 | 'All negative numbers (except for -1), and numbers from 0-$1FFFF are all valid ID's that can be manually assigned 83 | 'with no chance of a collision by the auto-assignment. 84 | 85 | Return (Rnd(1, $3FFF) Shl 17) | salt 'Auto-assign a unique widget ID. Should support >100k widgets. 86 | End Function 87 | End Class 88 | 89 | -------------------------------------------------------------------------------- /SimpleUI/widgetManager.monkey: -------------------------------------------------------------------------------- 1 | 'This class is somewhat like a server, which allows you to attach UI widgets to it. The convenience of this class 2 | 'comes from the fact that most UI objects inherit Widget and can therefore be polled and rendered by the manager. 3 | 'In the future, this class may allow dispatch groups to make it easier to make the behavior of one group of widgets 4 | 'dependant on another action, such as another widget's state or a different InputPointer, but as of this writing, 5 | 'there only exists the ability to render/poll all widgets inside a WidgetManager at once. 6 | ' -Nobuyuki (nobu@subsoap.com) 19 August 2013 7 | 8 | Import InputPointers 9 | Import ui 10 | Import widget 11 | 12 | Class WidgetManager 13 | Field Enabled:Bool = True 'DIFFERENT than Widget's Enabled property! Setting this True stops the manager from polling or rendering anything. 14 | Field Input:InputPointer 15 | Field Widgets:= New Stack 16 | Field WidgetsByID:= New IntMap 'For accessing widgets by ID 17 | Field WidgetsByName:= New StringMap 18 | 19 | Method New(input:InputPointer) 20 | Self.Input = input 21 | End Method 22 | 23 | ' Method New(widgets:Stack, input:InputPointer = Null) 24 | ' Self.Input = input 25 | ' Self.Widgets = widgets 26 | ' End Method 27 | 28 | Method New(widgets:Widget[], input:InputPointer = Null) 29 | Self.Input = input 30 | 31 | For Local i:Int = 0 Until widgets.Length 32 | Attach(widgets[i]) 33 | Next 34 | End Method 35 | 36 | 'Summary: Attaches a widget to this manager for control. Overrides the widget's input if specified. 37 | Method Attach:Int(widget:Widget, id:Int = -1, overrideInput:Bool = True) 38 | If overrideInput And (Input <> Null) Then widget.Input = Input 39 | 40 | If id = -1 Then id = widget.id 'Set ID to whatever the widget's ID is. 41 | If id = -1 Then 'If the widget doesn't have an ID, make one. 42 | id = UI.GenerateID(Widgets.Length()) 43 | widget.id = id 44 | End If 45 | 46 | Widgets.Push(widget) 47 | WidgetsByID.Add(id, widget) 48 | If widget.name <> "" Then 49 | Local ok:Bool = WidgetsByName.Add(widget.name, widget) 50 | If Not ok Then 51 | Print("WidgetManager: Warning, widget '" + widget.name + "' already exists.") 52 | Print("WidgetManager: The previous reference has been overwritten.") 53 | End If 54 | End If 55 | 56 | Return id 57 | End Method 58 | 59 | Method DetachAll:Void() 60 | WidgetsByName.Clear() 61 | WidgetsByID.Clear() 62 | Widgets.Clear() 63 | End Method 64 | 65 | 66 | 'Summary: Polls all widgets attached to this manager. 67 | Method PollAll:Void(xOffset:Float = 0, yOffset:Float = 0) 68 | If Enabled = False Then Return 69 | 70 | For Local o:Widget = EachIn Widgets 71 | o.Poll(xOffset, yOffset) 72 | Next 73 | End Method 74 | 75 | 'Summary: Renders all widgets attached to this manager. 76 | Method RenderAll:Void(xOffset:Float = 0, yOffset:Float = 0) 77 | If Enabled = False Then Return 78 | 79 | For Local o:Widget = EachIn Widgets 80 | o.Render(xOffset, yOffset) 81 | Next 82 | End Method 83 | 84 | Method Remove:Void(id:Int) 85 | Local w:Widget = WidgetsByID.Get(id) 86 | If w <> Null 87 | WidgetsByID.Remove(id) 88 | Widgets.RemoveEach(w) 89 | WidgetsByName.Remove(w.name) 90 | Else 91 | Print("WidgetManager: Warning, widget " + id + " not found.") 92 | End If 93 | End Method 94 | 95 | Method Remove:Void(p:Widget) 96 | WidgetsByID.Remove(p.id) 97 | Widgets.RemoveEach(p) 98 | WidgetsByName.Remove(p.name) 99 | End Method 100 | 101 | Method Remove:Void(name:String) 102 | Local w:Widget = WidgetsByName.Get(name) 103 | If w <> Null 104 | WidgetsByName.Remove(name) 105 | WidgetsByID.Remove(w.id) 106 | Widgets.RemoveEach(w) 107 | Else 108 | Print("WidgetManager: Warning, widget '" + name + "' not found.") 109 | 110 | End If 111 | End Method 112 | 113 | Method RemoveAll:Void() 114 | Widgets.Clear() 115 | WidgetsByID.Clear() 116 | WidgetsByName.Clear() 117 | End Method 118 | End Class -------------------------------------------------------------------------------- /formdesigner.monkey: -------------------------------------------------------------------------------- 1 | 'This imports the most basic things needed to get started. 2 | Import SimpleUI.common 3 | 'The following are not included in the common init, add them as necessary. 4 | Import SimpleUI.widgetManager 5 | Import SimpleUI.unpacker 6 | 7 | Import os 8 | 9 | Function Main:Int() 10 | New Game() 11 | Return 0 12 | End Function 13 | 14 | Class Game Extends App 15 | Field Cursor:= New ScaleAwarePointer() 16 | 17 | Field btns:WidgetManager 18 | Field newDoc:PushButton 19 | Field load:PushButton 20 | Field save:PushButton 21 | 22 | Field originX, originY 23 | Field destX, destY 24 | 25 | Field rects:= New Stack 26 | 27 | Field testLoader:WidgetManager 28 | 29 | Method OnCreate:Int() 30 | SetUpdateRate 60 31 | 32 | 33 | newDoc = New PushButton(4, 4, 64, 24, Cursor) 34 | newDoc.Text = "New" 35 | load = New PushButton(76, 4, 64, 24, Cursor) 36 | load.Text = "Load" 37 | save = New PushButton(148, 4, 64, 24, Cursor) 38 | save.Text = "Save" 39 | 40 | btns = New WidgetManager(Cursor) 41 | btns.Attach(newDoc) 42 | btns.Attach(load) 43 | btns.Attach(save) 44 | 45 | Local get:Int[] = GetDate() 46 | Seed = get[6] + get[5] + get[4] * (Millisecs() +1) 47 | Print Seed 48 | 49 | Unpacker.Init() 50 | testLoader = New WidgetManager(Cursor) 51 | End Method 52 | 53 | Method OnUpdate:Int() 54 | Cursor.Poll() 55 | 56 | If Cursor.Hit Then 57 | originX = Cursor.x 58 | originY = Cursor.y 59 | destX = Cursor.x + 2 60 | destY = Cursor.y + 2 61 | ElseIf Cursor.Down 62 | destX = Cursor.x 63 | destY = Cursor.y 64 | 65 | ElseIf Cursor.Up 66 | Local w:Float = destX - originX 67 | Local h:Float = destY - originY 68 | If w > 2 And h > 2 Then rects.Push(New Rect(originX, originY, w, h, GenName(Rnd(2, 5)))) 69 | End If 70 | 71 | btns.PollAll() 72 | 73 | 74 | If newDoc.hit Then 75 | rects.Clear() 76 | testLoader = New WidgetManager(Cursor) 77 | End If 78 | If save.hit Then 79 | SaveState(PackAll()) 80 | Print "Saved." 81 | End If 82 | 83 | If load.hit 84 | rects.Clear() 85 | testLoader = Unpacker.UnpackForm(LoadState(), Cursor) 86 | Print "Loaded." 87 | End If 88 | 89 | testLoader.PollAll() 90 | End Method 91 | 92 | Method OnRender:Int() 93 | Cls() 94 | 95 | For Local i:Int = 0 Until rects.Length 96 | Local o:Rect = rects.Get(i) 97 | SetColor(128, 0, 0) 98 | o.Draw() 99 | SetColor(255, 255, 255) 100 | DrawText(i, o.x, o.y) 101 | Next 102 | 103 | If Cursor.Down 104 | Local w:Float = destX - originX 105 | Local h:Float = destY - originY 106 | SetAlpha(0.5) 107 | DrawRect(originX, originY, w, h) 108 | SetAlpha(1) 109 | End If 110 | 111 | btns.RenderAll() 112 | testLoader.RenderAll() 113 | 114 | End Method 115 | 116 | Method GenName:String(syllables:Int) 117 | Local vowel:String[] =["a", "e", "i", "o", "u"] 118 | Local cons:String[] =["b", "d", "g", "k", "t", "t", "s", "s", "r", "r", "n", "j", "h", "p"] 119 | 120 | Local size:Int = syllables * 2 121 | If Int(Rnd(5)) = 0 Then size += 1 'end consonant 122 | 123 | Local parts:String[size] 124 | 125 | For Local i:Int = 0 Until parts.Length 126 | If i & 1 = 0 Then 'Consonant 127 | parts[i] = cons[Rnd(cons.Length)] 128 | Else 129 | Select Int(Rnd(5)) 130 | Case 0 'Double vowel 131 | parts[i] = vowel[Rnd(vowel.Length)] + vowel[Rnd(vowel.Length)] 132 | Default 'Single vowel 133 | parts[i] = vowel[Rnd(vowel.Length)] 134 | End Select 135 | End If 136 | Next 137 | 138 | Local out:String 139 | Return out.Join(parts) 140 | End Method 141 | 142 | Method PackAll:String() 143 | Local o:JsonObject = New JsonObject() 144 | Local wStack:= New Stack 'Stack of widgets to be converted to jsonArray later 145 | 146 | For Local i:Int = 0 Until rects.Length 147 | wStack.Push(Pack(rects.Get(i), i)) 148 | Next 149 | o.Set("Widgets", New JsonArray(wStack.ToArray())) 150 | 151 | Print o.ToJson() 152 | Return o.ToJson() 153 | End Method 154 | Method Pack:JsonObject(w:Rect, salt:Int) 155 | Local o:= New JsonObject() 156 | o.SetInt("id", UI.GenerateID(salt)) 157 | o.SetString("Text", w.name) 158 | o.SetString("type", "pushbutton") 159 | 160 | o.SetFloat("x", w.x) 161 | o.SetFloat("y", w.y) 162 | o.SetFloat("w", w.w) 163 | o.SetFloat("h", w.h) 164 | 165 | o.SetBool("Enabled", True) 166 | o.SetBool("Visible", True) 167 | 168 | Return o 169 | End Method 170 | End Class 171 | 172 | Class Rect 173 | Field x:Float, y:Float, w:Float, h:Float 174 | Field name:String 175 | 176 | Method New(X:Float, Y:Float, W:Float, H:Float, Name:String = "") 177 | x = X; y = Y; w = W; h = H 178 | name = Name 179 | End Method 180 | 181 | Method Draw:Void() 182 | DrawRect(x, y, w, h) 183 | SetColor(255, 255, 0) 184 | DrawText(name, x + w / 2, y + h / 2, 0.5, 0.5) 185 | SetColor(255, 255, 255) 186 | End Method 187 | End Class 188 | -------------------------------------------------------------------------------- /example.monkey: -------------------------------------------------------------------------------- 1 | Strict 2 | 3 | Import mojo 4 | 5 | 'This imports the most basic things needed to get started. 6 | Import SimpleUI.common 7 | 'The following are not included in the common init, add them as necessary. 8 | Import SimpleUI.widgetManager 9 | Import SimpleUI.Scrollers 10 | Import SimpleUI.panel 11 | 12 | Function Main:Int() 13 | New Game() 14 | Return 0 15 | End Function 16 | 17 | Class Game Extends App 18 | Field status:String = "Better example coming soon..." 19 | 20 | 'This is the cursor we use for the example. It -should- support scaled screens... 21 | Field Cursor:= New ScaledTouchPointer() 22 | 'This is a widget manager. For lazy people, you can poll/render batches of widgets at once! 23 | Field widgets:WidgetManager 24 | 'Some pushbuttons. You'll want to extend these to provide your own functionality. 25 | Field button:PushButton[3] 26 | 'A scroller. 27 | Field scroll:EndlessScroller 28 | 'A 2d panel. 29 | Field myPanel:TestPanel 30 | 31 | Method OnCreate:Int() 32 | SetUpdateRate 60 33 | 34 | 'Set up the widget manager to utilize our global InputPointer. 35 | widgets = New WidgetManager(Cursor) 36 | 37 | 'Initialize the buttons. 38 | For Local i:Int = 0 Until 3 39 | button[i] = New PushButton(16, 32 + i * 48, 96, 32, Cursor) 40 | button[i].Text = "Button " + i 41 | 42 | widgets.Attach(button[i]) 43 | Next 44 | 45 | 'Set up the scroller. 46 | scroll = New EndlessScroller(320, 32, 256, 320, 10, Cursor, 48) 47 | scroll.Items = scroll.Items.Resize(10) 48 | For Local i:Int = 0 Until 10 49 | Local c:= New ExampleCell() 50 | c.w = 256 51 | c.h = 48 52 | c.text = "Cell " + i 53 | c.r = Rnd(255) 54 | c.g = Rnd(255) 55 | c.b = Rnd(255) 56 | 57 | scroll.Items[i] = c 58 | Next 59 | widgets.Attach(scroll) 60 | 61 | 'Set up the panel, add a few test buttons to it. 62 | myPanel = New TestPanel(32, 240, 256, 224, 640, 480, Cursor) 63 | For Local i:Int = 0 Until 3 64 | Local panelButton:= New PushButton(144, 72 + i * 48, 96, 32, Cursor) 65 | panelButton.Text = "Button " + (i + 3) 66 | 67 | myPanel.Attach(panelButton) 68 | Next 69 | widgets.Attach(myPanel) 70 | 71 | Return 0 72 | End Method 73 | 74 | Method OnUpdate:Int() 75 | If KeyHit(KEY_ESCAPE) or KeyHit(KEY_CLOSE) or KeyHit(KEY_BACK) Then Error("") 76 | 77 | 'In order for anything to detect, we must poll the InputPointer at the beginning of each frame. 78 | Cursor.Poll() 79 | 80 | 'Tell our widget manager "Okay, let's poll our widgets for input." 81 | widgets.PollAll() 82 | 83 | 'Now let's check that input. 84 | For Local i:Int = 0 Until 3 85 | If button[i].hit 86 | status = "Button " + i + " hit." 87 | End If 88 | Next 89 | 90 | 'Here's another way to check input, in WidgetManagers and Panels 91 | For Local o:Widget = EachIn myPanel.Widgets 92 | Local test:PushButton = PushButton(o) 93 | If test = Null Then Continue 94 | If test.hit 95 | status = test.Text + " hit." 96 | End If 97 | Next 98 | 99 | Return 0 100 | End Method 101 | 102 | Method OnRender:Int() 103 | Cls(0, 16, 64) 104 | 105 | widgets.RenderAll() 106 | 107 | SetAlpha(0.4) 108 | If myPanel.childInput.inPanel Then SetColor(0, 255, 0) 109 | DrawCircle(Cursor.x, Cursor.y, 8) 110 | SetColor(255, 255, 255) 111 | SetAlpha(1) 112 | 113 | Local m:Float[] = GetMatrix() 114 | DrawText(status, 0, 0) 115 | 116 | Return 0 117 | End Method 118 | End Class 119 | 120 | 'Summary: Class providing a SimpleUI InputPointer for an AutoScaled touchscreen. No multitouch. 121 | Class ScaledTouchPointer Extends MousePointer 122 | Method x:Float() Property 123 | Return dTouchX() 124 | End Method 125 | Method y:Float() Property 126 | Return dTouchY() 127 | End Method 128 | 129 | 'Derived multitouch positions 130 | Function dTouchX:Int(index:Int = 0) 131 | Local m:Float[] = GetMatrix() 132 | Return TouchX(index) / m[0] - (m[4] / m[0]) 133 | End Function 134 | 135 | Function dTouchY:Int(index:Int=0) 136 | Local m:Float[] = GetMatrix() 137 | Return TouchY(index) / m[3] - (m[5] / m[3]) 138 | End Function 139 | End Class 140 | 141 | 142 | Class TestPanel Extends ScrollablePanel 143 | Method New(x:Int, y:Int, w:Int, h:Int, cw:Float, ch:Float, Input:InputPointer) 144 | Super.New(x, y, w, h, cw, ch, Input) 145 | End Method 146 | 147 | 148 | Method Render:Void(xOffset:Float = 0, yOffset:Float = 0) 149 | Super.Render(xOffset, yOffset) 150 | 151 | SetAlpha(0.25) 152 | If Scrolling Then DrawRect(x, y, 32, 32) 153 | SetAlpha(1) 154 | DrawText(cx, x, y) 155 | DrawText(cy, x, y + 16) 156 | 157 | 'draw outline box 158 | DrawLine(x, y, x + w + 8, y) 159 | DrawLine(x, y, x, y + h + 8) 160 | DrawLine(x + w + 8, y, x + w + 8, y + h + 8) 161 | DrawLine(x, y + h + 8, x + w + 8, y + h + 8) 162 | 163 | 'draw scrollbars 164 | SetAlpha(0.25) 165 | DrawRect(x, y + h, w, 8) 166 | DrawRect(x + w, y, 8, h) 167 | DrawCircle(x + PercentX * w, y + h + 4, 4) 168 | DrawCircle(x + w + 4, y + PercentY * h, 4) 169 | SetAlpha(1) 170 | End Method 171 | 172 | 'Summary: Renders the content of the panel after setting the proper translation/scissor. Override this and call Super to render widgets. 173 | Method RenderContent:Void(xOffset:Float = 0, yOffset:Float = 0) 174 | 'Note: The origin 0,0 is considered x,y of parent panel. 175 | For Local yy:Int = 0 Until ch / 32 176 | For Local xx:Int = 0 Until cw / 32 177 | Local odd:Int = xx + (yy & 1) & 1 178 | If odd = 1 Then SetColor(128, 128, 64) Else SetColor(128, 128, 255) 179 | If xx = 0 or xx = (cw / 32) - 1 Then SetColor(128, 0, 0) 180 | If yy = 0 or yy = (ch / 32) - 1 Then SetColor(128, 0, 0) 181 | DrawRect(xx * 32, yy * 32, 32, 32) 182 | SetColor(255, 255, 255) 183 | Next 184 | Next 185 | 186 | Super.RenderContent(xOffset, yOffset) 187 | End Method 188 | End Class -------------------------------------------------------------------------------- /SimpleUI/InputPointers.monkey: -------------------------------------------------------------------------------- 1 | ' This file is for dealing with the types of input devices available across platforms. 2 | ' Some types of interfaces may need to be custom-coded for a solution, for example, 3 | ' Using multitouch, or adjusting for surface scaling. In those cases, SimpleUI 4 | ' should not attempt to poll directly from a single input source. 5 | ' Therefore, the InputPointer interface is used to poll for input. One example 6 | ' class implementing InputPointer is provided, but you should write your own 7 | ' for your game to deal with how your game handles input. 8 | ' -Nobuyuki (nobu@subsoap.com) 25 Jun 2012 9 | 10 | Import mojo 11 | 12 | Interface InputPointer 'This is a generic wrapper for our human interface device. 13 | Method x:Float() Property 14 | Method y:Float() Property 15 | 16 | Method Poll:Void() 'Polls the input state 17 | Method Hit:Bool() Property '1st time hit 18 | Method Down:Bool() Property 'Holding down 19 | Method Up:Bool() Property 'Unclicked / Lifted up 20 | End Interface 21 | 22 | 23 | ' This class is provided as an example. You may extend it to override individual functions, 24 | ' or simply write your own from scratch. 25 | Class MousePointer implements InputPointer 26 | Private 27 | Field Holding:Bool 'Used to implement MouseUp event 28 | Field _hit:Bool, _down:Bool, _up:Bool 29 | Public 30 | Method x:Float() Property 31 | Return MouseX() 32 | End Method 33 | Method y:Float() Property 34 | Return MouseY() 35 | End Method 36 | 37 | Method Poll:Void() 'This method sets all of the properties to their internal values. 38 | 'The reason all of our input states are properties is so they can be defined in the abstract interface. 39 | 'When we call this method, we're telling all of its internal values to be set correctly. 40 | 'This method should be called each frame before checking any value. 41 | 42 | If MouseHit(MOUSE_LEFT) > 0 Then 43 | _hit = True 44 | Else 45 | _hit = False 46 | End If 47 | 48 | If MouseDown(MOUSE_LEFT) Then 49 | Self.Holding = True 50 | _down = True 51 | Else 'Not holding MouseDown 52 | _down = False 53 | If Self.Holding = True Then 'Was holding last frame. Do MouseUp 54 | Self.Holding = False 55 | _up = True 56 | Else ; _up = False 57 | End If 58 | End If 59 | 60 | End Method 61 | 62 | 'No Set methods; These properties are read-only 63 | Method Hit:Bool() Property 64 | ' If MouseHit(MOUSE_LEFT) > 0 then 65 | ' Self.Holding = True 66 | ' Return True 67 | ' End If 68 | ' Self.Holding = False 69 | ' Return False 70 | Return _hit 71 | End Method 72 | Method Down:Bool() Property 73 | ' If MouseDown(MOUSE_LEFT) 74 | ' Self.Holding = True 75 | ' Return True 76 | ' End If 77 | ' Self.Holding = False 78 | ' Return False 79 | Return _down 80 | End Method 81 | Method Up:Bool() Property 82 | ' If Self.Holding And Not MouseDown(MOUSE_LEFT) Then 'The mouse was released 83 | ' Self.Holding = False 84 | ' Print "Up" 85 | ' Return True 86 | ' End If 87 | ' Return False 88 | Return _up 89 | End Method 90 | 91 | End Class 92 | 93 | 'Summary: Provides a mouse pointer which is aware of the current global scaling matrix. 94 | Class ScaleAwarePointer Extends MousePointer 95 | Method x:Float() Property 96 | Local m:Float[] = GetMatrix() 97 | Return (MouseX() -m[4]) / m[0] 98 | End Method 99 | Method y:Float() Property 100 | Local m:Float[] = GetMatrix() 101 | Return (MouseY() -m[5]) / m[3] 102 | End Method 103 | End Class 104 | 105 | 106 | 'Summary: For multitouch-requiring components. Extend this if you need to. (Work in progress 21 Dec 2013) 107 | Class MultiTouchPointer Implements InputPointer 108 | Private 109 | Field Holding:Bool[16] 'Used to implement MouseUp event 110 | Field _hit:Bool[16], _down:Bool[16], _up:Bool[16] 111 | Public 112 | 113 | 'Single touch methods (InputPointer compatible) 114 | Method x:Float() Property 115 | Return TouchX() 116 | End Method 117 | Method y:Float() Property 118 | Return TouchY() 119 | End Method 120 | 121 | 'Multitouch methods. 122 | Method x:Float(finger:Int) 123 | Return TouchX(finger) 124 | End Method 125 | Method y:Float(finger:Int) 126 | Return TouchY(finger) 127 | End Method 128 | 129 | Method Poll:Void() 130 | For Local f:Int = 0 Until 16 131 | If TouchHit(f) > 0 Then 132 | _hit[f] = True 133 | Else 134 | _hit[f] = False 135 | End If 136 | 137 | If TouchDown(f) Then 138 | Self.Holding[f] = True 139 | _down[f] = True 140 | Else 'Not holding Down 141 | _down[f] = False 142 | If Self.Holding[f] = True Then 'Was holding last frame. Do Up 143 | Self.Holding[f] = False 144 | _up[f] = True 145 | Else; _up[f] = False 146 | End If 147 | End If 148 | 149 | Next 150 | 151 | End Method 152 | 153 | Method Hit:Bool() 154 | Return _hit[0] 155 | End Method 156 | Method Down:Bool() 157 | Return _down[0] 158 | End Method 159 | Method Up:Bool() 160 | Return _up[0] 161 | End Method 162 | 163 | 'Multitouch methods 164 | Method Hit:Bool(finger:Int) 165 | Return _hit[finger] 166 | End Method 167 | Method Down:Bool(finger:Int) 168 | Return _down[finger] 169 | End Method 170 | Method Up:Bool(finger:Int) 171 | Return _up[finger] 172 | End Method 173 | 174 | 'Summary: Returns how many fingers are being held down. 175 | Method Fingers:Int() 176 | Local result:Int 177 | For Local i:Int = 0 Until 16 178 | If _down[i] Then result += 1 179 | Next 180 | 181 | Return result 182 | End Method 183 | End Class 184 | 185 | 'Summary: A multitouch pointer which is aware of the matrix scale. 186 | Class ScaleAwareMultiTouchPointer Extends MultiTouchPointer 187 | Method x:Float() Property 188 | Local m:Float[] = GetMatrix() 189 | Return (TouchX() -m[4]) / m[0] 190 | End Method 191 | Method y:Float() Property 192 | Local m:Float[] = GetMatrix() 193 | Return (TouchY() -m[5]) / m[3] 194 | End Method 195 | 196 | Method x:Float(finger:Int) 197 | Local m:Float[] = GetMatrix() 198 | Return (TouchX(finger) - m[4]) / m[0] 199 | End Method 200 | Method y:Float(finger:Int) 201 | Local m:Float[] = GetMatrix() 202 | Return (TouchY(finger) - m[5]) / m[3] 203 | End Method 204 | End Class -------------------------------------------------------------------------------- /SimpleUI/colorPicker.monkey: -------------------------------------------------------------------------------- 1 | Import common 2 | 3 | Class CircularPicker Extends PushButton 4 | Protected 5 | Const SQ22:Float = 0.70710678118654752440084436210485 'Sqrt(2)/2 6 | 7 | Const INDICATOR_GFX:String = "0011110001222210120000211200002112000021120000210122221000111100" 8 | Field indicator_img:Image = CreateImage(8, 8,, Image.MidHandle) 9 | 10 | Field hue:Float 'Hue color 11 | Field indicatorX:Float, indicatorY:Float 'Saturation, Luminosity 12 | 13 | Public 14 | 15 | Field Dragging:Int = 0 'drag flag: 0=None; 1=Hue; 2=Sat/Lum 16 | Const DRAG_HUE = 1 17 | Const DRAG_XY = 2 18 | Const DRAG_NONE = 0 19 | 20 | 21 | Method New(x:Float, y:Float, size:Float, Input:InputPointer) 22 | Super.New(x + size / 2, y + size / 2, size, size, Input) 23 | 24 | GenIndicatorImage() 25 | End Method 26 | 27 | Method GenIndicatorImage:Void() 28 | 'Generate indicator image. 29 | Local px:= New IntStack 30 | For Local i:Int = 0 Until 64 31 | Select INDICATOR_GFX[i] 32 | Case 48 'Trans 33 | px.Push(0) 34 | 35 | Case 49 'Black 36 | px.Push($FF000000) 37 | 38 | Case 50 'White 39 | px.Push($FFFFFFFF) 40 | End Select 41 | 42 | Next 43 | indicator_img.WritePixels(px.ToArray(), 0, 0, 8, 8, 0, 8) 44 | End Method 45 | 46 | Method Poll:Void(xOffset:Float = 0, yOffset:Float = 0) 47 | Super.Poll(xOffset, yOffset) 48 | 49 | 50 | Local inR:Float = Min(w, h) * 0.375 51 | Local sqpos:Float = inR * SQ22 'offset 52 | Local sqw:Float = inR * 2 * SQ22 53 | 54 | 55 | If Input.Hit() And UI.WithinRect(Input.x, Input.y, x - w / 2.0, y - h / 2.0, w, h) 56 | If Not UI.WithinRect(Input.x, Input.y, x - sqpos, y - sqpos, sqw, sqw) 57 | Dragging = DRAG_HUE 58 | Else 59 | Dragging = DRAG_XY 60 | End If 61 | End If 62 | 63 | 64 | If Input.Down And Dragging = DRAG_HUE 65 | hue = (Int(ATan2(Input.y - y, Input.x - x) + 360) Mod 360) / 360.0 66 | ElseIf Input.Down And Dragging = DRAG_XY 67 | indicatorX = UI.Normalize(Input.x, x - sqpos, x - sqpos + sqw) 68 | indicatorY = UI.Normalize(Input.y, y - sqpos, y - sqpos + sqw) 69 | End If 70 | 71 | If Input.Up() 72 | Dragging = 0 73 | End If 74 | End Method 75 | 76 | Method Render:Void(xOffset:Float = 0, yOffset:Float = 0) 77 | If Not Visible Then Return 78 | Local rgb:Int[3] 79 | Local outR:Float = w * 0.5 80 | Local inR:Float = w * 0.375 81 | Local midR:Float = w * 0.4375 82 | 83 | 'Draw shadow. 84 | SetAlpha(0.25) 85 | SetColor(0, 0, 0) 86 | DrawCircle(x, y + 5.5, outR) 87 | SetAlpha(1) 88 | 89 | 'Draw the wheel. 90 | For Local i:Float = 0 Until 360 Step 0.5 91 | rgb = HSLtoRGB(float(i) / 360.0, 1, 0.5) 92 | SetColor(rgb[0], rgb[1], rgb[2]) 93 | DrawLine(x + Cos(i) * inR, y + Sin(i) * inR, x + Cos(i + 15) * outR, y + Sin(i + 15) * outR) 94 | SetColor(255, 255, 255) 95 | Next 96 | 'DrawText(Int(ATan2(MouseY() -x, MouseX() -y) + 360) Mod 360, 128, 128, 0.5, 0.5) 97 | SetColor(64, 64, 64) 98 | DrawArc(x + midR * Cos(hue * 360), y + midR * Sin(hue * 360), w * 0.0625, w * 0.0625,,, 27) 99 | SetColor(255, 255, 255) 100 | 101 | 'Draw the square. 102 | Local sqw:Float = inR * 2 * SQ22 103 | Local sqpos:Int = inR * SQ22 104 | For Local yy:Int = 0 Until sqw 105 | For Local xx:Int = 0 Until sqw 106 | Local gamut:Float = 1 - (xx / sqw) / 2 'range of luminance values to express approaches 0 as saturation reaches 1 107 | ' rgb = HSLtoRGB(0, float(xx) / sqw, 1.0 - float(yy ) / sqw) 108 | rgb = HSLtoRGB(hue, float(xx) / sqw, 1.0 - UI.Lerp(0, gamut, yy / sqw) - (1 - gamut)) 109 | SetColor(rgb[0], rgb[1], rgb[2]) 110 | DrawPoint(x + xx - sqpos, y + yy - sqpos) 111 | SetColor(255, 255, 255) 112 | Next 113 | Next 114 | 115 | 'draw outlines. 116 | SetColor(64, 64, 64) 117 | DrawRectOutline(x - sqpos, y - sqpos, sqw, sqw) 118 | DrawArc(x, y, inR, inR,,, 27) 119 | DrawArc(x, y, outR, outR,,, 27) 120 | SetColor(255, 255, 255) 121 | 122 | 123 | 'Draw indicator. 124 | DrawImage(indicator_img, Int(x - sqpos + indicatorX * sqw), Int(y - sqpos + indicatorY * sqw)) 125 | End Method 126 | 127 | 'Summary: Gets debug HSL values from the picker. 128 | Method GetHSL:Float[] () 129 | Local gamut:Float = 1 - indicatorX / 2 'range of luminance values to express approaches 0 as saturation reaches 1 130 | Local L:Float = 1.0 - UI.Lerp(0, gamut, indicatorY) - (1 - gamut) 131 | Return[hue, indicatorX, L] 132 | End Method 133 | 134 | 'Summary: Gets the picker's current RGB balues. 135 | Method GetRGB:Int[] () 136 | Local hsl:= GetHSL() 137 | Return HSLtoRGB(hsl[0], hsl[1], hsl[2]) 138 | End Method 139 | 140 | 'Summary: Sets the picker's current RGB balues. 141 | Method SetRGB:Void(r:Int, g:Int, b:Int) 142 | Local hsv:= RGBtoHSV(r, g, b) 143 | 144 | 'Fix some values before plugging them in. 145 | If hsv[0] = 1.0 Then hsv[0] = 0 'hsvtoRGB doesn't like hue values of 1.0. 146 | If hsv[0] <> hsv[0] Then hsv[0] = 0 'hsvtoRGB doesn't like hue values of undefined. 147 | 148 | 149 | hue = hsv[0] 150 | indicatorX = hsv[1] 151 | indicatorY = 1 - hsv[2] 152 | End Method 153 | 154 | 155 | 156 | 'Summary: Draws an arc. With the default arguments, this function can also draw elipse outlines. 157 | Function DrawArc:Void(x:Float, y:Float, xRad:Float, yRad:Float, aStart:Float = 0.0, aEnd:Float = 360.0, segments:Int = 8) 158 | Local x1:Float = x + ( Cos( aStart ) * xRad ) 159 | Local y1:Float = y - ( Sin( aStart ) * yRad ) 160 | Local x2:Float 161 | Local y2:Float 162 | Local div:Float = ( aEnd - aStart ) / segments 163 | For Local i:Int = 1 To segments 164 | x2 = x + ( Cos( aStart + ( i * div )) * xRad ) 165 | y2 = y - ( Sin( aStart + ( i * div )) * yRad ) 166 | DrawLine(x1, y1, x2, y2) 167 | x1 = x2 168 | y1 = y2 169 | Next 170 | End Function 171 | 172 | Function DrawRectOutline:Void(x:Float, y:Float, w:Float, h:Float) 173 | DrawLine(x, y, x + w, y) 174 | DrawLine(x,y,x,y+h) 175 | DrawLine(x,y+h,x+w,y+h) 176 | DrawLine(x+w,y,x+w,y+h) 177 | End Function 178 | 179 | 180 | Function Round:Int(a:Float) 181 | Return int(Floor(a + 0.5)) 182 | End Function 183 | 184 | ' colour conversions (hsl is range 0-1, return is RGB as a single int) 185 | ' Monkey conversion of http://www.geekymonkey.com/Programming/CSharp/RGB2HSL_HSL2RGB.htm 186 | ' shamelessly stolen and altered from the Diddy framework...... 187 | Function HSLtoRGB:Int[] (hue:Float, saturation:Float, luminance:Float) 188 | Local r:Float = luminance, g:Float = luminance, b:Float = luminance 189 | Local v:Float = 0 190 | If luminance <= 0.5 Then 191 | v = luminance * (1.0 + saturation) 192 | Else 193 | v = luminance + saturation - luminance * saturation 194 | End 195 | If v > 0 Then 196 | Local m:Float = luminance + luminance - v 197 | Local sv:Float = (v - m) / v 198 | hue *= 6 199 | Local sextant:Int = Int(hue) 200 | Local fract:Float = hue - sextant 201 | Local vsf:Float = v * sv * fract 202 | Local mid1:Float = m + vsf 203 | Local mid2:Float = v - vsf 204 | 205 | Select sextant 206 | Case 0 207 | r = v 208 | g = mid1 209 | b = m 210 | 211 | Case 1 212 | r = mid2 213 | g = v 214 | b = m 215 | 216 | Case 2 217 | r = m 218 | g = v 219 | b = mid1 220 | 221 | Case 3 222 | r = m 223 | g = mid2 224 | b = v 225 | 226 | Case 4 227 | r = mid1 228 | g = m 229 | b = v 230 | 231 | Case 5 232 | r = v 233 | g = m 234 | b = mid2 235 | End 236 | End 237 | 238 | Return[Int(r * 255), Int(g * 255), Int(b * 255)] 239 | End 240 | 241 | 'Adapted from https://www.cs.rit.edu/~ncs/color/t_convert.html 242 | ' h = [0,360], s = [0,1], v = [0,1] 243 | ' if s = 0, then h = -1 (undefined) 244 | 245 | Function RGBtoHSV:Float[] (r:Float, g:Float, b:Float) 246 | Local h:Float, s:Float, v:Float 247 | Local min:Float, max:Float, delta:Float 248 | 249 | r /= 255.0; g /= 255.0; b /= 255.0 250 | 251 | min = Min(Min(r, g), b) 252 | max = Max(Max(r, g), b) 253 | v = max ' v 254 | 255 | delta = max - min 256 | 257 | If max <> 0 258 | s = delta / max ' s 259 | Else 260 | ' r = g = b = 0 ' s = 0, v is undefined 261 | s = 0 262 | h = -1 263 | Return[h, s, v] 264 | End If 265 | 266 | If r = max 267 | h = ( g - b ) / delta ' between yellow & magenta 268 | ElseIf g = max 269 | h = 2 + ( b - r ) / delta ' between cyan & yellow 270 | Else 271 | h = 4 + ( r - g ) / delta ' between magenta & cyan 272 | End If 273 | 274 | 275 | h *= 60 ' degrees 276 | if h < 0 Then h += 360 277 | 278 | Return[h / 360.0, s, v] 279 | End Function 280 | 281 | End Class -------------------------------------------------------------------------------- /SimpleUI/panel.monkey: -------------------------------------------------------------------------------- 1 | Import ui 2 | Import widget 3 | Import InputPointers 4 | 5 | 'Summary: Provides a 2d panel which can be dragged in any direction. 6 | Class ScrollablePanel Extends Widget 7 | 'Base stuff 8 | Field cx:Float, cy:Float, cw:Float, ch:Float 'Content dimenions 9 | Field z:Float = 1 'Current zoom level 10 | 11 | 'Scroll elements 12 | Field Scrolling:Bool 'Is the control scrolling? 13 | Field holding:Bool 'Is the user holding down the mouse button? 14 | Field clickStart:Bool 'Is the user holding down the button for a (non-drag) click? 15 | 16 | Field firstX:Float, firstY:Float 'Persistent touch origin. 17 | Field xMomentum:Float, yMomentum:Float, xOrigin:Float, yOrigin:Float 'Touch drag fields. (for scroll events) 18 | 19 | 'Widget managing elements 20 | Field Widgets:= New Stack 21 | Field WidgetsByID:= New IntMap 'For accessing widgets by ID 22 | Field childInput:PanelPointer 23 | 24 | 'Flags 25 | Field __endlessX:Bool, __endlessY:Bool 'Uncap the XY boundaries 26 | Field __zLowerLimit:Float = 0.1, __zUpperLimit:Float = 3 27 | 28 | Const FRICTION:Float = 0.08 'Amount of friction to apply to scroll control 29 | Const csDist:Float = 42 'Square of the distance from origin to determine whether the user intends to click or scroll 30 | 31 | Method New(x:Int, y:Int, w:Int, h:Int, Input:InputPointer) 32 | Super.New(x, y, w, h, Input) 33 | 34 | childInput = New PanelPointer(Input) 35 | End Method 36 | Method New(x:Int, y:Int, w:Int, h:Int, cw:Float, ch:Float, Input:InputPointer) 37 | Super.New(x, y, w, h, Input) 38 | Self.cw = cw; Self.ch = ch 39 | 40 | childInput = New PanelPointer(Input) 41 | End Method 42 | 43 | 'Summary: Permanently attaches a widget to this manager for control. Overrides existing input with child input. 44 | Method Attach:Int(widget:Widget, id:Int = -1) 45 | widget.Input = childInput 46 | 47 | If id = -1 Then id = widget.id 'Set ID to whatever the widget's ID is. 48 | If id = -1 Then 'If the widget doesn't have an ID, make one. 49 | id = UI.GenerateID(Widgets.Length()) 50 | widget.id = id 51 | End If 52 | 53 | Widgets.Push(widget) 54 | WidgetsByID.Add(id, widget) 55 | 56 | Return id 57 | End Method 58 | 59 | Method DetachAll:Void() 60 | WidgetsByID.Clear() 61 | Widgets.Clear() 62 | End Method 63 | 64 | 65 | Method MouseHit:Void() 66 | 'First touch. Set origin to this location. Set the scroll origin for the first time, also. 67 | firstX = Input.x; firstY = Input.y 68 | xOrigin = Input.x; yOrigin = Input.y 69 | 70 | 'Start checking to see whether this is a click or a scroll. 71 | 'If the control is currently spinning with momentum, then don't process a click, just stop the control. 72 | 'Otherwise, process a click. 73 | If yMomentum = 0 And xMomentum = 0 Then clickStart = True 74 | 75 | yMomentum = 0; xMomentum = 0 76 | Scrolling = True 77 | End Method 78 | 79 | Method MouseDown:Void() 80 | holding = True 81 | End Method 82 | 83 | Method MouseUp:Void() 84 | holding = False 85 | 86 | 'Put the first click origins into a galaxy far, far away. 87 | firstX = -$FFFFFF; firstY = -$FFFFFF 'Prevents re-clicks from instantly stopping the scroller on the next poll. 88 | 89 | 'Scrolling = False 90 | End Method 91 | 92 | Method MouseClick:Void() 93 | If clickStart = True Then 'Clicked instead of dragged. Do appropriate cell click behavior. 94 | clickStart = False 95 | xMomentum = 0 96 | yMomentum = 0 97 | 98 | End If 99 | End Method 100 | 101 | Method Poll:Void(xOffset:Float = 0, yOffset:Float = 0) 102 | 'Before polling, let's make sure that the position of the cursor isn't too far away from the origin. 103 | 'If it is, then we know to set clickStart FALSE before checking MouseClick(). This prevents a click 104 | 'from being processed on the same frame the cursor input was first initiated. 105 | 106 | If UI.DistSq(firstX, firstY, Input.x, Input.y) > csDist Then clickStart = False 107 | 108 | 'This method performs the necessary polling of the object, as well as updating the scrolling/clickstate logic. 109 | Super.Poll() 110 | 111 | 'Is the click+drag origin from this control? 112 | Local OriginallyFromHere:Bool = UI.WithinRect(firstX, firstY, x, y, w, h) 113 | 114 | xMomentum *= (1.0 - FRICTION) 'Apply friction 115 | yMomentum *= (1.0 - FRICTION) 116 | 117 | Local amtX:Float = Input.x - xOrigin 'Amount change from origin since last update 118 | Local amtY:Float = Input.y - yOrigin 119 | 120 | If holding and OriginallyFromHere Then 'drag 121 | cx += amtX 'Increment the offset. 122 | cy += amtY 'Increment the offset. 123 | If Abs(amtX) > Abs(xMomentum) Then xMomentum = amtX 'only add momentum if going faster than before. 124 | If Abs(amtY) > Abs(yMomentum) Then yMomentum = amtY 125 | 126 | If Sgn(amtX) <> 0 And Sgn(amtX) <> Sgn(xMomentum) Then xMomentum = amtX 'stops direction popping 127 | If Sgn(amtY) <> 0 And Sgn(amtY) <> Sgn(yMomentum) Then yMomentum = amtY 128 | xOrigin = Input.x 'update the origin for next update 129 | yOrigin = Input.y 130 | End If 131 | 132 | If Input.Up Then Self.MouseUp() 133 | 134 | If holding = False Or (holding And Not OriginallyFromHere) Then 'no drag. Slide with momentum. 135 | cx += xMomentum 136 | cy += yMomentum 137 | End If 138 | 139 | 'Dink the momentums if they're below threshold. 140 | 141 | If Abs(xMomentum) < 0.01 Then 142 | xMomentum = 0 143 | cx = Floor(cx + 0.4) 'round 144 | End If 145 | If Abs(yMomentum) < 0.01 Then 146 | yMomentum = 0 147 | cy = Floor(cy+0.4) 'round 148 | End If 149 | 150 | 'Dink the offset values and momentum if we hit the internal contents of the panel's limit. 151 | If __endlessX = False 152 | If cx > 0 'left 153 | cx = 0 154 | xMomentum = 0 155 | ElseIf cw > w And - cx > cw - w 'right 156 | cx = - (cw - w) 157 | xMomentum = 0 158 | ElseIf cw <= w 'no X scroll possible 159 | cx = 0 160 | xMomentum = 0 161 | End If 162 | End If 163 | If __endlessY = False 164 | If cy > 0 'top 165 | cy = 0 166 | yMomentum = 0 167 | ElseIf ch > h And - cy > ch - h 'bottom 168 | cy = - (ch - h) 169 | yMomentum = 0 170 | ElseIf ch <= h 'no Y scroll possible 171 | cy = 0 172 | yMomentum = 0 173 | End If 174 | End If 175 | 176 | 177 | 'Update the scrolling status. 178 | Scrolling = ( Not clickStart) And Not (xMomentum = 0 And yMomentum = 0) 179 | childInput.scrolling = Scrolling 180 | 181 | 'Update the status of the cursor inside the panel. 182 | childInput.inPanel = UI.WithinRect(childInput.p.x, childInput.p.y, x, y, w, h) 183 | 184 | 'Poll the child widgets. 185 | If Enabled 186 | For Local o:Widget = EachIn Widgets 187 | o.Poll(x + cx, y + cy) 188 | Next 189 | End If 190 | 191 | End Method 192 | 193 | Method Render:Void(xOffset:Float = 0, yOffset:Float = 0) 194 | UI.PushScissor() 195 | UI.SetXformedScissor(x, y, w, h) 196 | 197 | PushMatrix() 198 | Translate(x + cx, y + cy) 199 | 200 | RenderContent(xOffset, yOffset) 201 | 202 | PopMatrix() 203 | 204 | UI.PopScissor() 205 | End Method 206 | 207 | 'Summary: Renders the content of the panel after setting the proper translation/scissor. Override this and call Super to render widgets. 208 | Method RenderContent:Void(xOffset:Float = 0, yOffset:Float = 0) 209 | 'Render attached widgets 210 | If Enabled 211 | For Local o:Widget = EachIn Widgets 212 | o.Render(xOffset, yOffset) 213 | Next 214 | End If 215 | 'Note: You can call Super before or after your code, depending on where in the render order you want child widgets to render. 216 | End Method 217 | 218 | 'Summary: Attempts to detach a widget from this panel. 219 | Method Remove:Bool(id:Int) 220 | Local w:Widget = WidgetsByID.Get(id) 221 | If w <> Null 222 | WidgetsByID.Remove(id) 223 | Widgets.RemoveEach(w) 224 | Return True 225 | Else 226 | Print("Panel: Warning, widget " + id + " not found.") 227 | Return False 228 | End If 229 | End Method 230 | 231 | 'Summary: Returns the position of the scrollable area as a percentage of its bounds (for scrollbars). 232 | Method PercentX:Float() Property 233 | If __endlessX Then Return - 1 234 | Return (-cx / (cw - w)) 235 | End Method 236 | 'Summary: Returns the position of the scrollable area as a percentage of its bounds (for scrollbars). 237 | Method PercentY:Float() Property 238 | If __endlessY Then Return - 1 239 | Return (-cy / (ch - h)) 240 | End Method 241 | End Class 242 | 243 | 244 | 'Summary: Allows a Panel to override normal input if it's scrolling. 245 | Class PanelPointer Implements InputPointer 246 | Field p:InputPointer 247 | 248 | Field scrolling:Bool 249 | Field inPanel:Bool 250 | 251 | Method New(prototype:InputPointer) 252 | p = prototype 253 | End Method 254 | 255 | Method x:Float() Property 256 | Return p.x 257 | End Method 258 | Method y:Float() Property 259 | Return p.y 260 | End Method 261 | 262 | Method Poll:Void() 263 | 'Nothing here. Update the prototype instead. 264 | End Method 265 | 266 | 'No Set methods; These properties are read-only 267 | Method Hit:Bool() Property 268 | If ( Not scrolling) And inPanel Then Return p.Hit() 269 | End Method 270 | Method Down:Bool() Property 271 | If ( Not scrolling) And inPanel Then Return p.Down() 272 | End Method 273 | Method Up:Bool() Property 274 | If ( Not scrolling) And inPanel Then Return p.Up() 275 | End Method 276 | End Class -------------------------------------------------------------------------------- /SimpleUI/Scrollers.monkey: -------------------------------------------------------------------------------- 1 | ' Scrollers provide a means to scroll lists/panels of data. They are roughly equivalent to a ListBox 2 | ' in other UI toolkits, although the default Scroller type loops endlessly. The interface for Scrollers 3 | ' is optimized for touch-based input. 4 | ' -Nobuyuki (nobu@subsoap.com) 8 July 2013. 5 | 6 | Import mojo 7 | Import ui 8 | Import InputPointers 9 | Import widget 10 | 11 | Class Scroller Extends EndlessScroller 12 | Field Slack:Float 'How much did the user attempt to scroll past the border of this control? 13 | Field Snapper:SlackDrawer 14 | 15 | 'Summary: Initializes a new Scroller with numElements items. 16 | Method New(x:Int, y:Int, w:Int, h:Int, numElements:Int, Input:InputPointer, cellHeight:Float = 32) 17 | Super.New(x, y, w, h, numElements, Input, cellHeight) 18 | Snapper = New ExampleSlackDrawer(Self) 19 | __loopCells = False 20 | End Method 21 | 22 | 'Summary: Snaps the input offset to a sane one based on this control's sizes 23 | Method BorderSnap:Float(offset:Float) 24 | Local contentHeight:Float = Items.Length * cellHeight 25 | Return Clamp(offset, Min(0.0, -contentHeight + h), 0.0) 26 | End Method 27 | 28 | Method Render:Void(xOffset:Float = 0, yOffset:Float = 0) 29 | Snapper.RenderStart() 30 | Super.Render(xOffset, yOffset) 31 | Snapper.RenderEnd() 32 | End Method 33 | 34 | Method Poll:Void(xOffset:Float = 0, yOffset:Float = 0) 35 | Super.Poll() 36 | 37 | If Slack <> 0 And Input.Down = False Then Slack = Slack * 0.98 'Reduce slack per frame 38 | 39 | Local slack:Float = cOffset 40 | cOffset = BorderSnap(cOffset) 41 | Slack = slack - cOffset 42 | End Method 43 | 44 | End Class 45 | 46 | Class EndlessScroller Extends Widget 47 | 'Private 48 | 'WARNING: THESE VARIABLES SHOULD BE PRIVATE, BUT THEY'VE BEEN MADE PUBLIC TO ALLOW SUBCLASSING. 49 | ' IN THE FUTURE, THESE VARIABLES SHOULD BE SET USING THE 'Protected' DIRECTIVE, ONCE 50 | ' MONKEY SUPPORTS IT. 51 | 52 | Field startPos:Float 'The first element to draw in the control. Important for determining clicks, also. 53 | Field drawOffset:Float 'The actual y offset from which the drawing routines start drawing cells. 54 | 55 | Field holding:Bool 'Is the user holding down the mouse button? 56 | Field clickStart:Bool 'Is the user holding down the button for a (non-drag) click? 57 | 58 | 'Options and flags 59 | Field __scissorCells:Bool = True 'Forces cells to render only within their borders. This is necessary if cells draw outside their borders. 60 | Field __loopCells:Bool = True 'If the number of drawn cells is shorter than scroller's bounds, loopback to 0 and fill the remainder of the space. 61 | 62 | Public 63 | 'The origin and clip region of this control are determined in Super by x, y, w, and h. 64 | 'Field x:Int, y:Int, w:Int, h:Int 65 | 66 | Field firstX:Float, firstY:Float 'Persistent touch origin. 67 | Field cMomentum:Float, cOffset:Float, cOrigin:Float 'Touch drag fields. (for scroll events) 68 | 69 | Const FRICTION:Float = 0.08 'Amount of friction to apply to scroll control 70 | Const csDist:Float = 42 'The square of the distance from origin to determine whether the user intends to click or scroll; precalculated for speed 71 | 72 | Field Scrolling:Bool 'Is the control scrolling? 73 | 74 | Field Items:ScrollerCell[] 'Cells in this scroller. 75 | Field cellHeight:Float 'Cell height, for determining hitbox and element positioning. 76 | Field cellsToDraw:Int 'Cells to draw in the drawing operation. 77 | 78 | Field IndexChanged:Bool = False 'Changes to TRUE for 1 frame if the selected index changes. 79 | Field SelectedIndex:Int = -1 'The index of the cell clicked. 80 | 81 | 'Summary: Initializes a new Scroller with numElements items. 82 | Method New(x:Int, y:Int, w:Int, h:Int, numElements:Int, Input:InputPointer, cellHeight:Float = 32) 83 | Super.New(x, y, w, h, Input) 84 | 85 | Items = Items.Resize(numElements) 86 | 87 | Self.cellHeight = cellHeight 88 | cellsToDraw = Ceil(h / cellHeight) 89 | End Method 90 | 91 | Method AddItem:Void(item:ScrollerCell) 92 | Items = Items.Resize(Items.Length + 1) 'Add one 93 | Items[Items.Length - 1] = item 94 | End Method 95 | 96 | 'Summary: Determines which cell is at the current cursor position. 97 | Method CellAtCursorPosition:Int(Cursor:InputPointer) 98 | Local i:Float = ( (Cursor.y - y + (cellHeight - drawOffset)) / h) * (h / cellHeight) 99 | i = Floor(i) - 1 100 | Local index:Int = i + startPos 'this value is the "true" element position we use. 101 | 102 | 'If the list isn't supposed to loop, return an -1 if clicked in the scroller bounds but not on a cell. 103 | If __loopCells = False And index >= Items.Length Then Return - 1 104 | 105 | 'Now, let's clamp index to the correct value. 106 | 'While index > Items.Length - 1; index -= Items.Length; Wend 107 | index = CycleDown(index, Items.Length) 108 | Return index 109 | End Method 110 | 111 | 'Summary: Determines the click position relative to the cell at the cursor position. 112 | Method ClickPosition:Float[] (Cursor:InputPointer) 113 | Local i:Float = (Cursor.y - y + (cellHeight - drawOffset)) Mod cellHeight 114 | Return[Cursor.x - Self.x, i] 115 | End Method 116 | 117 | 'Summary: Internal function to cycle down a value out of range. 118 | Method CycleDown:Int(index:Int, maxvalue:Int) 119 | If maxvalue <= 0 Then Return 0 'stop an infinite loop 120 | While index >= maxvalue; index -= maxvalue; Wend 121 | Return index 122 | End Method 123 | 124 | 'NOTE: TODO: Implement xOffset/yOffset properly here. They were added to conform with Widget 125 | Method Render:Void(xOffset:Float = 0, yOffset:Float = 0) 126 | 'Sanity check 127 | If Items.Length < 1 Then Return 128 | If Visible = False Then Return 129 | 130 | 'Set the clipping rectangle for the widget's bounds if we're not scissoring on a cell-by-cell basis. 131 | If Not __scissorCells 132 | UI.PushScissor() 133 | UI.SetXformedScissor(x, y, w, h) 134 | End If 135 | 136 | 'We need to figure out which elements we need to draw in the clip rect. 137 | 'First, we should determine the content size. 138 | Local contentHeight:Int = Items.Length * cellHeight 139 | 140 | 'Now let's determine the first and last element to draw within the content. 141 | 'The start position in the content depends on whether the Offset is positive or negative. StartPos will always be > 0. 142 | 'The end position is just startPos + cellsToDraw. If the value leaks over the content size, we'll 143 | 'catch that later by subtracting content's index size. 144 | 'NOTE: These positions assume a sane offset value 145 | 'Local startPos:Float 'This will be floored later. 146 | 147 | 148 | Local SaneOffset:Float = GetSaneOffset(cOffset, contentHeight, -contentHeight) 149 | 150 | If Sgn(SaneOffset) <= 0 'Offset is 0 or negative. Set startPos normally. 151 | startPos = (-SaneOffset) / cellHeight 152 | ElseIf Sgn(SaneOffset) >= 1 'Offset is positive. Set startPos by subtracting offset from contentHeight. 153 | startPos = (contentHeight - SaneOffset) / cellHeight 154 | End If 155 | 156 | 'Let's get the actual offset we're gonna start drawing from. Since we're going to draw h/cellHeight+1 elements, our offset 157 | 'should always be between 0 and -cellHeight. To do this, we use modulus. Once again, the number we need depends on the 158 | 'sign of the offset. 159 | 'Local drawOffset:Float 160 | 161 | If Sgn(cOffset) > 0 'positive offset means we need to subtract cellHeight to get a negative drawOffset. 162 | drawOffset = (cellHeight - (cOffset mod cellHeight)) * -1 163 | Else 'a negative cOffset provides the range we want using simple modulus. 164 | drawOffset = cOffset Mod cellHeight 165 | End If 166 | If drawOffset = -cellHeight Then drawOffset = 0 'Fudge factor 167 | 168 | 169 | For Local i:Int = 0 To cellsToDraw 'Draw only the cells which are visible. 170 | If __loopCells = False 'Check to see if looping is disabled, and only draw the original cells. 171 | If Sgn(cOffset) > 0 172 | If i + startPos < Items.Length Then Continue 173 | If i + startPos > (Items.Length) * 2 Then Continue 174 | ElseIf Sgn(cOffset) = 0 'Do normal clamping. 175 | If i + startPos > Items.Length - 1 Then Continue 176 | Else 177 | If i + startPos > Items.Length Then Continue 178 | End If 179 | End If 180 | 181 | Local index:Int = i + startPos 'this value is the "true" element position we use. 182 | 'While index > Items.Length - 1; index -= Items.Length; Wend 'Cycle the element within valid range. 183 | index = CycleDown(index, Items.Length) 184 | 185 | 186 | 'Before Drawing the cells, Calculate the clip rectangle for them if specified. 187 | If __scissorCells Then 188 | Local cr:Float[] =[x, y + drawOffset + (i * cellHeight), w, cellHeight] 189 | 'Clamp a cell partially going off the top of the widget 190 | If cr[1] < y Then 191 | cr[3] -= (y - cr[1]) 'Reduce height of clip rect 192 | cr[1] = y 'set top of clip rect to top of widget. 193 | End If 194 | 195 | 'Clamp a cell partially going off the bottom of the widget 196 | If cr[1] + cellHeight > y + h Then 197 | cr[3] = ( (y + h) - cr[1]) 'Reduce height of clip rect 198 | End If 199 | 200 | 'Set the cell's scissor region. 201 | UI.PushScissor() 202 | UI.SetXformedScissor(cr[0], cr[1], cr[2], cr[3]) 203 | End If 204 | 205 | 'Draw the cell. 206 | If Items[index] <> Null Then Items[index].Draw(x, y + drawOffset + (i * cellHeight)) 207 | 208 | 'Reset the clipping rectangle. 209 | UI.PopScissor() 210 | Next 211 | 212 | 'Reset the clipping rectangle for the widget bounds. 213 | If Not __scissorCells Then UI.PopScissor() 214 | End Method 215 | 216 | Method GetSaneOffset:Float(offset:Float, range:Float, startPoint = 0) 217 | 'This function returns a sane drawing offset within the total items' size from a raw offset to a value between startPoint and range. 218 | Local returnValue:Float = offset 219 | While returnValue > range ; returnValue -= range ; Wend 220 | While returnValue < startPoint; returnValue += range; Wend 221 | Return returnValue 222 | End Method 223 | 224 | Method MouseHit:Void() 225 | 'First touch. Set origin to this location. Set the scroll origin for the first time, also. 226 | firstX = Input.x; firstY = Input.y 227 | cOrigin = Input.y 228 | 229 | 'Start checking to see whether this is a click or a scroll. 230 | 'If the control is currently spinning with momentum, then don't process a click, just stop the control. 231 | 'Otherwise, process a click. 232 | If cMomentum = 0 Then clickStart = True 233 | 234 | cMomentum = 0 235 | Scrolling = True 236 | End Method 237 | 238 | Method MouseDown:Void() 239 | holding = True 240 | End Method 241 | 242 | Method MouseUp:Void() 243 | holding = False 244 | 245 | firstX = -9999; firstY = -9999 246 | 247 | 'Scrolling = False 248 | End Method 249 | 250 | Method MouseClick:Void() 251 | If clickStart = True Then 'Clicked instead of dragged. Do appropriate cell click behavior. 252 | clickStart = False 253 | cMomentum = 0 254 | 255 | Local cell:Int = CellAtCursorPosition(Input) 256 | 257 | 'Don't process a click if the index is out of range. 258 | If cell >= Items.Length or cell < - 1 Then Return 259 | If SelectedIndex >= Items.Length Then Return 260 | 261 | 'Deselect the previously selected index. 262 | If SelectedIndex >= 0 Then 263 | If Items[SelectedIndex] = Null Then Return 'Don't process a click if there's nothing here. 264 | Items[SelectedIndex].Selected = False 265 | End If 266 | 267 | 'If cell is -1 then this is probably a non-looping scroller click. Deselect the previously selected index. 268 | If cell = -1 269 | SelectedIndex = -1 270 | Return 271 | End If 272 | 273 | 'Update the selected index. 274 | SelectedIndex = cell 275 | If Items[SelectedIndex] <> Null Then Items[SelectedIndex].Selected = True 276 | 277 | 'Process the click event in the cell and indicate the index has changed. 278 | Local pos:Float[] = ClickPosition(Input) 279 | If Items[SelectedIndex] <> Null Then Items[SelectedIndex].Click(pos[0], pos[1]) 280 | IndexChanged = True 281 | End If 282 | End Method 283 | 284 | 'NOTE: TODO: Implement xOffset/yOffset properly here. They were added to conform with Widget 285 | Method Poll:Void(xOffset:Float = 0, yOffset:Float = 0) 286 | 'Before polling, let's make sure that the position of the cursor isn't too far away from the origin. 287 | 'If it is, then we know to set clickStart FALSE before checking MouseClick(). This prevents a click 288 | 'from being processed on the same frame the cursor input was first initiated. 289 | 290 | If UI.DistSq(firstX, firstY, Input.x, Input.y) > csDist Then clickStart = False 291 | 292 | 'Let's also reset the IndexChanged value of the control from the last loop. 293 | IndexChanged = False 294 | 295 | 'This method performs the necessary polling of the object, as well as updating the scrolling/clickstate logic. 296 | Super.Poll() 297 | 298 | 'Is the click+drag origin from this control? 299 | Local OriginallyFromHere:Bool = UI.WithinRect(firstX, firstY, x, y, w, h) 300 | 301 | cMomentum *= (1.0 - FRICTION) 'Apply friction 302 | 303 | Local amt:Float = Input.y - cOrigin 'Amount change from origin since last update 304 | 305 | If holding and OriginallyFromHere Then 'drag 306 | cOffset += amt 'Increment the offset. 307 | if Abs(amt) > Abs(cMomentum) Then cMomentum = amt 'only add momentum if going faster than before. 308 | if Sgn(amt) <> 0 And Sgn(amt) <> Sgn(cMomentum) Then cMomentum = amt 'stops direction popping 309 | cOrigin = Input.y 'update the origin for next update 310 | End If 311 | 312 | If Input.Up Then Self.MouseUp() 313 | 314 | If holding = False Or (holding And Not OriginallyFromHere) Then 'no drag. Slide with momentum. 315 | cOffset += cMomentum 316 | End If 317 | 318 | if Abs(cMomentum) < 0.01 Then 'Reset the control 319 | cMomentum = 0 320 | cOffset = Floor(cOffset+0.4) 'round 321 | 322 | 'Put cOffset back in a sane place 323 | cOffset = GetSaneOffset(cOffset, cellHeight * Items.Length, -cellHeight * Items.Length) 324 | End If 325 | 326 | 'Update the scrolling status. 327 | Scrolling = ( Not clickStart) And Not (cMomentum = 0) 328 | 329 | End Method 330 | 331 | End Class 332 | 333 | 'Summary: This interface is provided to display and handle clicks within a Scroller's cell. 334 | Interface ScrollerCell 335 | 'Property Methods are defined here to enforce their existence in implementing classes. 336 | Method Selected:Bool() Property 'Is this cell the selected one in the list? 337 | Method Selected:Void(value:Bool) Property 338 | 339 | Method Draw:Void(xOffset:Float, yOffset:Float) 340 | Method Click:Void(xOffset:Float = -1, yOffset:Float = -1) 341 | End Interface 342 | 343 | Class ExampleCell Implements ScrollerCell 344 | Private 345 | Field timer:Int 346 | Field _selected:Bool 347 | Public 348 | Field x:Float, y:Float, w:Float, h:Float 349 | Field r:Int, g:Int, b:Int 350 | Field text:String 351 | Field lastX:Float = -1, lastY:Float = -1 352 | 353 | Method Selected:Bool() Property 354 | Return _selected 355 | End Method 356 | 357 | Method Selected:Void(value:Bool) Property 358 | _selected = value 359 | End Method 360 | 361 | Method Draw:Void(xOffset:Float, yOffset:Float) 362 | SetAlpha(0.6) 363 | SetColor(r, g, b) 364 | 365 | If timer > 0 Then 366 | timer -= 1 367 | SetAlpha(1) 368 | End If 369 | 370 | DrawRect(xOffset, yOffset, w, h) 371 | SetColor(255, 255, 255) 372 | DrawText(text, x + xOffset, y + yOffset) 373 | 374 | SetAlpha(1) 375 | If _selected = True Then 376 | DrawCircle(xOffset + (w - h / 2), yOffset + h / 2, h / 3) 377 | 378 | If lastX > 0 Then DrawLine(xOffset + lastX, yOffset, xOffset + lastX, yOffset + h) 379 | If lastY > 0 Then DrawLine(xOffset, yOffset + lastY, xOffset + w, yOffset + lastY) 380 | SetAlpha(0.5); DrawText(lastX + "," + lastY, lastX + xOffset, lastY + yOffset) 381 | SetAlpha(1) 382 | End If 383 | End Method 384 | 385 | Method Click:Void(xOffset:Float = -1, yOffset:Float = -1) 386 | timer = 10 387 | If xOffset <> - 1 Then lastX = xOffset 388 | If yOffset <> - 1 Then lastY = yOffset 389 | End Method 390 | End Class 391 | 392 | 'Summary: This interface is provided to provide draw routines for bordered Scrollers when the user drags past the limit. 393 | Interface SlackDrawer 394 | 'Property Methods are defined here to enforce their existence in implementing classes. 395 | Method Horizontal:Bool() Property 'Is the parent control Horizontal? 396 | Method Horizontal:Void(value:Bool) Property 397 | 398 | Method New(parent:Scroller) 'Provides the parent for this control 399 | Method RenderStart:Void() 'Done at beginning of Render() 400 | Method RenderEnd:Void() 'Done at the end of Render() 401 | End Interface 402 | 403 | 'Summary: This example provides a simple snap effect when the user drags past the scroller limit. 404 | Class ExampleSlackDrawer Implements SlackDrawer 405 | Private 406 | Field _horizontal:Bool 407 | Public 408 | Field p:Scroller 409 | 410 | Method New(parent:Scroller) 411 | p = parent 412 | End Method 413 | 414 | 'Satisfies SlackDrawer interface requirement 415 | Method Horizontal:Bool() Property 416 | Return _horizontal 417 | End Method 418 | Method Horizontal:Void(value:Bool) Property 419 | _horizontal = value 420 | End Method 421 | 422 | 423 | Method RenderStart:Void() 424 | p.cOffset += (p.Slack * 0.5) 425 | End Method 426 | 427 | Method RenderEnd:Void() 428 | p.cOffset -= (p.Slack * 0.5) 429 | End Method 430 | 431 | End Class --------------------------------------------------------------------------------