├── example.png ├── example2.png ├── .gitignore ├── .gitattributes ├── mojoproject.toml ├── demo_simple_simd_counter.mojo ├── demo_custom_events.mojo ├── demo_horizontal_vertical_containers.mojo ├── demo_todos.mojo ├── demo_keyboard_and_css.mojo ├── keyboard_handler.js ├── LICENSE ├── demo_widgets_returns.mojo ├── theme_neutral.css ├── theme.css ├── demo_principal.mojo ├── base.js ├── readme.md └── ui.mojo /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rd4com/mojo-ui-html/HEAD/example.png -------------------------------------------------------------------------------- /example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rd4com/mojo-ui-html/HEAD/example2.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # pixi environments 3 | .pixi 4 | *.egg-info 5 | # magic environments 6 | .magic 7 | magic.lock 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # SCM syntax highlighting & preventing 3-way merges 2 | pixi.lock merge=binary linguist-language=YAML linguist-generated=true 3 | -------------------------------------------------------------------------------- /mojoproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = ["rd4com <144297616+rd4com@users.noreply.github.com>"] 3 | channels = ["https://conda.modular.com/max", "https://repo.prefix.dev/modular-community", "conda-forge"] 4 | name = "mojo-ui-html" 5 | platforms = ["linux-64"] 6 | version = "0.1.0" 7 | 8 | [tasks] 9 | 10 | [dependencies] 11 | max = ">=25.3.0,<26" 12 | -------------------------------------------------------------------------------- /demo_simple_simd_counter.mojo: -------------------------------------------------------------------------------- 1 | from ui import * 2 | from math import iota, sqrt 3 | from sys import simdwidthof 4 | def main(): 5 | GUI = Server() 6 | var counter = 0 7 | while GUI.NeedNewRendition(): 8 | #Not necessary to create a window if not needed 9 | if GUI.Button("increment"): counter+=1 10 | if GUI.Button("decrement"): counter-=1 11 | GUI.Slider("Counter",counter) 12 | var tmp = iota[DType.float16, simdwidthof[DType.float16]()](counter) 13 | GUI.Text(repr(tmp)) 14 | GUI.Text(repr(sqrt(tmp))) 15 | -------------------------------------------------------------------------------- /demo_custom_events.mojo: -------------------------------------------------------------------------------- 1 | # Not ready yet, just a start! 2 | from ui import * 3 | def main(): 4 | GUI = Server() 5 | var counter = 0 6 | while GUI.NeedNewRendition(): 7 | var MyCustomEvent = GUI.CustomEvent("MyEvent") 8 | if MyCustomEvent: 9 | counter+=1 10 | print(MyCustomEvent.take() == "ok") 11 | GUI.RawHtml(String( 12 | "", 13 | "➡️ mouse hover ⬅️", 14 | "" 15 | )) 16 | GUI.Text(repr(counter)) #Increments -------------------------------------------------------------------------------- /demo_horizontal_vertical_containers.mojo: -------------------------------------------------------------------------------- 1 | from ui import * 2 | def main(): 3 | GUI = Server() 4 | while GUI.NeedNewRendition(): 5 | with GUI.HorizontalGrow(): 6 | with GUI.VerticalGrow(): 7 | GUI.Text("First column") 8 | for i in range(3): 9 | GUI.Button(GUI.Digitize(i)) 10 | with GUI.VerticalGrow(): 11 | GUI.Text("Second column") 12 | for i in range(3): 13 | GUI.Button(repr(i)) 14 | 15 | #Result: 16 | # First column Second column 17 | # 0️⃣ 0 18 | # 1️⃣ 1 19 | # 2️⃣ 2 20 | -------------------------------------------------------------------------------- /demo_todos.mojo: -------------------------------------------------------------------------------- 1 | 2 | from ui import * 3 | def main(): 4 | GUI = Server() 5 | var txt:String = "test" 6 | var todos= List[String]() 7 | var time:String = "15:00" 8 | var date:String = "2024-01-01" 9 | var pos = Position(256, 128) 10 | while GUI.NeedNewRendition(): 11 | with GUI.Window("Todo app",pos): 12 | GUI.Text(String(len(todos))) 13 | with GUI.ScrollableArea(128): 14 | for t in todos: 15 | GUI.Text(t[]) 16 | GUI.NewLine() 17 | 18 | GUI.TimeSelector(time) 19 | GUI.DateSelector(date) 20 | GUI.TextInput("textinput",txt) 21 | 22 | if GUI.Button("Add"): 23 | todos.append(GUI.Circle.Blue+" "+time+" " + date + " " + txt) 24 | if GUI.Button("pop"): 25 | if len(todos):todos.pop() 26 | -------------------------------------------------------------------------------- /demo_keyboard_and_css.mojo: -------------------------------------------------------------------------------- 1 | from ui import * 2 | 3 | def main(): 4 | var GUI = Server() 5 | GUI.request_interval_second = 0 #no Time.sleep between events 6 | GUI.keyboard_handler = True 7 | var pos = SIMD[DType.int32, 2](0) #[x, y] 8 | while GUI.NeedNewRendition(): 9 | k = GUI.KeyDown() 10 | if not k.isa[NoneType](): 11 | # if k.isa[Int](): 12 | # print(k[Int])# example: ord('a'), .. 13 | if k.isa[String](): 14 | var k_tmp = k[String] 15 | if k_tmp == "ArrowUp": pos[1] -= 10 16 | elif k_tmp == "ArrowDown": pos[1] += 10 17 | elif k_tmp == "ArrowLeft": pos[0] -= 10 18 | elif k_tmp == "ArrowRight": pos[0] += 10 19 | GUI.RawHtml(String( 20 | "
🚙
" 24 | )) 25 | -------------------------------------------------------------------------------- /keyboard_handler.js: -------------------------------------------------------------------------------- 1 | 2 | async function send_keyboard(url){ 3 | 4 | const response = await fetch(url); 5 | const response2 = await response.text(); 6 | let parser = new DOMParser(); 7 | doc = parser.parseFromString( response2, 'text/html' ); 8 | document.replaceChild( doc.documentElement, document.documentElement ); 9 | 10 | } 11 | 12 | document.addEventListener('keypress', (e) => { 13 | const activeElement = document.activeElement; 14 | //console.log(e.key,e.code,e.keyCode) 15 | //console.log(activeElement.tagName) 16 | if ("body" == activeElement.tagName.toLowerCase()){ 17 | //only if InputElement dont have focus 18 | send_keyboard("/keyboard/down/"+e.keyCode) 19 | } 20 | //would be better to check if document.activeElement.data-attribute["keyboard"] 21 | //so that each elements can have separate event 22 | }); 23 | 24 | document.addEventListener('keydown', (e) => { 25 | const activeElement = document.activeElement; 26 | if ("body" == activeElement.tagName.toLowerCase()){ 27 | if (e.key.length > 1){ 28 | send_keyboard("/keyboard/down/keydown-"+e.key) 29 | } 30 | } 31 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 rd4com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo_widgets_returns.mojo: -------------------------------------------------------------------------------- 1 | from ui import * 2 | 3 | def main(): 4 | var GUI = Server() 5 | var pos = Position(128,128,1.0) 6 | var s:String = "" 7 | var t:String = "23:59" 8 | var c:String = "#3584E4" 9 | var i:Int = 0 10 | var i2:Int = 0 11 | var b:Bool = False 12 | var message:String = "Empty" 13 | 14 | while GUI.NeedNewRendition(): 15 | with GUI.Window(message,pos,"background-color:"+c): 16 | GUI.Text('s = '+s) 17 | GUI.Text('i = '+String(i)) 18 | GUI.Text('b = '+String(b)) 19 | GUI.Text('i2= '+String(i2)) 20 | GUI.Text('t = '+String(t)) 21 | GUI.Text('c = '+String(c)) 22 | 23 | if GUI.ComboBox("Choice",i,"a","b"): 24 | message = "ComboBox: "+String(i) 25 | if GUI.Button("Click"): message = "Click" 26 | if GUI.TextInput("Edit",s): 27 | message = "TextInput: "+ s 28 | if GUI.Toggle(b,"CheckBox"): 29 | message = "CheckBox: "+ String(b) 30 | if GUI.Slider("Slider",i2): 31 | message = "Slider: "+ String(i2) 32 | if GUI.TimeSelector(t): 33 | message = "TimeSelector: "+t 34 | if GUI.ColorSelector(c): 35 | message = "ColorSelector: "+c 36 | 37 | -------------------------------------------------------------------------------- /theme_neutral.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; padding:1px; 3 | font-family:monospace; 4 | font-size: 100%; 5 | min-height: 100%; 6 | background: rgb(128,128,128); 7 | } 8 | 9 | html {min-height: 100%;} 10 | 11 | .Button_ { 12 | margin:4px; 13 | border-width: 1px; 14 | border-color: black; 15 | border-style: solid; 16 | background-color: black; 17 | color:white; 18 | max-width: fit-content; 19 | } 20 | 21 | .ToggleOff_ { 22 | margin:4px;border-width: 1px;color: black;border-color: black;border-style: solid;background-color: red;max-width: fit-content; 23 | } 24 | 25 | .ToggleOn_ { 26 | margin:4px;border-width: 1px;color: black;border-color: black;border-style: solid;background-color: green;max-width: fit-content; 27 | } 28 | 29 | .Text_ { 30 | padding:4px; 31 | color: black; 32 | max-width: fit-content; 33 | } 34 | 35 | .SliderBox_ { 36 | border: 1px solid black; margin:4px; max-width: fit-content; 37 | } 38 | 39 | .SliderLabel_ { 40 | color: white; background-color: black; 41 | } 42 | 43 | 44 | .TextInputBox_ { 45 | background-color:black; color:white; border: 1px solid black; margin:4px; max-width: fit-content; 46 | } 47 | .TextInputElement_ { 48 | font-size: 75%;border: 0px;max-width: fit-content; 49 | } 50 | 51 | 52 | .Window_ { 53 | position: absolute; 54 | border-width: 1px;border-color: black;border-style: solid;;max-width: fit-content; 55 | } 56 | 57 | .WindowContent_ { 58 | padding:1px;background-color: white; 59 | } 60 | 61 | .WindowTitle_ { 62 | background-color: black;color: white;border-bottom: 1px solid black; 63 | } 64 | 65 | .DateSelector_ { 66 | margin:4px; 67 | /*border:0;*/ 68 | } 69 | 70 | .ColorSelector_ { 71 | padding:0px;margin:4px;border:4px double black; 72 | } 73 | 74 | .ComboBox_ { 75 | background-color:black; 76 | color:white; 77 | border: 1px solid black; 78 | margin:4px; 79 | max-width: fit-content; 80 | } 81 | .ComboBoxSelect_ { 82 | font-size: 75%;border: 0px;max-width: fit-content; 83 | } 84 | 85 | .TextChoiceFieldset_ { 86 | margin:4px;border:1px solid black; 87 | } 88 | 89 | .TextChoiceLegend_ { 90 | border:1px solid black;background-color: black;color:white 91 | } 92 | 93 | .Ticker_ { 94 | max-width: fit-content;margin:2px; 95 | } 96 | 97 | .ScrollableArea_ { 98 | margin:4px; border: 1px solid black; overflow: scroll; 99 | } 100 | 101 | .Collapsible_ { 102 | background-color: black; 103 | color:white; 104 | } 105 | 106 | .rendition_box { 107 | z-index: 2; 108 | position: absolute; 109 | background-color: red; 110 | border: 1px solid black; 111 | color: white; 112 | width: 128px; 113 | height: 128px; 114 | right: 0; 115 | font-size: 100px; 116 | top:0px; 117 | } -------------------------------------------------------------------------------- /theme.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; padding:1px; 3 | font-family:monospace; 4 | font-size: 200%; 5 | min-height: 100%; 6 | background: linear-gradient(0deg, rgba(255,255,0,1) 0%, rgba(255,255,0,1) 15%, rgba(255,0,0,1) 100%); 7 | } 8 | 9 | html {min-height: 100%;} 10 | 11 | .Button_ { 12 | margin:1px; 13 | border-width: 4px;color: blue;border-color: black;border-style: solid;background-color: yellow;max-width: fit-content; 14 | } 15 | 16 | 17 | .ToggleOff_ { 18 | margin:4px;border-width: 4px;color: black;border-color: black;border-style: solid;background-color: red;max-width: fit-content; 19 | } 20 | 21 | .ToggleOn_ { 22 | margin:4px;border-width: 4px;color: black;border-color: black;border-style: solid;background-color: green;max-width: fit-content; 23 | } 24 | 25 | 26 | .Text_ { 27 | padding:4px; 28 | color: black; 29 | max-width: fit-content; 30 | } 31 | 32 | 33 | .SliderBox_ { 34 | border: 4px dotted black;margin:1px;max-width: fit-content; 35 | } 36 | 37 | .SliderLabel_ { 38 | color: black; background-color: white; font-weight: bold; 39 | } 40 | 41 | 42 | .TextInputBox_ { 43 | font-weight: bold; 44 | border: 4px dashed black;margin:1px;max-width: fit-content; 45 | } 46 | .TextInputElement_ { 47 | font-size: 75%;border: 0px;max-width: fit-content; 48 | } 49 | 50 | 51 | .Window_ { 52 | position: absolute; 53 | border-width: 4px;border-color: black;border-style: solid;;max-width: fit-content; 54 | } 55 | 56 | .WindowContent_ { 57 | padding:1px;background-color: white; 58 | } 59 | 60 | .WindowTitle_ { 61 | background-color: rgb(255,127,0);color: white;border-bottom: 4px solid black; 62 | } 63 | 64 | 65 | .DateSelector_ { 66 | margin:4px;border:0; 67 | } 68 | 69 | .ColorSelector_ { 70 | padding:0px;margin:4px;border:4px double black; 71 | } 72 | 73 | 74 | .ComboBox_ { 75 | font-weight: bold; 76 | border: 4px solid black;margin:1px;max-width: fit-content; 77 | } 78 | .ComboBoxSelect_ { 79 | font-size: 75%;border: 0px;max-width: fit-content; 80 | } 81 | 82 | 83 | .TextChoiceFieldset_ { 84 | border:4px dashed black; 85 | } 86 | 87 | .TextChoiceLegend_ { 88 | border:4px solid black;background-color: orange; 89 | } 90 | 91 | .Ticker_ { 92 | max-width: fit-content;margin:2px; 93 | } 94 | 95 | .ScrollableArea_ { 96 | padding:4px;margin:4px; border: 4px dotted grey; overflow: scroll 97 | } 98 | 99 | .Collapsible_ { 100 | background-color: whitesmoke; 101 | color:black; 102 | } 103 | 104 | .rendition_box { 105 | z-index: 2; 106 | position: absolute; 107 | background-color: red; 108 | border: 1px solid black; 109 | color: white; 110 | width: 128px; 111 | height: 128px; 112 | right: 0; 113 | font-size: 100px; 114 | top:0px; 115 | } -------------------------------------------------------------------------------- /demo_principal.mojo: -------------------------------------------------------------------------------- 1 | from ui import * 2 | 3 | def main(): 4 | #⚠️ see readme.md in order to be aware about challenges and limitations! 5 | val = 50 6 | txt = String("Naïve UTF8 🥳") 7 | boolval = True 8 | multichoicevalue = String("First") 9 | colorvalue = String("#1C71D8") 10 | datevalue = String("2024-01-01") 11 | 12 | GUI = Server() 13 | 14 | POS = Position(1,1) 15 | POS2 = Position(1,350) 16 | POS3 = Position(32,512) 17 | POS4 = Position(512,16) 18 | 19 | combovalues = List[String]() 20 | for i in range(5): combovalues.append("Value "+String(i)) 21 | selection = 1 22 | 23 | while GUI.NeedNewRendition(): 24 | with GUI.Window("Debug window",POS): 25 | GUI.Text("Hello world 🔥") 26 | if GUI.Button("Button"): val = 50 27 | if GUI.Slider("Slider",val): 28 | print("Changed") 29 | GUI.TextInput("Input",txt) #⚠️ ```maxlength='32'``` attribute by default. 30 | GUI.ComboBox("ComboBox",combovalues,selection) 31 | GUI.Toggle(boolval,"Checkbox") 32 | 33 | with GUI.Window("Fun features",POS3): 34 | GUI.Text(GUI.Circle.Green + " Green circle") 35 | GUI.Text(GUI.Square.Blue + " Blue square") 36 | GUI.Text(GUI.Accessibility.Info + " Some icons") 37 | GUI.Text(GUI.Bold("Bold() ")+GUI.Highlight("Highlight()")) 38 | GUI.Text(GUI.Small("small") + " text") 39 | 40 | with GUI.Collapsible("Collapsible()"): 41 | GUI.Text("Content") 42 | 43 | with GUI.Window("More widgets",POS4): 44 | GUI.TextChoice("Multi Choice", multichoicevalue,"First","Second") 45 | GUI.Ticker("⬅️♾️ cycling left in a 128 pixels area",width=128) 46 | 47 | with GUI.Table(): 48 | for r in range(3): 49 | with GUI.Row(): 50 | for c in range(3): 51 | with GUI.Cell(): 52 | GUI.Text(String(r) + "," + String(c)) 53 | 54 | with GUI.ScrollableArea(123): 55 | GUI.Text(GUI.Bold("ScrollableArea()")) 56 | GUI.ColorSelector(colorvalue) 57 | GUI.NewLine() 58 | GUI.DateSelector(datevalue) #⚠️ format is unclear (see readme.md) 59 | for i in range(10): GUI.Text(String(i)) 60 | 61 | 62 | with GUI.Window("Values",POS2,CSSTitle="background-color:"+colorvalue): 63 | GUI.Text(txt) 64 | if selection < len(combovalues): #manual bound check for now 65 | GUI.Text(combovalues[selection]) 66 | with GUI.Tag("div","background-color:"+colorvalue): 67 | GUI.Text(colorvalue) 68 | GUI.Text(datevalue) 69 | with GUI.Tag("div","padding:0px;margin:0px;font-size:100"): 70 | GUI.Text("❤️‍🔥") 71 | GUI.Button("ok",CSS="font-size:32;background-color:"+colorvalue) 72 | 73 | -------------------------------------------------------------------------------- /base.js: -------------------------------------------------------------------------------- 1 | function plot_click(event, event_name){ 2 | var x = event.clientX; 3 | event_and_refresh("/custom_event_"+event_name+"/L"+x) 4 | } 5 | function plot_click2(event, event_name){ 6 | event.preventDefault() 7 | var x = event.clientX; 8 | event_and_refresh("/custom_event_"+event_name+"/R"+x) 9 | } 10 | window.onload = function () { 11 | window.scrollTo(0,localStorage.getItem("scrollpos")) 12 | // var elements = [] 13 | // var min_volume = 1.0/16.0 14 | // for (let i = 0; i < 16; i++) { 15 | // var tmp_element = document.getElementById("AudioPlayerSpecial"+i); 16 | // if ( tmp_element != null && typeof tmp_element !== "undefined"){ 17 | // tmp_element.volume = min_volume*parseFloat(tmp_element.dataset.volume) 18 | // elements.push(tmp_element) 19 | // } 20 | // } 21 | // for (var i=0; i=0.2){ 108 | e.target.dataset.zoomlevel = parseFloat(e.target.dataset.zoomlevel)-0.1 109 | window.location.href = "/window_scale_"+e.target.parentElement.id+"/-1" 110 | } 111 | } 112 | //e.target.nextSibling.style.transformOrigin="0% 0% 0px" 113 | //e.target.parentElement.style.transform = "scale("+e.target.dataset.zoomlevel+")" 114 | //window.location.href = "/window_scale_"+e.target.parentElement.id+"/"+e.target.dataset.zoomlevel 115 | e.preventDefault() 116 | } 117 | function event_and_refresh(data){ 118 | window.location.href=data 119 | } 120 | function send_element_value(event){ 121 | window.location.href=event.target.dataset.callbackurl+event.target.value 122 | } 123 | window.onscroll = function () { 124 | localStorage.setItem("scrollpos", window.scrollY) 125 | }; 126 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | #### 🆕 2 | 3 | - `mojoproject.toml` for `magic` ! 4 | - 🔁 Updated for `25.3.0` 🔥! 5 | 6 |   7 | 8 | #### ➕ Smaller changes/new features 9 | - UI remember scroll position between events and refreshes 10 | - Any window can be minimized/expanded into/from a titlebar with the `➖` button 11 | - Move some parameters from `Server` to `param_get_env` 12 | - `mojo run -D mojo_ui_html_theme="theme_neutral.css"` 13 | - `exit_if_request_not_from_localhost` (default: `True`) 14 | - Add custom html easily 15 | - `GUI.RawHtml("

Page

")` 16 | - `GUI.CustomEvent('MyEvent')` 17 | 18 | see [demo_custom_events.mojo](./demo_custom_events.mojo) 19 | 20 | (Just a start, not ready/user-friendly yet!) 21 | 22 | - `GUI.Event()` ➡️ `GUI.NeedNewRendition()` 23 | 24 | Because events are handled once, 25 | 26 | but multiple renditions are usually done! 27 | 28 | - Faster rendering using more types 29 | 30 | TODO: `initial_capacity` and `clear()` 31 | 32 | - Add `with` ➡️`GUI.HorizontalGrow`, ⬇️`GUI.VerticalGrow` 33 | 34 | Theses are flex `div`, the content added grows it. 35 | 36 | see [demo_horizontal_vertical](./demo_horizontal_vertical_containers.mojo) 37 | 38 | 39 |   40 | 41 | #### 🥾 Quick start 42 | ```bash 43 | git clone https://github.com/rd4com/mojo-ui-html 44 | cd mojo-ui-html 45 | magic shell 46 | mojo run demo_principal.mojo 47 | ``` 48 | 49 |   50 | 51 | #### ❤️‍🔥 Mojo is a programming language 52 | 53 | This gui is built using mojo! 54 | 55 | User-friendly, fast, active community, friends, python family, love it! 56 | 57 | > **MAX and Mojo usage and distribution are licensed under the [MAX & Mojo Community License](https://www.modular.com/legal/max-mojo-license)** 58 | 59 | 60 |   61 | 62 | #### 🎮 Good start to `learn mojo` by `creating` a small `videogame` ! 63 | This new feature: `GUI.keyboard_handler` is cool, 64 | 65 | it can work even when there are unfocussed UI widgets in a window👍 66 | ```mojo 67 | from ui import * 68 | 69 | def main(): 70 | var GUI = Server() 71 | GUI.request_interval_second = 0 #no Time.sleep between events 72 | GUI.keyboard_handler = True 73 | var pos = SIMD[DType.int32, 2](0) #[x, y] 74 | while GUI.NeedNewRendition(): 75 | k = GUI.KeyDown() 76 | if not k.isa[NoneType](): 77 | # if k.isa[Int](): 78 | # print(k[Int])# example: ord('a'), .. 79 | if k.isa[String](): 80 | var k_tmp = k[String] 81 | if k_tmp == "ArrowUp": pos[1] -= 10 82 | elif k_tmp == "ArrowDown": pos[1] += 10 83 | elif k_tmp == "ArrowLeft": pos[0] -= 10 84 | elif k_tmp == "ArrowRight": pos[0] += 10 85 | GUI.RawHtml(String( 86 | "
🚙
" 90 | )) 91 | ``` 92 | 93 | ⬅️ ⬆️ ⬇️ ➡️ to move the 🚙 on the `html` page! 94 | 95 |   96 | 97 | #### 📋 Example todo app 98 | ```mojo 99 | from ui import * 100 | def main(): 101 | GUI = Server() 102 | var txt:String = "test" 103 | var todos= List[String]() 104 | var time:String = "15:00" 105 | var date:String = "2024-01-01" 106 | var pos = Position(256,128,2.0) 107 | while GUI.NeedNewRendition(): 108 | with GUI.Window("Todo app",pos): 109 | GUI.Text(str(len(todos))) 110 | with GUI.ScrollableArea(128): 111 | for i in range(len(todos)): 112 | GUI.Text(todos[i]) 113 | GUI.NewLine() 114 | 115 | GUI.TimeSelector(time) 116 | GUI.DateSelector(date) 117 | GUI.TextInput("textinput",txt) 118 | 119 | if GUI.Button("Add"): 120 | todos.append(GUI.Circle.Blue+" "+time+" " + date + " " + txt) 121 | if GUI.Button("pop"): 122 | if len(todos):todos.pop() 123 | ``` 124 | 125 |   126 | 127 |   128 | 129 | 130 | #### 🔺 Next todo: 131 | Create a tutorial that adds different widgets to the page (one by one), 132 | 133 | progressively learning how to add to the ui. 134 | 135 |   136 | 137 |   138 | 139 | # mojo-ui-html 140 | 141 | 142 | - ### 👷👷‍♀️ Under construction, make sure to wear a helmet ! 143 | 144 | - ### 🤕 Bugs and unexpected behaviours are to be expected 145 | 146 | - ### ⏳ not beginner-friendly yet (will be in the future❤️‍🔥) 147 | 148 | - ### Not ready for use yet, feedbacks, ideas and contributions welcome! 149 | 150 |   151 | 152 | ## ⚠️ 153 | - Server on ```127.0.0.1:8000``` 154 | - not meant to be a web-framework, but a simple ui 155 | - Exit loop if request from other than "127.0.0.1" by default 156 | - As an additional safeguard (not been tested) 157 | - Dom generated from the content of values 158 | 159 | - ```example: ""``` 160 | - ```UNSAFE because value content can generate/modify html or javascript.``` 161 | 162 | - If the widget id is the address of a value, two input widgets of the same value will trigger twice (need more thinking for solution) 163 | 164 | 165 | 166 | - Blocking loop by default 167 | 168 | - ❤️‍🔥 [How 'widgets' attempts(⚠️) to maintain the rendering up-to-date ?](#how-widgets-attempts%EF%B8%8F-to-maintain-the-rendering-up-to-date-) 169 | - Will spin the loop multiple times and send only last rendition as response 170 | - events are handled once, but multiple renditions are done 171 | - ```should_re_render()``` for user initiated triggering! 172 | - once response is sent, need to wait for next request (blocking socket) 173 | 174 | 175 | - Probably more 176 | 177 |   178 | 179 | 180 | 181 | 182 | # Principal demo: ```theme.css```(default) 183 | 184 | 185 | 186 |   187 | 188 | # Simple simd counters: ```theme_neutral.css``` 189 | 190 | 191 | 192 | ```python 193 | from ui import * 194 | from math import iota, sqrt 195 | from sys import simdwidthof 196 | def main(): 197 | GUI = Server() 198 | var counter = 0 199 | while GUI.NeedNewRendition(): 200 | #Not necessary to create a window if not needed 201 | if GUI.Button("increment"): counter+=1 202 | if GUI.Button("decrement"): counter-=1 203 | GUI.Slider("Counter",counter) 204 | var tmp = iota[DType.float16, simdwidthof[DType.float16]()](counter) 205 | GUI.Text(repr(tmp)) 206 | GUI.Text(repr(sqrt(tmp))) 207 | ``` 208 | 209 |   210 | # Principal demo code: 211 | ```python 212 | from ui import * 213 | 214 | def main(): 215 | #⚠️ see readme.md in order to be aware about challenges and limitations! 216 | val = 50 217 | txt = String("Naïve UTF8 🥳") 218 | boolval = True 219 | multichoicevalue = String("First") 220 | colorvalue = String("#1C71D8") 221 | datevalue = String("2024-01-01") 222 | 223 | GUI = Server() 224 | 225 | POS = Position(1,1) 226 | POS2 = Position(1,350) 227 | POS3 = Position(32,512) 228 | POS4 = Position(512,16) 229 | 230 | combovalues = List[String]() 231 | for i in range(5): combovalues.append("Value "+str(i)) 232 | selection = 1 233 | 234 | while GUI.NeedNewRendition(): 235 | with GUI.Window("Debug window",POS): 236 | GUI.Text("Hello world 🔥") 237 | if GUI.Button("Button"): val = 50 238 | if GUI.Slider("Slider",val): 239 | print("Changed") 240 | GUI.TextInput("Input",txt) #⚠️ ```maxlength='32'``` attribute by default. 241 | GUI.ComboBox("ComboBox",combovalues,selection) 242 | GUI.Toggle(boolval,"Checkbox") 243 | 244 | with GUI.Window("Fun features",POS3): 245 | GUI.Text(GUI.Circle.Green + " Green circle") 246 | GUI.Text(GUI.Square.Blue + " Blue square") 247 | GUI.Text(GUI.Accessibility.Info + " Some icons") 248 | GUI.Text(GUI.Bold("Bold() ")+GUI.Highlight("Highlight()")) 249 | GUI.Text(GUI.Small("small") + " text") 250 | 251 | with GUI.Collapsible("Collapsible()"): 252 | GUI.Text("Content") 253 | 254 | with GUI.Window("More widgets",POS4): 255 | GUI.TextChoice("Multi Choice",multichoicevalue,"First","Second") 256 | GUI.Ticker("⬅️♾️ cycling left in a 128 pixels area",width=128) 257 | 258 | with GUI.Table(): 259 | for r in range(3): 260 | with GUI.Row(): 261 | for c in range(3): 262 | with GUI.Cell(): 263 | GUI.Text(str(r) + "," + str(c)) 264 | 265 | with GUI.ScrollableArea(123): 266 | GUI.Text(GUI.Bold("ScrollableArea()")) 267 | GUI.ColorSelector(colorvalue) 268 | GUI.NewLine() 269 | GUI.DateSelector(datevalue) #⚠️ format is unclear (see readme.md) 270 | for i in range(10): GUI.Text(str(i)) 271 | 272 | 273 | with GUI.Window("Values",POS2,CSSTitle="background-color:"+colorvalue): 274 | GUI.Text(txt) 275 | if selection < len(combovalues): #manual bound check for now 276 | GUI.Text(combovalues[selection]) 277 | with GUI.Tag("div","background-color:"+colorvalue): 278 | GUI.Text(colorvalue) 279 | GUI.Text(datevalue) 280 | with GUI.Tag("div","padding:0px;margin:0px;font-size:100"): 281 | GUI.Text("❤️‍🔥") 282 | GUI.Button("ok",CSS="font-size:32;background-color:"+colorvalue) 283 | ``` 284 | 285 |   286 | 287 | ## Features 288 | - 🎨 Themed with `CSS` 289 | - Default theme colors are kept familiar (🎁)🔥 290 | - `theme.css` where widgets have corresponding entries (class) 291 | - Customize individual widgets instances styles with keyword arguments 292 | - dom element style attribute 293 | - [The current styling system](#-the-current-styling-system) 294 | - helper function with variadic keyword arguments 295 | ```python 296 | #Example 297 | MyStyleOne = CSS(color="blue",`font-size`=32) 298 | var MyStyleTwo = CSS( 299 | `text-shadow` = "1px 1px 1px yellow", 300 | `font-size` = "32px", 301 | background = "linear-gradient(#ffff00, #f90)" 302 | ) 303 | ``` 304 | 305 | - 🆕 Keyboard event handler 306 | - Send events only if the user is not already interacting with a dom element. 307 | - That way, is is still possible to interact with an Input element independently. 308 | - ```Server.keyboard_handler``` is False by default, as additional safeguard 309 | ```python 310 | k = GUI.KeyDown() 311 | if k.isa[String](): #special keys (Backspace,ArrowLeft,..) 312 | if k.isa[Int](): #Normal characters: chr(k.take[Int]()) 313 | if k.isa[NoneType](): #No sent events or keyboard_handler is False 314 | ``` 315 | - see [demo_keyboard_and_css.mojo](./demo_keyboard_and_css.mojo) 316 | - Button 317 | - return True when clicked 318 | - ```CSS``` keyword argument, for the style attribute of the dom element (default: "") 319 | - Naïve UTF8 support 🥳 320 | - ⚠️ Label 321 | - can't be too long: behaviour unexpected ([Challenges for utf8 support](#challenges-for-utf8-support)) 322 | - two buttons with the same label lead to wrong event 323 | - usually, the first one will receive the event *(even if the second was clicked)* 324 | 325 | 326 | 327 | 328 | - TextInput 329 | - return ```True``` when UI interaction occured 330 | - mutate the argument (passed as inout) automatically 331 | - Naïve UTF8 support 🥳 332 | - ⚠️ need more work, see challenges sections 333 | - Additional untested safeguard: 334 | - DOM element is limited with the ```maxlength='32'``` attribute by default. 335 | - ```CSSBox``` keyword argument (default: "") 336 | - style attribute for the widget container (contains both label and input element) 337 | - todo: keyword arguments for label and input element 338 | - Text 339 | - Slider 340 | - return ```True``` when UI interaction occured 341 | - ```label:String``` not optional 342 | - mutate ```inout val:Int``` argument 343 | - ```min:Int=0, max:Int=100``` keyword arguments 344 | - ```CSSLabel``` keyword argument, style attribute of label (default: "") 345 | - ```CSSBox``` keyword argument, style attribute of widget container (default: "") 346 | - Windowing system (optional) 347 | - ```Minimized``` by clicking the ```-``` icon on the title bar 348 | (stored in Position struct) 349 | ```python 350 | #example, var MyWindow: Position, var GUI:Server 351 | with GUI.Window("The Title",MyWindow): 352 | if MyWindow.opened: GUI.Text("opened") 353 | ``` 354 | - ```Moved``` by dragging the title bar! 🥳 355 | - 🔥🔥🔥 ```Individually scaled/zoomed``` with mousewheel "scroll" on the titlebar ! 356 | - Positions and scales on the mojo side in user defined values (```Position(0,0)```) 357 | - ```Position(0,0).opened==False``` if window is minimized 358 | - ```CSSTitle``` keyword argument (default to empty) 359 | - Provides Additional css for the style attribute of the title box 360 | - Usefull for changing the title bar background-color, for example 361 | 362 | - Toggle 363 | - return ```True``` when UI interaction occured 364 | - Mutate a bool passed as argument (inout) 365 | - Similar to a checkbox 366 | - ComboBox 367 | - return ```True``` when UI interaction occured 368 | - ID is the inout address of the selection value 369 | - The selection value is the index of the selected value in the DynamicVector of selections 370 | - VariadicList support ! 🔥 371 | - ```ComboBox("Simple combobox",selection,"one","two","three")``` 372 | 373 | 374 | - Collapsible 375 | - Implemented as a with block 376 | - ```CSS``` keyword argument, to define the style attribute of the title part. 377 | 378 | - TextChoice 379 | - Inout string to store the selected value 380 | - Available choices as a variadic list 381 | - ```TextChoice("Label", selected, "First", "Second")``` 382 | 383 | - Ticker 384 | - Cycle left (⬅️♾️) in an area of a specifig width (200 pixels by default). 385 | - ```Ticker("Emojis are supported",width=64)``` 386 | 387 | - Table 388 | - Simple but it is a start! 389 | - Example: 390 | ```python 391 | with GUI.Table(): 392 | for r in range(3): 393 | with GUI.Row(): 394 | for c in range(3): 395 | with GUI.Cell(): 396 | GUI.Text(str(r) + "," + str(c)) 397 | ``` 398 | 399 | - ScrollableArea 🔥 400 | - ```height:Int = 128``` (pixels) 401 | - Example: 402 | ```python 403 | with GUI.ScrollableArea(50): 404 | for i in range(10): GUI.Text(str(i)) 405 | ``` 406 | 407 | - NewLine 408 | 409 | - 🎨 ColorSelector 410 | - inout string argument (example: ```var c:String = "#FF0000"```) 411 | - return ```True``` when UI interaction occured 412 | - ⌚ TimeSelector 413 | - inout string argument (example: ```var t:String = "23:59"```) 414 | - return ```True``` when UI interaction occured 415 | - 🗓️ DateSelector 416 | - inout string argument (example: ```var d:String = "2024-01-01"```) 417 | - return ```True``` when UI interaction occured 418 | - ⚠️ date format: 419 | - Not same for every machine? 420 | - Todo: unix timestamp 421 | 422 | - Tag 423 | - With block implementation, example: ```with GUI.Tag("div"):``` 424 | - ```style``` keyword argument (example: ```"background-color:orange;"```) 425 | 426 | - ```_additional_attributes``` keyword argument 427 | - specify attributes on the html DOM element (example: ```"class='otherclass'"```) 428 | 429 | - ❤️‍🔥 should_re_render 430 | - mark the rendition created by the current iteration as "out-of-date" 431 | - allows for re-rendering before sending a response 432 | - maximum is +- ```10``` by default 433 | - widgets also triggers re-renders (if event handled) 434 | - more testing is required ⚠️ 435 | - re-rendering means another iteration of the loop 436 | - could trigger more than one (until two successive ones are equals, need testing) 437 | - could not be done if already reached maximum (+-) 438 | 439 | 440 | 441 | - Add html manually: 442 | - `GUI.RawHtml("

Page

")` 443 | 444 | - ⬇️VerticalGrow and ➡️HorizontalGrow 445 | 446 | Flex divs implemented `with` context managers: 447 | ```mojo 448 | from ui import * 449 | def main(): 450 | GUI = Server() 451 | while GUI.NeedNewRendition(): 452 | with GUI.HorizontalGrow(): 453 | with GUI.VerticalGrow(): 454 | GUI.Text("First column") 455 | for i in range(3): 456 | GUI.Button(GUI.Digitize(i)) 457 | with GUI.VerticalGrow(): 458 | GUI.Text("Second column") 459 | for i in range(3): 460 | GUI.Button(repr(i)) 461 | ``` 462 | Result: 463 | ``` 464 | First column Second column 465 | 0️⃣ 0 466 | 1️⃣ 1 467 | 2️⃣ 2 468 | ``` 469 | 470 | - Expressivity: 471 | - Bold("Hello") -> **Hello** 472 | - Highlight("Hello") 473 | - Small("Hello") 474 | - Digitize(153) -> 1️⃣5️⃣3️⃣ 475 | - Square.Green 🟩 and Circle.Yellow 🟡 (Blue, Red, Black, Purple, Brown, Orange, Green, Yellow, White) 476 | - Accessibility.Info (Info ℹ️, Warning ⚠️, Success ✅) 477 | - Arrow.Right (Up ⬆️, Down ⬇️, Right ➡️, Left ⬅️) 478 | 479 |   480 | 481 | ## Mechanism 482 | The address of a value passed as an ```inout argument``` is used as a dom element id to check for events. 483 | 484 | For example, ```GUI.Slider("Slider",val)``` will generate an html input element with the id ```address of val```. 485 | 486 | The generated html is sent, and the page listen for any event on ``````. 487 | 488 | If an event occur on the page, it first check if the target element is marked with data-attribute (example: data-change). 489 | 490 | If it is the case, an url is generated, according to: 491 | - the e.target dom element id 492 | - the target value (depending on wich widget it represent) 493 | 494 | In this example: ```/slider_address_of_val/new_dom_element_value```. 495 | 496 | The page is then redirected to that url in order to "send" the event. 497 | 498 | On the mojo side, an event is "received" and 499 | the loop runs again. 500 | 501 | This time, the inout argument address will correspond to the current event url and the new value is assigned. 502 | 503 | Anything can be used to generate an id, require more thinking ! 504 | 505 | 506 |   507 | # How 'widgets' attempts(⚠️) to maintain the rendering up-to-date ? 508 | 509 | > Please make sure to read this section up to the end! 510 | 511 | When widgets handles an event, 512 | 513 | they also mark the current response as potentially based on out-of-date values. 514 | 515 | So the loop will runs again (one more time, if not reached limit), 516 | 517 | in order to reduce the probability of a page partially containing out-of-date values. 518 | 519 |   520 | 521 | And more, 522 | it should run as many times as needed until two successive renditions are equals! 523 | 524 | It is also possible to call *"should re-render anyway"* anywhere, 525 | 526 | making very interesting logic possible: ```GUI.should_re_render()```. 527 | 528 | There is an additional safeguard on the maximum number of renditions: ```+/- 10 by default```. 529 | 530 | *(in attempt to avoid infinite loops)* 531 | 532 | The idea is also to try to reduce the probabilities of interacting with out-of-date values: 533 | 534 | **Only the last rendition will be sent for interaction(🔥)!** 535 | 536 | 537 | 538 |   539 | 540 | (TODO: parametrize this) 541 | 542 | On the top right corner, 543 | 544 | the number of iterations done (more or less) to generate the page is shown for debugging. 545 | 546 | Note that two successive frames might not be enough, in somes cases, 547 | 548 | but it is a start and feedbacks are welcomed! 549 | 550 | Example illustrating the new features: [Simple todo list](demo_todos.mojo) 551 | 552 |   553 | 554 | #### Another example: 555 | ```python 556 | from ui import * 557 | def main(): 558 | GUI = Server() 559 | var color:String = "#3584E4" 560 | var favorite_color:String = "#33D17A" 561 | var pos = Position(128,128,2.0) 562 | while GUI.NeedNewRendition(): 563 | with GUI.Window("Test",pos,"background-color:"+color): 564 | if color == favorite_color: 565 | color = "#FFFFAA" 566 | GUI.should_re_render() 567 | GUI.Text(color) 568 | GUI.Text(favorite_color) 569 | if GUI.Button(" ✅ Set as favorite color "): 570 | favorite_color = color 571 | GUI.ColorSelector(color) 572 | ``` 573 | Note that if the favorite color is clicked two times in a row, 574 | 575 | The additional safeguard did prevent the infinite loop! *(see top right corner)* 576 | 577 | 578 | 579 |   580 | 581 | ## Characteristics: 582 | ### 🏜️ Less dependencies 583 | - Use a socket as PythonObject for now 584 | - To make it platform agnostic and ready to runs anywhere with little changes. 585 | 586 | ### 🛞 Non blocking event loop (default mode: blocking) 587 | - Usecase: if no request/event, custom user defined calculations on multiple workers. 588 | - Additionally, slowed down by a call to the time.sleep() 589 | 590 | ### 🏕️ Immediate mode vs retained 591 | - Works inside an explicitely user-defined loop 592 | - the user choose what should happen when there are no events. (not implemented yet) 593 | - The full dom is re-generated after each event 594 | ### 🎨 CSS and HTML 595 | - Interesting features: 596 | - audio/video playing 597 | - drag and drop 598 | - modals 599 | - more 600 | - To implement custom widgets 601 | - Both are user friendly and easy to learn 602 | 603 | 604 | 605 | 606 | 607 | 608 |   609 | ## Challenges for UTF8 support: 610 | The new value of an TextInput() is passed to mojo trough the URL (GET request). 611 | 612 | As a temporary solution, the new value is converted to UTF8 by javascript. 613 | 614 | On the mojo side, part the url is splitted by "-" and atol() is used with chr(). 615 | 616 | Example: ```/change_140732054756824/104-101-108-108-111``` 617 | 618 | ⚠️ 619 | - There is an unknown maximum size for new values! ( because URLs are size limited) 620 | - Currenly, the socket will only read 1024 bytes from the request. (can be changed) 621 | 622 | For theses reasons, an additional safeguard is provided (untested): 623 | - For the TextInput widget: 624 | - The input DOM element is limited with the ```maxlength='32'``` attribute by default. 625 | 626 | Need more thinking! any ideas ? 627 | 628 |   629 | 630 | # 🎨 The current styling system 631 | The idea is to provide choices of default CSS to act as a base and include theses inside the ```" 148 | 149 | fn should_re_render(mut self): self.re_render_current = True 150 | 151 | fn Span(mut self, arg:String): 152 | self.RawHtml(""+arg+"") 159 | self.RawHtml("") 160 | self.RawHtml("") 161 | 162 | fn NeedNewRendition(mut self) -> Bool: 163 | var ref_c = Pointer(to=self.client) 164 | var ref_s = Pointer(to=self.server) 165 | if self.send_response == True: 166 | self.total_renditions+=1 167 | var current_rendition:String=" " 168 | try: 169 | current_rendition = self.response 170 | var different = False 171 | if len(current_rendition)!=len(self.last_rendition): 172 | different=True 173 | else: 174 | for c in range(len(current_rendition)): 175 | if current_rendition[c] != self.last_rendition[c]: different = True 176 | 177 | if self.re_render_current: 178 | different=True 179 | self.re_render_current=False 180 | 181 | if self.total_renditions >10: 182 | different=False 183 | 184 | 185 | if different: 186 | self.last_rendition = current_rendition 187 | self._response_init() 188 | self.send_response=True 189 | # need counter to stop the loop after 190 | return True 191 | else: 192 | #self.response += "
"+str(self.total_renditions)+"
" 193 | self.response+= "" 194 | ref_c[][0].sendall(PythonObject(self.response).encode()) 195 | ref_c[][0].close() 196 | self.send_response=False 197 | self.last_rendition = current_rendition 198 | 199 | except e: print(e) 200 | 201 | 202 | try: #if error is raised in the block, no request or an error 203 | #could do loop here 204 | ref_c[] = ref_s[].accept() 205 | self.total_renditions = 0 206 | 207 | self._response_init() 208 | 209 | @parameter 210 | if self.exit_if_request_not_from_localhost: 211 | if ref_c[][1][0] != '127.0.0.1': 212 | print("Exit, request from: "+String(ref_c[][1][0])) 213 | return False 214 | 215 | var tmp_ = String(ref_c[][0].recv(1024).decode()) 216 | self.request = tmp_.split('\n')[0].split(" ") 217 | self.send_response=True 218 | except e: 219 | print(e) 220 | self.send_response=False 221 | if self.request_interval_second != 0: sleep(self.request_interval_second) 222 | 223 | return True #todo: should return self.Running: bool to exit the loop 224 | 225 | fn SetNoneRequest(mut self): 226 | self.request = List[String]() 227 | 228 | fn KeyDown(mut self)-> Variant[Int,String,NoneType]: 229 | if not self.keyboard_handler: return NoneType() 230 | # print(self.request.__repr__()) 231 | try: 232 | if 233 | self.request and 234 | self.request[1].startswith("/keyboard/down/") 235 | : 236 | var tmp = self.request[1].split("/") 237 | if len(tmp)>=3: 238 | if not String(tmp[3]).startswith("keydown-"): 239 | var res = atol(String(tmp[3])) 240 | self.SetNoneRequest() 241 | return res 242 | else: 243 | var res = String(tmp[3]).split("keydown-")[1] 244 | self.SetNoneRequest() 245 | return res 246 | except e: 247 | print(e) 248 | return NoneType() #Int(0) 249 | return NoneType() #Int(0) 250 | 251 | 252 | def Button(mut self,txt:String,CSS:String="") ->Bool: 253 | var id:String = "" 254 | var ptr = txt.unsafe_ptr() 255 | for c in range(len(txt)): 256 | id+=String(ptr[c]) 257 | id+="-" 258 | _=txt 259 | self.response += String("
", txt, "
") 260 | if self.request and self.request[1] == "/click_"+id: 261 | self.should_re_render() 262 | self.SetNoneRequest() 263 | return True 264 | return False 265 | 266 | def Toggle[L:MutableOrigin](mut self,ref[L]val:Bool,label:String)->Bool: 267 | var res:Bool = False 268 | var val_repr:String = "ToggleOff_" 269 | var id:String = String(self.ID(val)) 270 | if self.request and self.request[1] == "/click_"+id: 271 | val = not val 272 | self.should_re_render() 273 | self.SetNoneRequest() 274 | res = True 275 | 276 | if val: val_repr = "ToggleOn_" 277 | self.response += String("
", label, "
") 278 | return res 279 | 280 | fn Text(mut self:Self, txt:String): 281 | self.response += "
"+txt+"
" 282 | 283 | def Window( 284 | mut self, 285 | name: String, 286 | mut pos:Position, 287 | CSSTitle:String="" 288 | )->Window[__origin_of(self), __origin_of(pos)]: 289 | return Window[__origin_of(self), __origin_of(pos)]( 290 | self, 291 | name, 292 | pos, 293 | CSSTitle 294 | ) 295 | 296 | def Slider[L:MutableOrigin](mut self,label:String,ref[L]val:Int, min:Int = 0, max:Int = 100,CSSLabel:String="",CSSBox:String="")->Bool: 297 | #Todo: if new_value > max: new_value = max, check if min
", label, " ",String(val),"
") 306 | self.response += "" 307 | self.response += "" 308 | return retval 309 | 310 | fn ID[T:AnyType](mut self, ref[_]arg:T)->Int: 311 | return UnsafePointer(to=arg).__int__() 312 | 313 | fn TextInput[maxlength:Int=32]( 314 | mut self, 315 | label:String, 316 | mut val:String, 317 | CSSBox:String="", 318 | )->Bool: 319 | var ret_val = False 320 | try: 321 | var id:String = String(self.ID(val)) 322 | var tmp2 = "/change_"+id+"/" 323 | if self.request and self.request[1] == tmp2: 324 | val = "" #empty 325 | self.should_re_render() 326 | self.SetNoneRequest() 327 | ret_val = True 328 | else: 329 | if self.request and self.request[1].startswith(tmp2): 330 | var tmp3 = self.request[1].split(tmp2)[1].split("-") 331 | val = String() 332 | for i in range(len(tmp3)): 333 | val.append_byte(Int(tmp3[i])) 334 | self.should_re_render() 335 | self.SetNoneRequest() 336 | ret_val = True 337 | 338 | self.response += "
" 339 | if label!="": 340 | self.response += ""+label+"" 341 | self.response += "" 342 | self.response += "
" 343 | except e: print("Error TextInput widget: "+ String(e)) 344 | return ret_val 345 | 346 | def ComboBox[L:MutableOrigin](mut self,label:String,values:List[String],ref[L]selection:Int)->Bool: 347 | var ret_val = False 348 | var id:String = String(self.ID(selection)) 349 | var tmp2 = "/combobox_"+id+"/" 350 | if self.request and self.request[1].startswith(tmp2): 351 | selection = atol(self.request[1].split(tmp2)[1]) 352 | self.should_re_render() 353 | self.SetNoneRequest() 354 | ret_val = True 355 | 356 | 357 | self.response += "
" 358 | self.response += ""+label+" " 359 | self.response += "" 365 | self.response += "
" 366 | return ret_val 367 | 368 | def ComboBox[L:MutableOrigin](mut self,label:String,ref[L]selection:Int,*selections:String)->Bool: 369 | var ret_val = False 370 | var id:String = String(self.ID(selection)) 371 | var tmp2 = "/combobox_"+id+"/" 372 | if self.request and self.request[1].startswith(tmp2): 373 | selection = atol(self.request[1].split(tmp2)[1]) 374 | self.should_re_render() 375 | self.SetNoneRequest() 376 | ret_val = True 377 | 378 | 379 | self.response += "
" 380 | self.response += ""+label+" " 381 | self.response += "" 387 | self.response += "
" 388 | return ret_val 389 | 390 | def TextChoice[L:MutableOrigin](mut self, label:String,ref[L]selected: String, *selections:String): 391 | var id:String = String(self.ID(selected)) 392 | var tmp2 = "/text_choice/"+id+"/" 393 | if self.request and self.request[1].startswith(tmp2): 394 | try: 395 | result = atol(self.request[1].split(tmp2)[1]) 396 | if result >= len(selections): 397 | raise Error("Selected index >= len(selections)") 398 | selected = String(selections[result]) 399 | self.should_re_render() 400 | self.SetNoneRequest() 401 | 402 | except e: print("Error TextChoice widget: "+String(e)) 403 | 404 | self.response+=""" 405 |
406 | """ + label + "" 407 | for i in range(len(selections)): 408 | var current = String(selections[i]) 409 | var url = "/text_choice/"+id+"/"+String(i) 410 | if current == selected: 411 | self.response+= "▪️" + (current)+'
' 412 | else: 413 | self.response+= "▪️" + (current)+'
' 414 | self.response += "
" 415 | 416 | def Bold(mut self, t:String)->String: return ""+t+"" 417 | def Highlight(mut self, t:String)->String: return ""+t+"" 418 | def Small(mut self, t:String)->String: return ""+t+"" 419 | def _Ticker(mut self,t:String)->String: 420 | return ""+t+"" 421 | def Ticker(mut self,t:String,width:Int=200): 422 | self.response+="
"+t+"
" 423 | 424 | def Digitize(mut self, number: Int)->String : 425 | var digits = List[String]("0️⃣","1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣","8️⃣","9️⃣") 426 | tmp = String(number) 427 | var res:String = "" 428 | for i in range(len(tmp)): 429 | res+=digits[(ord(tmp[i])-48)] 430 | return(res) 431 | 432 | def Collapsible(mut self,title:String,CSS:String="")->Collapsible[__origin_of(self)]: 433 | return Collapsible( 434 | self, 435 | title, 436 | CSS 437 | ) 438 | 439 | def Table[L:MutableOrigin](ref[L]self)->WithTag[L]: 440 | return WithTag( 441 | self, 442 | "table", 443 | "margin:4px;border:1px solid black;", 444 | " " 445 | ) 446 | 447 | def Row[L:MutableOrigin](ref[L]self)->WithTag[L]: 448 | return WithTag( 449 | self, 450 | "tr", 451 | "border:1px solid black;", 452 | " " 453 | ) 454 | 455 | def Cell[L:MutableOrigin](ref[L]self)->WithTag[L]: 456 | return WithTag( 457 | self, 458 | "td", 459 | "border:1px solid black;", 460 | " " 461 | ) 462 | 463 | def ScrollableArea[L:MutableOrigin](ref[L]self,height:Int=128)->ScrollableArea[L]: 464 | return ScrollableArea(self,height) 465 | 466 | def ColorSelector(mut self, mut arg:String)->Bool: 467 | var ret_val = False 468 | var id:String = String(self.ID(arg)) 469 | var tmp3 = "/colorselector_"+id+"/" 470 | if self.request and self.request[1].startswith(tmp3): 471 | try: 472 | result = "#"+String(self.request[1].split(tmp3)[1]) 473 | arg=result 474 | self.should_re_render() 475 | self.SetNoneRequest() 476 | ret_val = True 477 | except e: print("Error ColorSelector widget: "+String(e)) 478 | self.response += "" 479 | return ret_val 480 | 481 | def TimeSelector(mut self, mut arg:String)->Bool: 482 | var ret_val = False 483 | var id = String(self.ID(arg)) 484 | var tmp3 = "/timeselector_"+id+"/" 485 | if self.request and self.request[1].startswith(tmp3): 486 | try: 487 | result = self.request[1].split(tmp3)[1] 488 | arg=result 489 | self.should_re_render() 490 | self.SetNoneRequest() 491 | ret_val = True 492 | except e: print("Error TimeSelector widget: "+String(e)) 493 | 494 | self.response += "" 495 | return ret_val 496 | 497 | #⚠️ not sure at all about the date format (see readme.md) 498 | def DateSelector(mut self, mut arg:String)->Bool: 499 | var ret_val = False 500 | var id = String(self.ID(arg)) 501 | var tmp3 = "/dateselector_"+id+"/" 502 | if self.request and self.request[1].startswith(tmp3): 503 | try: 504 | result = String(self.request[1].split(tmp3)[1]) 505 | arg=result 506 | self.should_re_render() 507 | self.SetNoneRequest() 508 | ret_val = True 509 | except e: print("Error DateSelector widget: "+String(e)) 510 | self.response += "" 511 | return ret_val 512 | def NewLine(mut self): self.response+="
" 513 | 514 | fn Tag[L:MutableOrigin]( 515 | ref[L]self, 516 | tag:String, 517 | style:String="", 518 | _additional_attributes:String=" " 519 | ) -> WithTag[L] #) -> WithTag[MutableStaticLifetime] 520 | : 521 | return WithTag( 522 | self, 523 | tag, 524 | style, 525 | _additional_attributes 526 | ) 527 | 528 | fn AudioBase64WavSpecial(mut self, id: Int, volume:Int, b64wav:String): 529 | self.RawHtml("") 532 | 533 | def CustomEvent(mut self,unique_name:String)->Optional[String]: 534 | var ret = Optional[String](None) 535 | if self.request and self.request[1].startswith("/custom_event_"+unique_name): 536 | ret = String(self.request[1].split("/")[2]) 537 | self.SetNoneRequest() 538 | self.should_re_render() 539 | self.SetNoneRequest() 540 | 541 | return ret 542 | 543 | 544 | #TODO: add css, align, .. 545 | def HorizontalGrow(mut self)->WithTag[__origin_of(self)]: 546 | return WithTag(self, "div","display: flex;flex-direction: row;", "") 547 | def VerticalGrow(mut self)->WithTag[__origin_of(self)]: 548 | return WithTag(self, "div","display: flex;flex-direction: column;", "") 549 | 550 | @value 551 | struct ScrollableArea[L:MutableOrigin]: 552 | var server: Pointer[Server, L] 553 | var height: Int 554 | fn __init__(out self, ref[L]ui:Server, height: Int): 555 | self.server = Pointer(to=ui) 556 | self.height = height 557 | fn __enter__(self): 558 | self.server[].response += "
" 559 | 560 | fn __exit__( self): _=self.close() 561 | fn __exit__( self, err:Error)->Bool: return self.close() 562 | 563 | fn close(self) -> Bool: 564 | self.server[].response += "
" 565 | return True 566 | 567 | @value 568 | struct Collapsible[L:MutableOrigin]: 569 | var title: String 570 | var server: Pointer[Server, L] 571 | var CSS: String 572 | fn __init__(out self, ref[L]ui:Server, title:String, css:String): 573 | self.title = title 574 | self.CSS = css 575 | self.server = Pointer(to=ui) 576 | fn __enter__(self): 577 | self.server[].response += "
"+self.title+"" 578 | 579 | fn __exit__( self): _=self.close() 580 | fn __exit__( self, err:Error)->Bool: return self.close() 581 | 582 | fn close(self) -> Bool: 583 | self.server[].response += "
" 584 | return True 585 | 586 | 587 | @value 588 | struct WithTag[L:MutableOrigin]: 589 | var server: Pointer[Server, L] 590 | var tag:String 591 | var style:String 592 | var _additional_attributes:String 593 | fn __init__(out self, ref[L]ui: Server, tag: String, style:String,_additional_attributes:String): 594 | self.server = Pointer(to=ui) 595 | self.tag = tag 596 | self.style = style 597 | self._additional_attributes = _additional_attributes 598 | fn __enter__(self): 599 | self.server[].response += "<"+self.tag+" "+self._additional_attributes+" style='" + self.style + "'>" 600 | fn __exit__( self): self.close() 601 | fn __exit__( self, err:Error)->Bool: 602 | self.close() 603 | print(err) 604 | return False 605 | fn close(self): 606 | self.server[].response += "" 607 | 608 | @value 609 | struct Window[ 610 | L: MutableOrigin, 611 | LPOS: MutableOrigin, 612 | ]: 613 | var server: Pointer[Server, L] 614 | var name: String 615 | var pos: Pointer[Position, LPOS] 616 | var titlecss: String 617 | fn __init__(out self, ref[L]ui: Server, name:String,ref[LPOS] pos: Position,titlecss:String): 618 | self.server = Pointer(to=ui) 619 | self.name = name 620 | self.pos = Pointer(to=pos) 621 | self.titlecss = titlecss 622 | fn __enter__(self) -> Pointer[Position, LPOS]: 623 | try: 624 | var id = String(self.pos)#str(hash(self.name._as_ptr(),len(self.name))) 625 | var positions:String = "" 626 | var req = self.server[].request 627 | 628 | if req and req[1].startswith("/window_scale_"+id): 629 | var val = req[1].split("/window_scale_"+id)[1].split("/") 630 | if val[1] == "1": 631 | self.pos[].scale+=0.1 632 | else: 633 | if self.pos[].scale >=0.2: 634 | self.pos[].scale-=0.1 635 | self.server[].request = List[String]() 636 | elif req and req[1].startswith("/window_"+id): 637 | var val = req[1].split("/window_"+id)[1].split("/") 638 | self.pos[].x += atol(String(val[1])) #todo try: block for atol 639 | self.pos[].y += atol(String(val[2])) 640 | self.server[].request = List[String]() 641 | elif req and req[1].startswith("/click_/window_toggle_"+id): 642 | self.pos[].opened = not self.pos[].opened 643 | self.server[].request = List[String]() 644 | positions += "left:"+String(self.pos[].x)+"px;" 645 | positions += "top:"+String(self.pos[].y)+"px;" 646 | var scale:String = String(self.pos[].scale) 647 | self.server[].response += "
" 648 | self.server[].response += "
❌ " + self.name + " 
" 649 | var opened = String(" ") 650 | if not self.pos[].opened: opened = "hidden" 651 | self.server[].response += "
" 652 | except e: print("Window __enter__ widget:"+String(e)) 653 | return self.pos 654 | fn __exit__( self): _=self.close() 655 | fn close(self) -> Bool: 656 | self.server[].response += "
" 657 | return True 658 | fn __exit__( self, err:Error)->Bool: return self.close() 659 | 660 | alias CSS_T=Variant[String,Int] 661 | fn CSS(**kwargs: CSS_T) -> String: 662 | var res:String =";" 663 | try: 664 | for i in kwargs: 665 | var kw=i[] 666 | if kwargs[kw].isa[String](): 667 | res+= kw+":" 668 | res+= String(kwargs[kw].take[String]()) +";" 669 | if kwargs[kw].isa[Int](): 670 | res+= kw+":" 671 | res+= String(kwargs[kw].take[Int]()) +";" 672 | except e: print("CSS function"+String(e)) 673 | return res 674 | --------------------------------------------------------------------------------