├── 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 += "