Quellcode durchsuchen

Merge branch 'main' into globals

# Conflicts:
#	main.py
#	nicegui/elements/element.py
#	nicegui/elements/group.py
#	nicegui/elements/page.py
#	nicegui/run.py
#	nicegui/ui.py
Falko Schindler vor 3 Jahren
Ursprung
Commit
a727e14fd2
46 geänderte Dateien mit 259 neuen und 218 gelöschten Zeilen
  1. 1 0
      README.md
  2. 113 86
      main.py
  3. 1 1
      nicegui/config.py
  4. 0 1
      nicegui/elements/bool_element.py
  5. 0 6
      nicegui/elements/button.py
  6. 21 2
      nicegui/elements/card.py
  7. 0 1
      nicegui/elements/choice_element.py
  8. 14 2
      nicegui/elements/column.py
  9. 0 4
      nicegui/elements/custom_example.py
  10. 0 5
      nicegui/elements/custom_view.py
  11. 0 3
      nicegui/elements/dialog.py
  12. 21 10
      nicegui/elements/element.py
  13. 0 2
      nicegui/elements/float_element.py
  14. 0 2
      nicegui/elements/group.py
  15. 0 4
      nicegui/elements/html.py
  16. 0 1
      nicegui/elements/icon.py
  17. 0 7
      nicegui/elements/image.py
  18. 0 5
      nicegui/elements/joystick.py
  19. 0 7
      nicegui/elements/label.py
  20. 0 3
      nicegui/elements/line_plot.py
  21. 0 1
      nicegui/elements/link.py
  22. 0 4
      nicegui/elements/log.py
  23. 0 3
      nicegui/elements/markdown.py
  24. 1 4
      nicegui/elements/menu.py
  25. 25 0
      nicegui/elements/menu_item.py
  26. 1 3
      nicegui/elements/notify.py
  27. 0 2
      nicegui/elements/number.py
  28. 0 3
      nicegui/elements/plot.py
  29. 0 1
      nicegui/elements/radio.py
  30. 14 2
      nicegui/elements/row.py
  31. 0 1
      nicegui/elements/scene.py
  32. 0 1
      nicegui/elements/scene_object3d.py
  33. 9 8
      nicegui/elements/scene_objects.py
  34. 0 3
      nicegui/elements/select.py
  35. 0 1
      nicegui/elements/string_element.py
  36. 0 7
      nicegui/elements/svg.py
  37. 0 1
      nicegui/elements/toggle.py
  38. 0 1
      nicegui/elements/upload.py
  39. 0 7
      nicegui/elements/value_element.py
  40. 0 2
      nicegui/lifecycle.py
  41. 16 0
      nicegui/routes.py
  42. 12 3
      nicegui/run.py
  43. 4 2
      nicegui/static/templates/local/materialdesignicons/iconfont/README.md
  44. 0 3
      nicegui/timer.py
  45. 3 2
      nicegui/ui.py
  46. 3 1
      nicegui/utils.py

+ 1 - 0
README.md

@@ -59,6 +59,7 @@ You can call `ui.run()` with optional arguments for some high-level configuratio
 - `favicon` (default: `'favicon.ico'`)
 - `favicon` (default: `'favicon.ico'`)
 - `reload`: automatically reload the ui on file changes (default: `True`)
 - `reload`: automatically reload the ui on file changes (default: `True`)
 - `show`: automatically open the ui in a browser tab (default: `True`)
 - `show`: automatically open the ui in a browser tab (default: `True`)
+- `uvicorn_logging_level`: logging level for uvicorn server (default: `'warning'`)
 - `interactive`: used internally when run in interactive Python shell (default: `False`)
 - `interactive`: used internally when run in interactive Python shell (default: `False`)
 
 
 ## Docker
 ## Docker

+ 113 - 86
main.py

@@ -8,7 +8,7 @@ import docutils.core
 import re
 import re
 import asyncio
 import asyncio
 from nicegui.elements.markdown import Markdown
 from nicegui.elements.markdown import Markdown
-from nicegui.elements.element import Element
+from nicegui.elements.element import Design, Element
 from nicegui.globals import page_stack
 from nicegui.globals import page_stack
 
 
 # add docutils css to webpage
 # add docutils css to webpage
@@ -16,11 +16,9 @@ page_stack[0].head_html += docutils.core.publish_parts('', writer_name='html')['
 
 
 @contextmanager
 @contextmanager
 def example(content: Union[Element, str]):
 def example(content: Union[Element, str]):
-
     callFrame = inspect.currentframe().f_back.f_back
     callFrame = inspect.currentframe().f_back.f_back
     begin = callFrame.f_lineno
     begin = callFrame.f_lineno
     with ui.row().classes('flex w-full'):
     with ui.row().classes('flex w-full'):
-
         if isinstance(content, str):
         if isinstance(content, str):
             ui.markdown(content).classes('mr-8 w-4/12')
             ui.markdown(content).classes('mr-8 w-4/12')
         else:
         else:
@@ -35,7 +33,8 @@ def example(content: Union[Element, str]):
                 ui.label(content.__name__).classes('text-h5')
                 ui.label(content.__name__).classes('text-h5')
 
 
         with ui.card().classes('mt-12 w-2/12'):
         with ui.card().classes('mt-12 w-2/12'):
-            yield
+            with ui.column():
+                yield
         callFrame = inspect.currentframe().f_back.f_back
         callFrame = inspect.currentframe().f_back.f_back
         end = callFrame.f_lineno
         end = callFrame.f_lineno
         code = inspect.getsource(sys.modules[__name__])
         code = inspect.getsource(sys.modules[__name__])
@@ -43,6 +42,8 @@ def example(content: Union[Element, str]):
         code = [l[4:] for l in code]
         code = [l[4:] for l in code]
         code.insert(0, '```python')
         code.insert(0, '```python')
         code.insert(1, 'from nicegui import ui')
         code.insert(1, 'from nicegui import ui')
+        if code[2].split()[0] not in ['from', 'import']:
+            code.insert(2, '')
         code.append('')
         code.append('')
         code.append('ui.run()')
         code.append('ui.run()')
         code.append('```')
         code.append('```')
@@ -75,73 +76,16 @@ with ui.row().classes('flex w-full'):
                 ui.label('Output:')
                 ui.label('Output:')
                 output = ui.label('').classes('text-bold')
                 output = ui.label('').classes('text-bold')
 
 
-design = '''### Styling & Design
-
-NiceGUI uses the [Quasar Framework](https://quasar.dev/) and hence has its full design power.
-Each NiceGUI element provides a `props` method whose content is passed [to the Quasar component](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components):
-Have a look at [the Quasar documentation](https://quasar.dev/vue-components/button#design) for all styling props.
-You can also apply [Tailwind](https://tailwindcss.com/) utility classes with the `classes` method. 
-
-If you really need to apply CSS, you can use the `styles` method. Here the delimiter is `;` instead of a blank space.
-'''
-with example(design):
-
-    ui.radio(['x', 'y', 'z']).props('inline color=green')
-    ui.button().props('icon=touch_app outline round').classes('shadow-lg ml-14')
-
-binding = '''### Bindings
-
-With help of the [binding](https://pypi.org/project/binding/) package NiceGUI is able to directly bind UI elements to models.
-Binding is possible for UI element properties like text, value or visibility and for model properties that are (nested) class attributes.
-
-Each element provides methods like `bind_value` and `bind_visibility` to create a two-way binding with the corresponding property.
-To define a one-way binding use the `_from` and `_to` variants of these methods.
-Just pass a property of the model as parameter to these methods to create the binding.
-'''
-with example(binding):
-
-    class Demo:
-
-        def __init__(self):
-            self.number = 1
-
-    demo = Demo()
-    v = ui.checkbox('visible', value=True)
-    with ui.column().bind_visibility_from(v.value):
-        ui.slider(min=1, max=3).bind_value(demo.number)
-        ui.toggle({1: 'a', 2: 'b', 3: 'c'}).bind_value(demo.number)
-        ui.number().bind_value(demo.number)
-
-
-with example(ui.timer):
-    from datetime import datetime
-
-    with ui.row().classes('items-center'):
-        clock = ui.label()
-        t = ui.timer(interval=0.1, callback=lambda: clock.set_text(datetime.now().strftime("%X.%f")[:-5]))
-        ui.checkbox('active').bind_value(t.active)
-
-    with ui.row():
-        def lazy_update():
-            new_text = datetime.now().strftime('%X.%f')[:-5]
-            if lazy_clock.text[:8] == new_text[:8]:
-                return False
-            lazy_clock.text = new_text
-        lazy_clock = ui.label()
-        ui.timer(interval=0.1, callback=lazy_update)
 
 
 with example(ui.label):
 with example(ui.label):
-
     ui.label('some label')
     ui.label('some label')
 
 
 with example(ui.image):
 with example(ui.image):
-
     ui.image('http://placeimg.com/640/360/tech')
     ui.image('http://placeimg.com/640/360/tech')
     base64 = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QCMRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAACKgAwAEAAAAAQAAACMAAAAA/8IAEQgAIwAiAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAMCBAEFAAYHCAkKC//EAMMQAAEDAwIEAwQGBAcGBAgGcwECAAMRBBIhBTETIhAGQVEyFGFxIweBIJFCFaFSM7EkYjAWwXLRQ5I0ggjhU0AlYxc18JNzolBEsoPxJlQ2ZJR0wmDShKMYcOInRTdls1V1pJXDhfLTRnaA40dWZrQJChkaKCkqODk6SElKV1hZWmdoaWp3eHl6hoeIiYqQlpeYmZqgpaanqKmqsLW2t7i5usDExcbHyMnK0NTV1tfY2drg5OXm5+jp6vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAQIAAwQFBgcICQoL/8QAwxEAAgIBAwMDAgMFAgUCBASHAQACEQMQEiEEIDFBEwUwIjJRFEAGMyNhQhVxUjSBUCSRoUOxFgdiNVPw0SVgwUThcvEXgmM2cCZFVJInotIICQoYGRooKSo3ODk6RkdISUpVVldYWVpkZWZnaGlqc3R1dnd4eXqAg4SFhoeIiYqQk5SVlpeYmZqgo6SlpqeoqaqwsrO0tba3uLm6wMLDxMXGx8jJytDT1NXW19jZ2uDi4+Tl5ufo6ery8/T19vf4+fr/2wBDAAwMDAwMDBUMDBUeFRUVHikeHh4eKTQpKSkpKTQ+NDQ0NDQ0Pj4+Pj4+Pj5LS0tLS0tXV1dXV2JiYmJiYmJiYmL/2wBDAQ8QEBkXGSsXFytnRjlGZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2f/2gAMAwEAAhEDEQAAAeqBgCIareozvbaK3avBqa52teT6He3z0TqCUZa22r//2gAIAQEAAQUCaVVKTjGnLFqSqqlGuciX+87YgM8ScWhAx5KWUJUdJClKadMye6O//9oACAEDEQE/AUxI86A0ynfb/9oACAECEQE/ASaYZBLxpKNinFh2dv8A/9oACAEBAAY/AmUniHVXxfVx7ZIP9x0GlOJdfa+BeVentkSWR66jsI1HUfF+f4l1UykiqR/CypAorg6n/hvuH5nv/8QAMxABAAMAAgICAgIDAQEAAAILAREAITFBUWFxgZGhscHw0RDh8SAwQFBgcICQoLDA0OD/2gAIAQEAAT8hrchP08Nlp8V+7MHK/wCcEXw8q94vkT4K5DD0fpsJBFkwYvy/8cJBuuX7l82UhL9HmlzVKCOfi+3/ADe6Z2jgePxcMYN/xxYQtAu8UCj/ALXDvn/sBxRB/g3/AL//2gAMAwEAAhEDEQAAEE5gPHEUEAP/xAAzEQEBAQADAAECBQUBAQABAQkBABEhMRBBUWEgcfCRgaGx0cHh8TBAUGBwgJCgsMDQ4P/aAAgBAxEBPxAN4PZaNJuOW/g//9oACAECEQE/EAGt2fwmfzBp3X8P/9oACAEBAAE/ELGubg74j5M+RuAgxMrE4g5c4qAjQh1Oh9GL3/xggJDuHs5H2fY1rQIGDISTZ3KuGYzkk8dSkh4Ah8TJ8c0SsIco+yPRD76/486QSwOdnIpjvmvjAQ8pEx4ixlVcDldAdtawTzP5CSqs1wAPeJDMz0nwvHVlRSYTI1ic6b58RUC4kuSTXmFOJuxknJgsgDQMkjQgj/gCBHee6QjzflUA4/5//9k='
     base64 = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QCMRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAACKgAwAEAAAAAQAAACMAAAAA/8IAEQgAIwAiAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAMCBAEFAAYHCAkKC//EAMMQAAEDAwIEAwQGBAcGBAgGcwECAAMRBBIhBTETIhAGQVEyFGFxIweBIJFCFaFSM7EkYjAWwXLRQ5I0ggjhU0AlYxc18JNzolBEsoPxJlQ2ZJR0wmDShKMYcOInRTdls1V1pJXDhfLTRnaA40dWZrQJChkaKCkqODk6SElKV1hZWmdoaWp3eHl6hoeIiYqQlpeYmZqgpaanqKmqsLW2t7i5usDExcbHyMnK0NTV1tfY2drg5OXm5+jp6vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAQIAAwQFBgcICQoL/8QAwxEAAgIBAwMDAgMFAgUCBASHAQACEQMQEiEEIDFBEwUwIjJRFEAGMyNhQhVxUjSBUCSRoUOxFgdiNVPw0SVgwUThcvEXgmM2cCZFVJInotIICQoYGRooKSo3ODk6RkdISUpVVldYWVpkZWZnaGlqc3R1dnd4eXqAg4SFhoeIiYqQk5SVlpeYmZqgo6SlpqeoqaqwsrO0tba3uLm6wMLDxMXGx8jJytDT1NXW19jZ2uDi4+Tl5ufo6ery8/T19vf4+fr/2wBDAAwMDAwMDBUMDBUeFRUVHikeHh4eKTQpKSkpKTQ+NDQ0NDQ0Pj4+Pj4+Pj5LS0tLS0tXV1dXV2JiYmJiYmJiYmL/2wBDAQ8QEBkXGSsXFytnRjlGZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2f/2gAMAwEAAhEDEQAAAeqBgCIareozvbaK3avBqa52teT6He3z0TqCUZa22r//2gAIAQEAAQUCaVVKTjGnLFqSqqlGuciX+87YgM8ScWhAx5KWUJUdJClKadMye6O//9oACAEDEQE/AUxI86A0ynfb/9oACAECEQE/ASaYZBLxpKNinFh2dv8A/9oACAEBAAY/AmUniHVXxfVx7ZIP9x0GlOJdfa+BeVentkSWR66jsI1HUfF+f4l1UykiqR/CypAorg6n/hvuH5nv/8QAMxABAAMAAgICAgIDAQEAAAILAREAITFBUWFxgZGhscHw0RDh8SAwQFBgcICQoLDA0OD/2gAIAQEAAT8hrchP08Nlp8V+7MHK/wCcEXw8q94vkT4K5DD0fpsJBFkwYvy/8cJBuuX7l82UhL9HmlzVKCOfi+3/ADe6Z2jgePxcMYN/xxYQtAu8UCj/ALXDvn/sBxRB/g3/AL//2gAMAwEAAhEDEQAAEE5gPHEUEAP/xAAzEQEBAQADAAECBQUBAQABAQkBABEhMRBBUWEgcfCRgaGx0cHh8TBAUGBwgJCgsMDQ4P/aAAgBAxEBPxAN4PZaNJuOW/g//9oACAECEQE/EAGt2fwmfzBp3X8P/9oACAEBAAE/ELGubg74j5M+RuAgxMrE4g5c4qAjQh1Oh9GL3/xggJDuHs5H2fY1rQIGDISTZ3KuGYzkk8dSkh4Ah8TJ8c0SsIco+yPRD76/486QSwOdnIpjvmvjAQ8pEx4ixlVcDldAdtawTzP5CSqs1wAPeJDMz0nwvHVlRSYTI1ic6b58RUC4kuSTXmFOJuxknJgsgDQMkjQgj/gCBHee6QjzflUA4/5//9k='
     ui.image(base64).style('width:30px')
     ui.image(base64).style('width:30px')
 
 
 with example(ui.svg):
 with example(ui.svg):
-
     svg_content = '''
     svg_content = '''
         <svg viewBox="0 0 200 200" width="100" height="100" xmlns="http://www.w3.org/2000/svg">
         <svg viewBox="0 0 200 200" width="100" height="100" xmlns="http://www.w3.org/2000/svg">
         <circle cx="100" cy="100" r="78" fill="yellow" stroke="black" stroke-width="3" />
         <circle cx="100" cy="100" r="78" fill="yellow" stroke="black" stroke-width="3" />
@@ -159,7 +103,6 @@ Use [Quasar classes](https://quasar.dev/vue-components/img) for positioning and
 To overlay an svg, make the `viewBox` exactly the size of the image and provide `100%` width/height to match the actual rendered size.
 To overlay an svg, make the `viewBox` exactly the size of the image and provide `100%` width/height to match the actual rendered size.
 '''
 '''
 with example(overlay):
 with example(overlay):
-
     with ui.image('http://placeimg.com/640/360/nature'):
     with ui.image('http://placeimg.com/640/360/nature'):
         ui.label('nice').classes('absolute-bottom text-subtitle2 text-center')
         ui.label('nice').classes('absolute-bottom text-subtitle2 text-center')
 
 
@@ -171,15 +114,12 @@ with example(overlay):
         ui.svg(svg_content).style('background:transparent')
         ui.svg(svg_content).style('background:transparent')
 
 
 with example(ui.markdown):
 with example(ui.markdown):
-
     ui.markdown('### Headline\nWith hyperlink to [GitHub](https://github.com/zauberzeug/nicegui).')
     ui.markdown('### Headline\nWith hyperlink to [GitHub](https://github.com/zauberzeug/nicegui).')
 
 
 with example(ui.html):
 with example(ui.html):
-
     ui.html('<p>demo paragraph in <strong>html</strong></p>')
     ui.html('<p>demo paragraph in <strong>html</strong></p>')
 
 
 with example(ui.button):
 with example(ui.button):
-
     def button_increment():
     def button_increment():
         global button_count
         global button_count
         button_count += 1
         button_count += 1
@@ -190,26 +130,22 @@ with example(ui.button):
     button_result = ui.label('pressed: 0')
     button_result = ui.label('pressed: 0')
 
 
 with example(ui.checkbox):
 with example(ui.checkbox):
-
     ui.checkbox('check me', on_change=lambda e: checkbox_state.set_text(e.value))
     ui.checkbox('check me', on_change=lambda e: checkbox_state.set_text(e.value))
     with ui.row():
     with ui.row():
         ui.label('the checkbox is:')
         ui.label('the checkbox is:')
         checkbox_state = ui.label('False')
         checkbox_state = ui.label('False')
 
 
 with example(ui.switch):
 with example(ui.switch):
-
     ui.switch('switch me', on_change=lambda e: switch_state.set_text("ON" if e.value else'OFF'))
     ui.switch('switch me', on_change=lambda e: switch_state.set_text("ON" if e.value else'OFF'))
     with ui.row():
     with ui.row():
         ui.label('the switch is:')
         ui.label('the switch is:')
         switch_state = ui.label('OFF')
         switch_state = ui.label('OFF')
 
 
 with example(ui.slider):
 with example(ui.slider):
-
     slider = ui.slider(min=0, max=100, value=50).props('label')
     slider = ui.slider(min=0, max=100, value=50).props('label')
     ui.label().bind_text_from(slider.value)
     ui.label().bind_text_from(slider.value)
 
 
 with example(ui.input):
 with example(ui.input):
-
     ui.input(
     ui.input(
         label='Text',
         label='Text',
         placeholder='press ENTER to apply',
         placeholder='press ENTER to apply',
@@ -218,30 +154,25 @@ with example(ui.input):
     result = ui.label('')
     result = ui.label('')
 
 
 with example(ui.number):
 with example(ui.number):
-
     number_input = ui.number(label='Number', value=3.1415927, format='%.2f')
     number_input = ui.number(label='Number', value=3.1415927, format='%.2f')
     with ui.row():
     with ui.row():
         ui.label('underlying value: ')
         ui.label('underlying value: ')
         ui.label().bind_text_from(number_input.value)
         ui.label().bind_text_from(number_input.value)
 
 
 with example(ui.radio):
 with example(ui.radio):
-
     radio = ui.radio([1, 2, 3], value=1).props('inline')
     radio = ui.radio([1, 2, 3], value=1).props('inline')
     ui.radio({1: 'A', 2: 'B', 3: 'C'}, value=1).props('inline').bind_value(radio.value)
     ui.radio({1: 'A', 2: 'B', 3: 'C'}, value=1).props('inline').bind_value(radio.value)
 
 
 with example(ui.toggle):
 with example(ui.toggle):
-
     toggle = ui.toggle([1, 2, 3], value=1)
     toggle = ui.toggle([1, 2, 3], value=1)
     ui.toggle({1: 'A', 2: 'B', 3: 'C'}, value=1).bind_value(toggle.value)
     ui.toggle({1: 'A', 2: 'B', 3: 'C'}, value=1).bind_value(toggle.value)
 
 
 with example(ui.select):
 with example(ui.select):
-
     with ui.row():
     with ui.row():
         select = ui.select([1, 2, 3], value=1).props('inline')
         select = ui.select([1, 2, 3], value=1).props('inline')
         ui.select({1: 'One', 2: 'Two', 3: 'Three'}, value=1).props('inline').bind_value(select.value)
         ui.select({1: 'One', 2: 'Two', 3: 'Three'}, value=1).props('inline').bind_value(select.value)
 
 
 with example(ui.upload):
 with example(ui.upload):
-
     ui.upload(on_upload=lambda files: content.set_text(files))
     ui.upload(on_upload=lambda files: content.set_text(files))
     content = ui.label()
     content = ui.label()
 
 
@@ -257,7 +188,6 @@ with example(ui.plot):
         plt.ylabel('Damped oscillation')
         plt.ylabel('Damped oscillation')
 
 
 with example(ui.line_plot):
 with example(ui.line_plot):
-
     lines = ui.line_plot(n=2, limit=20, figsize=(2.5, 1.8)).with_legend(['sin', 'cos'], loc='upper center', ncol=2)
     lines = ui.line_plot(n=2, limit=20, figsize=(2.5, 1.8)).with_legend(['sin', 'cos'], loc='upper center', ncol=2)
     line_updates = ui.timer(0.1, lambda: lines.push([datetime.now()], [
     line_updates = ui.timer(0.1, lambda: lines.push([datetime.now()], [
         [np.sin(datetime.now().timestamp()) + 0.02 * np.random.randn()],
         [np.sin(datetime.now().timestamp()) + 0.02 * np.random.randn()],
@@ -272,7 +202,6 @@ with example(ui.log):
     ui.button('Log time', on_click=lambda: log.push(datetime.now().strftime("%X.%f")[:-5]))
     ui.button('Log time', on_click=lambda: log.push(datetime.now().strftime("%X.%f")[:-5]))
 
 
 with example(ui.scene):
 with example(ui.scene):
-
     with ui.scene(width=200, height=200) as scene:
     with ui.scene(width=200, height=200) as scene:
         scene.sphere().material('#4488ff')
         scene.sphere().material('#4488ff')
         scene.cylinder(1, 0.5, 2, 20).material('#ff8800', opacity=0.5).move(-2, 1)
         scene.cylinder(1, 0.5, 2, 20).material('#ff8800', opacity=0.5).move(-2, 1)
@@ -293,7 +222,6 @@ with example(ui.scene):
         scene.stl(teapot).scale(0.2).move(-3, 4)
         scene.stl(teapot).scale(0.2).move(-3, 4)
 
 
 with example(ui.joystick):
 with example(ui.joystick):
-
     ui.joystick(
     ui.joystick(
         color='blue',
         color='blue',
         size=50,
         size=50,
@@ -302,7 +230,6 @@ with example(ui.joystick):
     coordinates = ui.label('0, 0')
     coordinates = ui.label('0, 0')
 
 
 with example(ui.dialog):
 with example(ui.dialog):
-
     with ui.dialog() as dialog:
     with ui.dialog() as dialog:
         with ui.card():
         with ui.card():
             ui.label('Hello world!')
             ui.label('Hello world!')
@@ -311,19 +238,90 @@ with example(ui.dialog):
     ui.button('Open dialog', on_click=dialog.open)
     ui.button('Open dialog', on_click=dialog.open)
 
 
 with example(ui.menu):
 with example(ui.menu):
-
+    choice = ui.label('Try the menu.')
     with ui.menu() as menu:
     with ui.menu() as menu:
-        with ui.card():
-            ui.label('Menu item 1')
-            ui.label('Menu item 2')
-            ui.button('Close', on_click=menu.close).props('icon=close text-color=black color=white flat')
+        ui.menu_item('Menu item 1', lambda: choice.set_text('Selected item 1.'))
+        ui.menu_item('Menu item 2', lambda: choice.set_text('Selected item 2.'))
+        ui.menu_item('Close', on_click=menu.close)
 
 
     ui.button('Open menu', on_click=menu.open).props('color=secondary')
     ui.button('Open menu', on_click=menu.open).props('color=secondary')
 
 
 with example(ui.notify):
 with example(ui.notify):
-
     ui.button('Show notification', on_click=lambda: ui.notify('Some message', close_button='OK'))
     ui.button('Show notification', on_click=lambda: ui.notify('Some message', close_button='OK'))
 
 
+design = '''### Styling
+
+NiceGUI uses the [Quasar Framework](https://quasar.dev/) and hence has its full design power.
+Each NiceGUI element provides a `props` method whose content is passed [to the Quasar component](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components):
+Have a look at [the Quasar documentation](https://quasar.dev/vue-components/button#design) for all styling props.
+You can also apply [Tailwind](https://tailwindcss.com/) utility classes with the `classes` method. 
+
+If you really need to apply CSS, you can use the `styles` method. Here the delimiter is `;` instead of a blank space.
+
+All three functions also provide `remove` and `replace` parameters in case the predefined look is not wanted in a particular styling.
+'''
+with example(design):
+    ui.radio(['x', 'y', 'z'], value='x').props('inline color=green')
+    ui.button().props('icon=touch_app outline round').classes('shadow-lg ml-14')
+
+with example(ui.card):
+    with ui.card(design=Design.plain):
+        ui.image('http://placeimg.com/640/360/nature')
+        with ui.card_section():
+            ui.label('Lorem ipsum dolor sit amet, consectetur adipiscing elit, ...')
+
+with example(ui.column):
+    with ui.column():
+        ui.label('label 1')
+        ui.label('label 2')
+        ui.label('label 3')
+
+with example(ui.row):
+    with ui.row():
+        ui.label('label 1')
+        ui.label('label 2')
+        ui.label('label 3')
+
+binding = '''### Bindings
+
+With help of the [binding](https://pypi.org/project/binding/) package NiceGUI is able to directly bind UI elements to models.
+Binding is possible for UI element properties like text, value or visibility and for model properties that are (nested) class attributes.
+
+Each element provides methods like `bind_value` and `bind_visibility` to create a two-way binding with the corresponding property.
+To define a one-way binding use the `_from` and `_to` variants of these methods.
+Just pass a property of the model as parameter to these methods to create the binding.
+'''
+with example(binding):
+    class Demo:
+
+        def __init__(self):
+            self.number = 1
+
+    demo = Demo()
+    v = ui.checkbox('visible', value=True)
+    with ui.column().bind_visibility_from(v.value):
+        ui.slider(min=1, max=3).bind_value(demo.number)
+        ui.toggle({1: 'a', 2: 'b', 3: 'c'}).bind_value(demo.number)
+        ui.number().bind_value(demo.number)
+
+
+with example(ui.timer):
+    from datetime import datetime
+
+    with ui.row().classes('items-center'):
+        clock = ui.label()
+        t = ui.timer(interval=0.1, callback=lambda: clock.set_text(datetime.now().strftime("%X.%f")[:-5]))
+        ui.checkbox('active').bind_value(t.active)
+
+    with ui.row():
+        def lazy_update():
+            new_text = datetime.now().strftime('%X.%f')[:-5]
+            if lazy_clock.text[:8] == new_text[:8]:
+                return False
+            lazy_clock.text = new_text
+        lazy_clock = ui.label()
+        ui.timer(interval=0.1, callback=lazy_update)
+
 lifecycle = '''### Lifecycle
 lifecycle = '''### Lifecycle
 
 
 You can run a function or coroutine on startup as a parallel task by passing it to `ui.on_startup`.
 You can run a function or coroutine on startup as a parallel task by passing it to `ui.on_startup`.
@@ -331,7 +329,6 @@ If NiceGUI is shut down or restarted, the tasks will be automatically canceled (
 You can also execute cleanup code with `ui.on_shutdown`.
 You can also execute cleanup code with `ui.on_shutdown`.
 '''
 '''
 with example(lifecycle):
 with example(lifecycle):
-
     with ui.row() as row:
     with ui.row() as row:
         ui.label('count:')
         ui.label('count:')
         count_label = ui.label('0')
         count_label = ui.label('0')
@@ -348,11 +345,41 @@ with example(lifecycle):
     ui.on_startup(counter())
     ui.on_startup(counter())
 
 
 with example(ui.page):
 with example(ui.page):
-
     with ui.page('/other_page') as other:
     with ui.page('/other_page') as other:
         ui.label('Welcome to the other side')
         ui.label('Welcome to the other side')
         ui.link('Back to main page', '/')
         ui.link('Back to main page', '/')
 
 
     ui.link('Visit other page', '/other_page')
     ui.link('Visit other page', '/other_page')
 
 
+add_route = """###Route
+
+Add a new route by calling `ui.add_route` with a starlette route including a path and a function to be called. 
+Routed paths must start with a `'/'`.
+"""
+with example(add_route):
+    import starlette
+
+    ui.add_route(
+        starlette.routing.Route(
+            '/new/route',
+            lambda request: starlette.responses.PlainTextResponse('Response')
+        )
+    )
+
+    ui.link('Try the new route!', '/new/route')
+
+get_decorator = """###Get decorator
+Syntactic sugar to add routes.
+Decorating a function with the `@ui.get` makes it available at the specified endpoint, e.g. `'/another/route/1'`.
+"""
+with example(get_decorator):
+    import starlette
+
+    @ui.get('/another/route/{id}')
+    def produce_plain_response(request):
+        path_param_id = request.path_params['id']
+        return starlette.responses.PlainTextResponse(f'Response {path_param_id}')
+
+    ui.link('Try yet another route!', '/another/route/1')
+
 ui.run()
 ui.run()

+ 1 - 1
nicegui/config.py

@@ -5,7 +5,6 @@ import os
 from . import globals
 from . import globals
 
 
 class Config(BaseModel):
 class Config(BaseModel):
-
     # NOTE: should be in sync with ui.run arguments
     # NOTE: should be in sync with ui.run arguments
     host: str = '0.0.0.0'
     host: str = '0.0.0.0'
     port: int = 80
     port: int = 80
@@ -13,6 +12,7 @@ class Config(BaseModel):
     favicon: str = 'favicon.ico'
     favicon: str = 'favicon.ico'
     reload: bool = True
     reload: bool = True
     show: bool = True
     show: bool = True
+    uvicorn_logging_level = 'warning'
     interactive: bool = False
     interactive: bool = False
 
 
 
 

+ 0 - 1
nicegui/elements/bool_element.py

@@ -10,5 +10,4 @@ class BoolElement(ValueElement):
                  value: bool,
                  value: bool,
                  on_change: Callable,
                  on_change: Callable,
                  ):
                  ):
-
         super().__init__(view, value=value, on_change=on_change)
         super().__init__(view, value=value, on_change=on_change)

+ 0 - 6
nicegui/elements/button.py

@@ -25,29 +25,23 @@ class Button(Element):
 
 
     @property
     @property
     def text(self):
     def text(self):
-
         return self.view.label
         return self.view.label
 
 
     @text.setter
     @text.setter
     def text(self, text: any):
     def text(self, text: any):
-
         self.view.label = text
         self.view.label = text
 
 
     def set_text(self, text: str):
     def set_text(self, text: str):
-
         self.text = text
         self.text = text
 
 
     def bind_text_to(self, target, forward=lambda x: x):
     def bind_text_to(self, target, forward=lambda x: x):
-
         self.text.bind_to(target, forward=forward, nesting=1)
         self.text.bind_to(target, forward=forward, nesting=1)
         return self
         return self
 
 
     def bind_text_from(self, target, backward=lambda x: x):
     def bind_text_from(self, target, backward=lambda x: x):
-
         self.text.bind_from(target, backward=backward, nesting=1)
         self.text.bind_from(target, backward=backward, nesting=1)
         return self
         return self
 
 
     def bind_text(self, target, forward=lambda x: x, backward=lambda x: x):
     def bind_text(self, target, forward=lambda x: x, backward=lambda x: x):
-
         self.text.bind(target, forward=forward, backward=backward, nesting=1)
         self.text.bind(target, forward=forward, backward=backward, nesting=1)
         return self
         return self

+ 21 - 2
nicegui/elements/card.py

@@ -1,10 +1,29 @@
 import justpy as jp
 import justpy as jp
+from .element import Design
 from .group import Group
 from .group import Group
 
 
 class Card(Group):
 class Card(Group):
 
 
-    def __init__(self):
+    def __init__(self, design: Design = Design.default):
+        """Card Element
+
+        Provides a container with a dropped shadow.
 
 
-        view = jp.QCard(classes='column items-start q-pa-md', style='gap: 1em', delete_flag=False)
+        :param design: `Design.plain` does not apply any stylings to the underlying Quasar card.
+            If ommitted, `Design.default` configures padding and spacing.
+            When using `Design.plain`, content expands to the edges.
+            To provide margins for other content you can use `ui.card_section`.
+        """
+        if design == design.default:
+            view = jp.QCard(classes='column items-start q-pa-md', style='gap: 1em', delete_flag=False)
+        elif design == design.plain:
+            view = jp.QCard(delete_flag=False)
+        else:
+            raise Exception(f'unsupported design: {design}')
+        super().__init__(view)
 
 
+class CardSection(Group):
+
+    def __init__(self):
+        view = jp.QCardSection(delete_flag=False)
         super().__init__(view)
         super().__init__(view)

+ 0 - 1
nicegui/elements/choice_element.py

@@ -10,7 +10,6 @@ class ChoiceElement(ValueElement):
                  *,
                  *,
                  value: Any,
                  value: Any,
                  on_change: Callable):
                  on_change: Callable):
-
         if isinstance(options, list):
         if isinstance(options, list):
             view.options = [{'label': option, 'value': option} for option in options]
             view.options = [{'label': option, 'value': option} for option in options]
         else:
         else:

+ 14 - 2
nicegui/elements/column.py

@@ -1,10 +1,22 @@
 import justpy as jp
 import justpy as jp
+from .element import Design
 from .group import Group
 from .group import Group
 
 
 class Column(Group):
 class Column(Group):
 
 
-    def __init__(self):
+    def __init__(self, design: Design = Design.default):
+        '''Row Element
 
 
-        view = jp.QDiv(classes='column items-start', style='gap: 1em', delete_flag=False)
+        Provides a container which arranges its child in a row.
+
+        :param design: `Design.plain` does not apply any stylings.
+            If ommitted, `Design.default` configures padding and spacing.
+        '''
+        if design == design.default:
+            view = jp.QDiv(classes='column items-start', style='gap: 1em', delete_flag=False)
+        elif design == design.plain:
+            view = jp.QDiv(classes='column', delete_flag=False)
+        else:
+            raise Exception(f'unsupported design: {design}')
 
 
         super().__init__(view)
         super().__init__(view)

+ 0 - 4
nicegui/elements/custom_example.py

@@ -4,7 +4,6 @@ from .element import Element
 class CustomExampleView(CustomView):
 class CustomExampleView(CustomView):
 
 
     def __init__(self, on_change):
     def __init__(self, on_change):
-
         super().__init__('custom_example', __file__, value=0)
         super().__init__('custom_example', __file__, value=0)
 
 
         self.on_change = on_change
         self.on_change = on_change
@@ -12,7 +11,6 @@ class CustomExampleView(CustomView):
         self.initialize(temp=False, onAdd=self.handle_add)
         self.initialize(temp=False, onAdd=self.handle_add)
 
 
     def handle_add(self, msg):
     def handle_add(self, msg):
-
         self.options.value += msg.number
         self.options.value += msg.number
         if self.on_change is not None:
         if self.on_change is not None:
             return self.on_change(self.options.value)
             return self.on_change(self.options.value)
@@ -21,10 +19,8 @@ class CustomExampleView(CustomView):
 class CustomExample(Element):
 class CustomExample(Element):
 
 
     def __init__(self, *, on_change=None):
     def __init__(self, *, on_change=None):
-
         super().__init__(CustomExampleView(on_change))
         super().__init__(CustomExampleView(on_change))
 
 
     def add(self, number: str):
     def add(self, number: str):
-
         self.view.options.value += number
         self.view.options.value += number
         self.view.on_change(self.view.options.value)
         self.view.on_change(self.view.options.value)

+ 0 - 5
nicegui/elements/custom_view.py

@@ -4,11 +4,9 @@ from starlette.routing import Route
 from starlette.responses import FileResponse
 from starlette.responses import FileResponse
 
 
 class CustomView(jp.JustpyBaseComponent):
 class CustomView(jp.JustpyBaseComponent):
-
     vue_dependencies = []
     vue_dependencies = []
 
 
     def __init__(self, vue_type, filepath, dependencies=[], **options):
     def __init__(self, vue_type, filepath, dependencies=[], **options):
-
         self.vue_type = vue_type
         self.vue_type = vue_type
         self.vue_filepath = os.path.realpath(filepath).replace('.py', '.js')
         self.vue_filepath = os.path.realpath(filepath).replace('.py', '.js')
         self.vue_dependencies = dependencies
         self.vue_dependencies = dependencies
@@ -21,7 +19,6 @@ class CustomView(jp.JustpyBaseComponent):
         super().__init__(temp=False)
         super().__init__(temp=False)
 
 
     def add_page(self, wp: jp.WebPage):
     def add_page(self, wp: jp.WebPage):
-
         for dependency in self.vue_dependencies:
         for dependency in self.vue_dependencies:
             is_remote = dependency.startswith('http://') or dependency.startswith('https://')
             is_remote = dependency.startswith('http://') or dependency.startswith('https://')
             src = dependency if is_remote else f'lib/{dependency}'
             src = dependency if is_remote else f'lib/{dependency}'
@@ -40,11 +37,9 @@ class CustomView(jp.JustpyBaseComponent):
         super().add_page(wp)
         super().add_page(wp)
 
 
     def react(self, _):
     def react(self, _):
-
         pass
         pass
 
 
     def convert_object_to_dict(self):
     def convert_object_to_dict(self):
-
         return {
         return {
             'vue_type': self.vue_type,
             'vue_type': self.vue_type,
             'id': self.id,
             'id': self.id,

+ 0 - 3
nicegui/elements/dialog.py

@@ -13,7 +13,6 @@ class Dialog(Group):
 
 
         :param value: whether the dialog is already opened (default: False)
         :param value: whether the dialog is already opened (default: False)
         """
         """
-
         view = jp.QDialog(
         view = jp.QDialog(
             value=value,
             value=value,
             classes='row items-start bg-red-400',
             classes='row items-start bg-red-400',
@@ -23,9 +22,7 @@ class Dialog(Group):
         super().__init__(view)
         super().__init__(view)
 
 
     def open(self):
     def open(self):
-
         self.view.value = True
         self.view.value = True
 
 
     def close(self):
     def close(self):
-
         self.view.value = False
         self.view.value = False

+ 21 - 10
nicegui/elements/element.py

@@ -1,15 +1,14 @@
 import justpy as jp
 import justpy as jp
+from enum import Enum
 from binding.binding import BindableProperty
 from binding.binding import BindableProperty
 from ..globals import view_stack, page_stack
 from ..globals import view_stack, page_stack
 
 
 class Element:
 class Element:
-
     visible = BindableProperty
     visible = BindableProperty
 
 
     def __init__(self,
     def __init__(self,
                  view: jp.HTMLBaseComponent,
                  view: jp.HTMLBaseComponent,
                  ):
                  ):
-
         self.parent_view = view_stack[-1]
         self.parent_view = view_stack[-1]
         self.parent_view.add(view)
         self.parent_view.add(view)
         self.view = view
         self.view = view
@@ -20,22 +19,18 @@ class Element:
 
 
     @property
     @property
     def visible(self):
     def visible(self):
-
         return self.visible_
         return self.visible_
 
 
     @visible.setter
     @visible.setter
     def visible(self, visible: bool):
     def visible(self, visible: bool):
-
         self.visible_ = visible
         self.visible_ = visible
         (self.view.remove_class if self.visible_ else self.view.set_class)('hidden')
         (self.view.remove_class if self.visible_ else self.view.set_class)('hidden')
 
 
     def bind_visibility_to(self, target, forward=lambda x: x):
     def bind_visibility_to(self, target, forward=lambda x: x):
-
         self.visible.bind_to(target, forward=forward, nesting=1)
         self.visible.bind_to(target, forward=forward, nesting=1)
         return self
         return self
 
 
     def bind_visibility_from(self, target, backward=lambda x: x, *, value=None):
     def bind_visibility_from(self, target, backward=lambda x: x, *, value=None):
-
         if value is not None:
         if value is not None:
             def backward(x): return x == value
             def backward(x): return x == value
 
 
@@ -43,7 +38,6 @@ class Element:
         return self
         return self
 
 
     def bind_visibility(self, target, forward=lambda x: x, backward=None, *, value=None):
     def bind_visibility(self, target, forward=lambda x: x, backward=None, *, value=None):
-
         if value is not None:
         if value is not None:
             def backward(x): return x == value
             def backward(x): return x == value
 
 
@@ -51,7 +45,11 @@ class Element:
         return self
         return self
 
 
     def classes(self, add: str = '', *, remove: str = '', replace: str = ''):
     def classes(self, add: str = '', *, remove: str = '', replace: str = ''):
-
+        '''HTML classes to modify the look of the element.
+        Every class in the `remove` parameter will be removed from the element.
+        Classes are seperated with a blank space.
+        This can be helpful if the predefined classes by NiceGUI are not wanted in a particular styling.
+        '''
         class_list = [] if replace else self.view.classes.split()
         class_list = [] if replace else self.view.classes.split()
         class_list = [c for c in class_list if c not in remove]
         class_list = [c for c in class_list if c not in remove]
         class_list += add.split()
         class_list += add.split()
@@ -61,7 +59,11 @@ class Element:
         return self
         return self
 
 
     def style(self, add: str = '', *, remove: str = '', replace: str = ''):
     def style(self, add: str = '', *, remove: str = '', replace: str = ''):
-
+        '''CSS style sheet definitions to modify the look of the element.
+        Every style in the `remove` parameter will be removed from the element.
+        Styles are seperated with a semicolon.
+        This can be helpful if the predefined style sheet definitions by NiceGUI are not wanted in a particular styling.
+        '''
         style_list = [] if replace else self.view.style.split(';')
         style_list = [] if replace else self.view.style.split(';')
         style_list = [c for c in style_list if c not in remove.split(';')]
         style_list = [c for c in style_list if c not in remove.split(';')]
         style_list += add.split(';')
         style_list += add.split(';')
@@ -71,7 +73,12 @@ class Element:
         return self
         return self
 
 
     def props(self, add: str = '', *, remove: str = '', replace: str = ''):
     def props(self, add: str = '', *, remove: str = '', replace: str = ''):
-
+        '''Quasar props https://quasar.dev/vue-components/button#design to modify the look of the element.
+        Boolean props will automatically activated if they appear in the list of the `add` property.
+        Props are seperated with a blank space.
+        Every prop passed to the `remove` parameter will be removed from the element.
+        This can be helpful if the predefined props by NiceGUI are not wanted in a particular styling.
+        '''
         for prop in remove.split() + replace.split():
         for prop in remove.split() + replace.split():
             setattr(self.view, prop.split('=')[0], None)
             setattr(self.view, prop.split('=')[0], None)
 
 
@@ -82,3 +89,7 @@ class Element:
                 setattr(self.view, prop, True)
                 setattr(self.view, prop, True)
 
 
         return self
         return self
+
+class Design(Enum):
+    default = 1
+    plain = 2

+ 0 - 2
nicegui/elements/float_element.py

@@ -11,13 +11,11 @@ class FloatElement(ValueElement):
                  format: str = None,
                  format: str = None,
                  on_change: Callable,
                  on_change: Callable,
                  ):
                  ):
-
         self.format = format
         self.format = format
 
 
         super().__init__(view, value=value, on_change=on_change)
         super().__init__(view, value=value, on_change=on_change)
 
 
     def value_to_view(self, value: float):
     def value_to_view(self, value: float):
-
         if value is None:
         if value is None:
             return None
             return None
         elif self.format is None:
         elif self.format is None:

+ 0 - 2
nicegui/elements/group.py

@@ -4,10 +4,8 @@ from ..globals import view_stack
 class Group(Element):
 class Group(Element):
 
 
     def __enter__(self):
     def __enter__(self):
-
         view_stack.append(self.view)
         view_stack.append(self.view)
         return self
         return self
 
 
     def __exit__(self, *_):
     def __exit__(self, *_):
-
         view_stack.pop()
         view_stack.pop()

+ 0 - 4
nicegui/elements/html.py

@@ -12,21 +12,17 @@ class Html(Element):
 
 
         :param content: the HTML code to be displayed
         :param content: the HTML code to be displayed
         """
         """
-
         view = jp.QDiv()
         view = jp.QDiv()
         super().__init__(view)
         super().__init__(view)
         self.content = content
         self.content = content
 
 
     @property
     @property
     def content(self):
     def content(self):
-
         return self.content.inner_html
         return self.content.inner_html
 
 
     @content.setter
     @content.setter
     def content(self, content: any):
     def content(self, content: any):
-
         self.set_content(content)
         self.set_content(content)
 
 
     def set_content(self, content: str):
     def set_content(self, content: str):
-
         self.view.inner_html = content
         self.view.inner_html = content

+ 0 - 1
nicegui/elements/icon.py

@@ -6,7 +6,6 @@ class Icon(Element):
     def __init__(self,
     def __init__(self,
                  name: str,
                  name: str,
                  ):
                  ):
-
         view = jp.QIcon(name=name, classes=f'q-pt-xs', size='20px')
         view = jp.QIcon(name=name, classes=f'q-pt-xs', size='20px')
 
 
         super().__init__(view)
         super().__init__(view)

+ 0 - 7
nicegui/elements/image.py

@@ -12,36 +12,29 @@ class Image(Group):
 
 
         :param source: the source of the image; can be an url or a base64 string
         :param source: the source of the image; can be an url or a base64 string
         """
         """
-
         view = jp.QImg(src=source)
         view = jp.QImg(src=source)
 
 
         super().__init__(view)
         super().__init__(view)
 
 
     @property
     @property
     def source(self):
     def source(self):
-
         return self.view.src
         return self.view.src
 
 
     @source.setter
     @source.setter
     def source(self, source: any):
     def source(self, source: any):
-
         self.view.src = source
         self.view.src = source
 
 
     def set_source(self, source: str):
     def set_source(self, source: str):
-
         self.source = source
         self.source = source
 
 
     def bind_source_to(self, target, forward=lambda x: x):
     def bind_source_to(self, target, forward=lambda x: x):
-
         self.source.bind_to(target, forward=forward, nesting=1)
         self.source.bind_to(target, forward=forward, nesting=1)
         return self
         return self
 
 
     def bind_source_from(self, target, backward=lambda x: x):
     def bind_source_from(self, target, backward=lambda x: x):
-
         self.source.bind_from(target, backward=backward, nesting=1)
         self.source.bind_from(target, backward=backward, nesting=1)
         return self
         return self
 
 
     def bind_source(self, target, forward=lambda x: x, backward=lambda x: x):
     def bind_source(self, target, forward=lambda x: x, backward=lambda x: x):
-
         self.source.bind(target, forward=forward, backward=backward, nesting=1)
         self.source.bind(target, forward=forward, backward=backward, nesting=1)
         return self
         return self

+ 0 - 5
nicegui/elements/joystick.py

@@ -5,7 +5,6 @@ from .element import Element
 class JoystickView(CustomView):
 class JoystickView(CustomView):
 
 
     def __init__(self, on_start, on_move, on_end, **options):
     def __init__(self, on_start, on_move, on_end, **options):
-
         super().__init__('joystick', __file__, ['nipplejs.min.js'], **options)
         super().__init__('joystick', __file__, ['nipplejs.min.js'], **options)
 
 
         self.on_start = on_start
         self.on_start = on_start
@@ -18,19 +17,16 @@ class JoystickView(CustomView):
                         onEnd=self.handle_end)
                         onEnd=self.handle_end)
 
 
     def handle_start(self, msg):
     def handle_start(self, msg):
-
         if self.on_start is not None:
         if self.on_start is not None:
             return self.on_start(msg)
             return self.on_start(msg)
         return False
         return False
 
 
     def handle_move(self, msg):
     def handle_move(self, msg):
-
         if self.on_move is not None:
         if self.on_move is not None:
             return self.on_move(msg)
             return self.on_move(msg)
         return False
         return False
 
 
     def handle_end(self, msg):
     def handle_end(self, msg):
-
         if self.on_end is not None:
         if self.on_end is not None:
             return self.on_end(msg)
             return self.on_end(msg)
         return False
         return False
@@ -53,5 +49,4 @@ class Joystick(Element):
         :param on_end: callback for when the user releases the joystick
         :param on_end: callback for when the user releases the joystick
         :param options: arguments like `color` which should be passed to the `underlying nipple.js library <https://github.com/yoannmoinet/nipplejs#options>`_
         :param options: arguments like `color` which should be passed to the `underlying nipple.js library <https://github.com/yoannmoinet/nipplejs#options>`_
         """
         """
-
         super().__init__(JoystickView(on_start, on_move, on_end, **options))
         super().__init__(JoystickView(on_start, on_move, on_end, **options))

+ 0 - 7
nicegui/elements/label.py

@@ -12,36 +12,29 @@ class Label(Element):
 
 
         :param text: the content of the label
         :param text: the content of the label
         """
         """
-
         view = jp.Div(text=text)
         view = jp.Div(text=text)
 
 
         super().__init__(view)
         super().__init__(view)
 
 
     @property
     @property
     def text(self):
     def text(self):
-
         return self.view.text
         return self.view.text
 
 
     @text.setter
     @text.setter
     def text(self, text: any):
     def text(self, text: any):
-
         self.view.text = text
         self.view.text = text
 
 
     def set_text(self, text: str):
     def set_text(self, text: str):
-
         self.text = text
         self.text = text
 
 
     def bind_text_to(self, target, forward=lambda x: x):
     def bind_text_to(self, target, forward=lambda x: x):
-
         self.text.bind_to(target, forward=forward, nesting=1)
         self.text.bind_to(target, forward=forward, nesting=1)
         return self
         return self
 
 
     def bind_text_from(self, target, backward=lambda x: x):
     def bind_text_from(self, target, backward=lambda x: x):
-
         self.text.bind_from(target, backward=backward, nesting=1)
         self.text.bind_from(target, backward=backward, nesting=1)
         return self
         return self
 
 
     def bind_text(self, target, forward=lambda x: x, backward=lambda x: x):
     def bind_text(self, target, forward=lambda x: x, backward=lambda x: x):
-
         self.text.bind(target, forward=forward, backward=backward, nesting=1)
         self.text.bind(target, forward=forward, backward=backward, nesting=1)
         return self
         return self

+ 0 - 3
nicegui/elements/line_plot.py

@@ -21,7 +21,6 @@ class LinePlot(Plot):
         :param close: whether the figure should be closed after exiting the context; set to `False` if you want to update it later, default is `True`
         :param close: whether the figure should be closed after exiting the context; set to `False` if you want to update it later, default is `True`
         :param kwargs: arguments like `figsize` which should be passed to `pyplot.figure <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.figure.html>`_
         :param kwargs: arguments like `figsize` which should be passed to `pyplot.figure <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.figure.html>`_
         """
         """
-
         super().__init__(close=close, **kwargs)
         super().__init__(close=close, **kwargs)
 
 
         self.x = []
         self.x = []
@@ -32,13 +31,11 @@ class LinePlot(Plot):
         self.push_counter = 0
         self.push_counter = 0
 
 
     def with_legend(self, titles: List[str], **kwargs):
     def with_legend(self, titles: List[str], **kwargs):
-
         self.fig.gca().legend(titles, **kwargs)
         self.fig.gca().legend(titles, **kwargs)
         self.view.set_figure(self.fig)
         self.view.set_figure(self.fig)
         return self
         return self
 
 
     def push(self, x: List[float], Y: List[List[float]]):
     def push(self, x: List[float], Y: List[List[float]]):
-
         self.push_counter += 1
         self.push_counter += 1
 
 
         self.x = [*self.x, *x][self.slice]
         self.x = [*self.x, *x][self.slice]

+ 0 - 1
nicegui/elements/link.py

@@ -7,7 +7,6 @@ class Link(Element):
                  text: str = '',
                  text: str = '',
                  href: str = '#',
                  href: str = '#',
                  ):
                  ):
-
         view = jp.A(text=text, href=href, classes='underline text-blue')
         view = jp.A(text=text, href=href, classes='underline text-blue')
 
 
         super().__init__(view)
         super().__init__(view)

+ 0 - 4
nicegui/elements/log.py

@@ -7,7 +7,6 @@ from .element import Element
 class LogView(CustomView):
 class LogView(CustomView):
 
 
     def __init__(self, max_lines: int):
     def __init__(self, max_lines: int):
-
         super().__init__('log', __file__, max_lines=max_lines)
         super().__init__('log', __file__, max_lines=max_lines)
 
 
 class Log(Element):
 class Log(Element):
@@ -19,18 +18,15 @@ class Log(Element):
 
 
         :param max_lines: maximum number of lines before dropping oldest ones (default: None)
         :param max_lines: maximum number of lines before dropping oldest ones (default: None)
         """
         """
-
         super().__init__(LogView(max_lines=max_lines))
         super().__init__(LogView(max_lines=max_lines))
 
 
         self.classes('border whitespace-pre font-mono').style('opacity: 1 !important; cursor: text !important')
         self.classes('border whitespace-pre font-mono').style('opacity: 1 !important; cursor: text !important')
 
 
     async def push_async(self, line: str):
     async def push_async(self, line: str):
-
         await asyncio.gather(*[
         await asyncio.gather(*[
             self.view.run_method(f'push("{urllib.parse.quote(line)}")', socket)
             self.view.run_method(f'push("{urllib.parse.quote(line)}")', socket)
             for socket in WebPage.sockets[self.page.page_id].values()
             for socket in WebPage.sockets[self.page.page_id].values()
         ])
         ])
 
 
     def push(self, line: str):
     def push(self, line: str):
-
         asyncio.get_event_loop().create_task(self.push_async(line))
         asyncio.get_event_loop().create_task(self.push_async(line))

+ 0 - 3
nicegui/elements/markdown.py

@@ -13,11 +13,9 @@ class Markdown(Html):
 
 
         :param content: the markdown content to be displayed
         :param content: the markdown content to be displayed
         """
         """
-
         super().__init__(content)
         super().__init__(content)
 
 
     def set_content(self, content: str):
     def set_content(self, content: str):
-
         html = markdown2.markdown(content, extras=['fenced-code-blocks'])
         html = markdown2.markdown(content, extras=['fenced-code-blocks'])
         # we need explicit markdown styling because tailwind css removes all default styles
         # we need explicit markdown styling because tailwind css removes all default styles
         html = Markdown.apply_tailwind(html)
         html = Markdown.apply_tailwind(html)
@@ -25,7 +23,6 @@ class Markdown(Html):
 
 
     @staticmethod
     @staticmethod
     def apply_tailwind(html: str):
     def apply_tailwind(html: str):
-
         rep = {
         rep = {
             '<h1': '<h1 class="text-5xl mb-4 mt-6"',
             '<h1': '<h1 class="text-5xl mb-4 mt-6"',
             '<h2': '<h2 class="text-4xl mb-3 mt-5"',
             '<h2': '<h2 class="text-4xl mb-3 mt-5"',

+ 1 - 4
nicegui/elements/menu.py

@@ -5,7 +5,7 @@ class Menu(Group):
 
 
     def __init__(self,
     def __init__(self,
                  *,
                  *,
-                 value: bool = False
+                 value: bool = False,
                  ):
                  ):
         """Menu
         """Menu
 
 
@@ -13,15 +13,12 @@ class Menu(Group):
 
 
         :param value: whether the menu is already opened (default: False)
         :param value: whether the menu is already opened (default: False)
         """
         """
-
         view = jp.QMenu(value=value)
         view = jp.QMenu(value=value)
 
 
         super().__init__(view)
         super().__init__(view)
 
 
     def open(self):
     def open(self):
-
         self.view.value = True
         self.view.value = True
 
 
     def close(self):
     def close(self):
-
         self.view.value = False
         self.view.value = False

+ 25 - 0
nicegui/elements/menu_item.py

@@ -0,0 +1,25 @@
+from typing import Callable
+import justpy as jp
+from .element import Element
+from ..utils import handle_exceptions, provide_arguments
+
+
+class MenuItem(Element):
+
+    def __init__(self,
+                 text: str = '',
+                 on_click: Callable = None,
+                 ):
+        """Menu Item Element
+
+        A menu item to be added to a menu.
+
+        :param text: label of the menu item
+        :param on_click: callback to be executed when selecting the menu item
+        """
+        view = jp.QItem(text=text, clickable=True)
+
+        if on_click is not None:
+            view.on('click', handle_exceptions(provide_arguments(on_click)))
+
+        super().__init__(view)

+ 1 - 3
nicegui/elements/notify.py

@@ -9,7 +9,7 @@ class Notify(Element):
                  message: str,
                  message: str,
                  *,
                  *,
                  position: str = 'bottom',
                  position: str = 'bottom',
-                 close_button: str = None
+                 close_button: str = None,
                  ):
                  ):
         """Notification
         """Notification
 
 
@@ -19,14 +19,12 @@ class Notify(Element):
         :param position: position on the screen ("top-left", "top-right", "bottom-left","bottom-right, "top", "bottom", "left", "right" or "center", default: "bottom")
         :param position: position on the screen ("top-left", "top-right", "bottom-left","bottom-right, "top", "bottom", "left", "right" or "center", default: "bottom")
         :param close_button: optional label of a button to dismiss the notification (default: None)
         :param close_button: optional label of a button to dismiss the notification (default: None)
         """
         """
-
         view = jp.QNotify(message=message, position=position, closeBtn=close_button)
         view = jp.QNotify(message=message, position=position, closeBtn=close_button)
 
 
         super().__init__(view)
         super().__init__(view)
         asyncio.get_event_loop().create_task(self.notify_async())
         asyncio.get_event_loop().create_task(self.notify_async())
 
 
     async def notify_async(self):
     async def notify_async(self):
-
         self.view.notify = True
         self.view.notify = True
         await self.parent_view.update()
         await self.parent_view.update()
         self.view.notify = False
         self.view.notify = False

+ 0 - 2
nicegui/elements/number.py

@@ -20,7 +20,6 @@ class Number(FloatElement):
         :param format: a string like '%.2f' to format the displayed value
         :param format: a string like '%.2f' to format the displayed value
         :param on_change: callback to execute when the input is confirmed by leaving the focus
         :param on_change: callback to execute when the input is confirmed by leaving the focus
         """
         """
-
         view = jp.QInput(
         view = jp.QInput(
             type='number',
             type='number',
             label=label,
             label=label,
@@ -31,7 +30,6 @@ class Number(FloatElement):
         super().__init__(view, value=value, format=format, on_change=on_change)
         super().__init__(view, value=value, format=format, on_change=on_change)
 
 
     def handle_change(self, msg):
     def handle_change(self, msg):
-
         msg['value'] = float(msg['value'])
         msg['value'] = float(msg['value'])
 
 
         return super().handle_change(msg)
         return super().handle_change(msg)

+ 0 - 3
nicegui/elements/plot.py

@@ -16,7 +16,6 @@ class Plot(Element):
         :param close: whether the figure should be closed after exiting the context; set to `False` if you want to update it later, default is `True`
         :param close: whether the figure should be closed after exiting the context; set to `False` if you want to update it later, default is `True`
         :param kwargs: arguments like `figsize` which should be passed to `pyplot.figure <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.figure.html>`_
         :param kwargs: arguments like `figsize` which should be passed to `pyplot.figure <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.figure.html>`_
         """
         """
-
         self.close = close
         self.close = close
         self.fig = plt.figure(**kwargs)
         self.fig = plt.figure(**kwargs)
 
 
@@ -26,13 +25,11 @@ class Plot(Element):
         super().__init__(view)
         super().__init__(view)
 
 
     def __enter__(self):
     def __enter__(self):
-
         plt.figure(self.fig)
         plt.figure(self.fig)
 
 
         return self
         return self
 
 
     def __exit__(self, *_):
     def __exit__(self, *_):
-
         self.view.set_figure(plt.gcf())
         self.view.set_figure(plt.gcf())
 
 
         if self.close:
         if self.close:

+ 0 - 1
nicegui/elements/radio.py

@@ -16,7 +16,6 @@ class Radio(ChoiceElement):
         :param value: the inital value
         :param value: the inital value
         :param on_change: callback to execute when selection changes
         :param on_change: callback to execute when selection changes
         """
         """
-
         view = jp.QOptionGroup(options=options, input=self.handle_change)
         view = jp.QOptionGroup(options=options, input=self.handle_change)
 
 
         super().__init__(view, options, value=value, on_change=on_change)
         super().__init__(view, options, value=value, on_change=on_change)

+ 14 - 2
nicegui/elements/row.py

@@ -1,10 +1,22 @@
 import justpy as jp
 import justpy as jp
+from .element import Design
 from .group import Group
 from .group import Group
 
 
 class Row(Group):
 class Row(Group):
 
 
-    def __init__(self):
+    def __init__(self, design: Design = Design.default):
+        '''Row Element
 
 
-        view = jp.QDiv(classes='row items-start', style='gap: 1em', delete_flag=False)
+        Provides a container which arranges its child in a row.
+
+        :param design: `Design.plain` does not apply any stylings.
+            If ommitted, `Design.default` configures padding and spacing.
+        '''
+        if design == design.default:
+            view = jp.QDiv(classes='row items-start', style='gap: 1em', delete_flag=False)
+        elif design == design.plain:
+            view = jp.QDiv(classes='row', delete_flag=False)
+        else:
+            raise Exception(f'unsupported design: {design}')
 
 
         super().__init__(view)
         super().__init__(view)

+ 0 - 1
nicegui/elements/scene.py

@@ -34,7 +34,6 @@ class SceneView(CustomView):
             traceback.print_exc()
             traceback.print_exc()
 
 
 class Scene(Element):
 class Scene(Element):
-
     from .scene_objects import Group as group
     from .scene_objects import Group as group
     from .scene_objects import Box as box
     from .scene_objects import Box as box
     from .scene_objects import Sphere as sphere
     from .scene_objects import Sphere as sphere

+ 0 - 1
nicegui/elements/scene_object3d.py

@@ -6,7 +6,6 @@ import numpy as np
 from justpy.htmlcomponents import WebPage
 from justpy.htmlcomponents import WebPage
 
 
 class Object3D:
 class Object3D:
-
     stack: list[Object3D] = []
     stack: list[Object3D] = []
 
 
     def __init__(self, type: str, *args):
     def __init__(self, type: str, *args):

+ 9 - 8
nicegui/elements/scene_objects.py

@@ -1,3 +1,4 @@
+from __future__ import annotations
 from typing import Optional
 from typing import Optional
 from .scene_object3d import Object3D
 from .scene_object3d import Object3D
 
 
@@ -46,7 +47,7 @@ class Cylinder(Object3D):
 class Extrusion(Object3D):
 class Extrusion(Object3D):
 
 
     def __init__(self,
     def __init__(self,
-                 outline: list[tuple[float, float]],
+                 outline: list[list[float, float]],
                  height: float,
                  height: float,
                  wireframe: bool = False,
                  wireframe: bool = False,
                  ):
                  ):
@@ -63,18 +64,18 @@ class Stl(Object3D):
 class Line(Object3D):
 class Line(Object3D):
 
 
     def __init__(self,
     def __init__(self,
-                 start: tuple[float, float, float],
-                 end: tuple[float, float, float],
+                 start: list[float, float, float],
+                 end: list[float, float, float],
                  ):
                  ):
         super().__init__('line', start, end)
         super().__init__('line', start, end)
 
 
 class Curve(Object3D):
 class Curve(Object3D):
 
 
     def __init__(self,
     def __init__(self,
-                 start: tuple[float, float, float],
-                 control1: tuple[float, float, float],
-                 control2: tuple[float, float, float],
-                 end: tuple[float, float, float],
+                 start: list[float, float, float],
+                 control1: list[float, float, float],
+                 control2: list[float, float, float],
+                 end: list[float, float, float],
                  num_points: int = 20,
                  num_points: int = 20,
                  ):
                  ):
         super().__init__('curve', start, control1, control2, end, num_points)
         super().__init__('curve', start, control1, control2, end, num_points)
@@ -83,6 +84,6 @@ class Texture(Object3D):
 
 
     def __init__(self,
     def __init__(self,
                  url: str,
                  url: str,
-                 coordinates: list[list[Optional[tuple[float]]]],
+                 coordinates: list[list[Optional[list[float]]]],
                  ):
                  ):
         super().__init__('texture', url, coordinates)
         super().__init__('texture', url, coordinates)

+ 0 - 3
nicegui/elements/select.py

@@ -16,13 +16,11 @@ class Select(ChoiceElement):
         :param value: the inital value
         :param value: the inital value
         :param on_change: callback to execute when selection changes
         :param on_change: callback to execute when selection changes
         """
         """
-
         view = jp.QSelect(options=options, input=self.handle_change)
         view = jp.QSelect(options=options, input=self.handle_change)
 
 
         super().__init__(view, options, value=value, on_change=on_change)
         super().__init__(view, options, value=value, on_change=on_change)
 
 
     def value_to_view(self, value: any):
     def value_to_view(self, value: any):
-
         matches = [o for o in self.view.options if o['value'] == value]
         matches = [o for o in self.view.options if o['value'] == value]
         if any(matches):
         if any(matches):
             return matches[0]['label']
             return matches[0]['label']
@@ -30,7 +28,6 @@ class Select(ChoiceElement):
             return value
             return value
 
 
     def handle_change(self, msg):
     def handle_change(self, msg):
-
         msg['label'] = msg['value']['label']
         msg['label'] = msg['value']['label']
         msg['value'] = msg['value']['value']
         msg['value'] = msg['value']['value']
         return super().handle_change(msg)
         return super().handle_change(msg)

+ 0 - 1
nicegui/elements/string_element.py

@@ -10,5 +10,4 @@ class StringElement(ValueElement):
                  value: float,
                  value: float,
                  on_change: Callable,
                  on_change: Callable,
                  ):
                  ):
-
         super().__init__(view, value=value, on_change=on_change)
         super().__init__(view, value=value, on_change=on_change)

+ 0 - 7
nicegui/elements/svg.py

@@ -12,37 +12,30 @@ class Svg(Element):
 
 
         :param content: the svg definition
         :param content: the svg definition
         """
         """
-
         view = jp.Div(style="padding:0;width:100%;height:100%")
         view = jp.Div(style="padding:0;width:100%;height:100%")
         super().__init__(view)
         super().__init__(view)
         self.content = content
         self.content = content
 
 
     @property
     @property
     def content(self):
     def content(self):
-
         return self.view.inner_html()
         return self.view.inner_html()
 
 
     @content.setter
     @content.setter
     def content(self, content: any):
     def content(self, content: any):
-
         self.view.components = []
         self.view.components = []
         jp.parse_html(content, a=self.view)
         jp.parse_html(content, a=self.view)
 
 
     def set_content(self, content: str):
     def set_content(self, content: str):
-
         self.content = content
         self.content = content
 
 
     def bind_content_to(self, target, forward=lambda x: x):
     def bind_content_to(self, target, forward=lambda x: x):
-
         self.content.bind_to(target, forward=forward, nesting=1)
         self.content.bind_to(target, forward=forward, nesting=1)
         return self
         return self
 
 
     def bind_content_from(self, target, backward=lambda x: x):
     def bind_content_from(self, target, backward=lambda x: x):
-
         self.content.bind_from(target, backward=backward, nesting=1)
         self.content.bind_from(target, backward=backward, nesting=1)
         return self
         return self
 
 
     def bind_content(self, target, forward=lambda x: x, backward=lambda x: x):
     def bind_content(self, target, forward=lambda x: x, backward=lambda x: x):
-
         self.content.bind(target, forward=forward, backward=backward, nesting=1)
         self.content.bind(target, forward=forward, backward=backward, nesting=1)
         return self
         return self

+ 0 - 1
nicegui/elements/toggle.py

@@ -16,7 +16,6 @@ class Toggle(ChoiceElement):
         :param value: the inital value
         :param value: the inital value
         :param on_change: callback to execute when selection changes
         :param on_change: callback to execute when selection changes
         """
         """
-
         view = jp.QBtnToggle(input=self.handle_change)
         view = jp.QBtnToggle(input=self.handle_change)
 
 
         super().__init__(view, options, value=value, on_change=on_change)
         super().__init__(view, options, value=value, on_change=on_change)

+ 0 - 1
nicegui/elements/upload.py

@@ -25,7 +25,6 @@ class Upload(Element):
         super().__init__(view)
         super().__init__(view)
 
 
     def submit(self, _, msg):
     def submit(self, _, msg):
-
         for form_data in msg.form_data:
         for form_data in msg.form_data:
             if form_data.type == 'file':
             if form_data.type == 'file':
                 self.on_upload([base64.b64decode(f.file_content) for f in form_data.files])
                 self.on_upload([base64.b64decode(f.file_content) for f in form_data.files])

+ 0 - 7
nicegui/elements/value_element.py

@@ -6,7 +6,6 @@ from .element import Element
 from ..utils import EventArguments
 from ..utils import EventArguments
 
 
 class ValueElement(Element):
 class ValueElement(Element):
-
     value = BindableProperty()
     value = BindableProperty()
 
 
     def __init__(self,
     def __init__(self,
@@ -15,7 +14,6 @@ class ValueElement(Element):
                  value: Any,
                  value: Any,
                  on_change: Callable,
                  on_change: Callable,
                  ):
                  ):
-
         super().__init__(view)
         super().__init__(view)
 
 
         self.on_change = on_change
         self.on_change = on_change
@@ -23,11 +21,9 @@ class ValueElement(Element):
         self.value.bind_to(self.view.value, forward=self.value_to_view)
         self.value.bind_to(self.view.value, forward=self.value_to_view)
 
 
     def value_to_view(self, value):
     def value_to_view(self, value):
-
         return value
         return value
 
 
     def handle_change(self, msg):
     def handle_change(self, msg):
-
         self.value = msg['value']
         self.value = msg['value']
 
 
         if self.on_change is not None:
         if self.on_change is not None:
@@ -40,16 +36,13 @@ class ValueElement(Element):
                 traceback.print_exc()
                 traceback.print_exc()
 
 
     def bind_value_to(self, target, forward=lambda x: x):
     def bind_value_to(self, target, forward=lambda x: x):
-
         self.value.bind_to(target, forward=forward, nesting=1)
         self.value.bind_to(target, forward=forward, nesting=1)
         return self
         return self
 
 
     def bind_value_from(self, target, backward=lambda x: x):
     def bind_value_from(self, target, backward=lambda x: x):
-
         self.value.bind_from(target, backward=backward, nesting=1)
         self.value.bind_from(target, backward=backward, nesting=1)
         return self
         return self
 
 
     def bind_value(self, target, forward=lambda x: x, backward=lambda x: x):
     def bind_value(self, target, forward=lambda x: x, backward=lambda x: x):
-
         self.value.bind(target, forward=forward, backward=backward, nesting=1)
         self.value.bind(target, forward=forward, backward=backward, nesting=1)
         return self
         return self

+ 0 - 2
nicegui/lifecycle.py

@@ -3,11 +3,9 @@ from typing import Awaitable, Callable, List, Union
 startup_tasks: List[Union[Callable, Awaitable]] = []
 startup_tasks: List[Union[Callable, Awaitable]] = []
 
 
 def on_startup(self, task: Union[Callable, Awaitable]):
 def on_startup(self, task: Union[Callable, Awaitable]):
-
     self.startup_tasks.append(task)
     self.startup_tasks.append(task)
 
 
 shutdown_tasks: List[Union[Callable, Awaitable]] = []
 shutdown_tasks: List[Union[Callable, Awaitable]] = []
 
 
 def on_shutdown(self, task: Union[Callable, Awaitable]):
 def on_shutdown(self, task: Union[Callable, Awaitable]):
-
     self.shutdown_tasks.append(task)
     self.shutdown_tasks.append(task)

+ 16 - 0
nicegui/routes.py

@@ -1,4 +1,20 @@
+from starlette.routing import Route
 from . import globals
 from . import globals
 
 
 def add_route(self, route):
 def add_route(self, route):
+    """
+    :param route: starlette route including a path and a function to be called
+    :return:
+    """
     globals.app.routes.insert(0, route)
     globals.app.routes.insert(0, route)
+
+def get(self, path: str):
+    """
+    Use as a decorator for a function like @ui.get('/another/route/{id}').
+    :param path: string that starts with a '/'
+    :return:
+    """
+    def decorator(func):
+        self.add_route(Route(path, func))
+        return func
+    return decorator

+ 12 - 3
nicegui/run.py

@@ -8,12 +8,21 @@ if not globals.config.interactive and globals.config.reload and not inspect.stac
 
 
     if globals.config.show:
     if globals.config.show:
         webbrowser.open(f'http://{globals.config.host}:{globals.config.port}/')
         webbrowser.open(f'http://{globals.config.host}:{globals.config.port}/')
-    uvicorn.run('nicegui:app', host=globals.config.host, port=globals.config.port, lifespan='on', reload=True)
+    uvicorn.run('nicegui:app', host=globals.config.host, port=globals.config.port, lifespan='on', reload=True,
+                log_level=globals.config.uvicorn_logging_level)
     sys.exit()
     sys.exit()
 
 
-def run(self, *, host='0.0.0.0', port=80, title='NiceGUI', favicon='favicon.ico', reload=True, show=True):
+def run(self, *,
+        host: str = '0.0.0.0',
+        port: int = 80,
+        title: str = 'NiceGUI',
+        favicon: str = 'favicon.ico',
+        reload: bool = True,
+        show: bool = True,
+        uvicorn_logging_level: str = 'warning',
+        ):
 
 
     if globals.config.interactive or reload == False:  # NOTE: if reload == True we already started uvicorn above
     if globals.config.interactive or reload == False:  # NOTE: if reload == True we already started uvicorn above
         if show:
         if show:
             webbrowser.open(f'http://{host if host != "0.0.0.0" else "127.0.0.1"}:{port}/')
             webbrowser.open(f'http://{host if host != "0.0.0.0" else "127.0.0.1"}:{port}/')
-        uvicorn.run(globals.app, host=host, port=port, lifespan='on')
+        uvicorn.run(globals.app, host=host, port=port, lifespan='on', log_level=uvicorn_logging_level)

+ 4 - 2
nicegui/static/templates/local/materialdesignicons/iconfont/README.md

@@ -1,8 +1,10 @@
 The recommended way to use the Material Icons font is by linking to the web font hosted on Google Fonts:
 The recommended way to use the Material Icons font is by linking to the web font hosted on Google Fonts:
 
 
 ```html
 ```html
-<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
-      rel="stylesheet">
+<link
+  href="https://fonts.googleapis.com/icon?family=Material+Icons"
+  rel="stylesheet"
+/>
 ```
 ```
 
 
 Read more in our full usage guide:
 Read more in our full usage guide:

+ 0 - 3
nicegui/timer.py

@@ -7,7 +7,6 @@ from .globals import view_stack
 from .utils import handle_exceptions, handle_awaitable
 from .utils import handle_exceptions, handle_awaitable
 
 
 class Timer:
 class Timer:
-
     tasks = []
     tasks = []
 
 
     active = BindableProperty
     active = BindableProperty
@@ -29,13 +28,11 @@ class Timer:
         self.active = active
         self.active = active
 
 
         async def timeout():
         async def timeout():
-
             await asyncio.sleep(interval)
             await asyncio.sleep(interval)
             await handle_exceptions(handle_awaitable(callback))()
             await handle_exceptions(handle_awaitable(callback))()
             await parent.update()
             await parent.update()
 
 
         async def loop():
         async def loop():
-
             while True:
             while True:
                 try:
                 try:
                     start = time.time()
                     start = time.time()

+ 3 - 2
nicegui/ui.py

@@ -1,5 +1,4 @@
 class Ui:
 class Ui:
-
     from .config import config  # NOTE: before run
     from .config import config  # NOTE: before run
     from .run import run  # NOTE: before justpy
     from .run import run  # NOTE: before justpy
 
 
@@ -17,6 +16,7 @@ class Ui:
     from .elements.log import Log as log
     from .elements.log import Log as log
     from .elements.markdown import Markdown as markdown
     from .elements.markdown import Markdown as markdown
     from .elements.menu import Menu as menu
     from .elements.menu import Menu as menu
+    from .elements.menu_item import MenuItem as menu_item
     from .elements.notify import Notify as notify
     from .elements.notify import Notify as notify
     from .elements.number import Number as number
     from .elements.number import Number as number
     from .elements.page import Page as page
     from .elements.page import Page as page
@@ -35,9 +35,10 @@ class Ui:
     from .elements.row import Row as row
     from .elements.row import Row as row
     from .elements.column import Column as column
     from .elements.column import Column as column
     from .elements.card import Card as card
     from .elements.card import Card as card
+    from .elements.card import CardSection as card_section
 
 
     from .timer import Timer as timer
     from .timer import Timer as timer
 
 
     from .lifecycle import startup_tasks, on_startup, shutdown_tasks, on_shutdown
     from .lifecycle import startup_tasks, on_startup, shutdown_tasks, on_shutdown
 
 
-    from .routes import add_route
+    from .routes import add_route, get

+ 3 - 1
nicegui/utils.py

@@ -4,11 +4,11 @@ import traceback
 class EventArguments:
 class EventArguments:
 
 
     def __init__(self, sender, **kwargs):
     def __init__(self, sender, **kwargs):
-
         self.sender = sender
         self.sender = sender
         for key, value in kwargs.items():
         for key, value in kwargs.items():
             setattr(self, key, value)
             setattr(self, key, value)
 
 
+
 def provide_arguments(func, *keys):
 def provide_arguments(func, *keys):
     def inner_function(sender, event):
     def inner_function(sender, event):
         try:
         try:
@@ -17,6 +17,7 @@ def provide_arguments(func, *keys):
             return func(EventArguments(sender, **{key: event[key] for key in keys}))
             return func(EventArguments(sender, **{key: event[key] for key in keys}))
     return inner_function
     return inner_function
 
 
+
 def handle_exceptions(func):
 def handle_exceptions(func):
     def inner_function(*args, **kwargs):
     def inner_function(*args, **kwargs):
         try:
         try:
@@ -25,6 +26,7 @@ def handle_exceptions(func):
             traceback.print_exc()
             traceback.print_exc()
     return inner_function
     return inner_function
 
 
+
 def handle_awaitable(func):
 def handle_awaitable(func):
     async def inner_function(*args, **kwargs):
     async def inner_function(*args, **kwargs):
         if asyncio.iscoroutinefunction(func):
         if asyncio.iscoroutinefunction(func):