Explorar o código

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 %!s(int64=3) %!d(string=hai) anos
pai
achega
a727e14fd2
Modificáronse 46 ficheiros con 259 adicións e 218 borrados
  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'`)
 - `reload`: automatically reload the ui on file changes (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`)
 
 ## Docker

+ 113 - 86
main.py

@@ -8,7 +8,7 @@ import docutils.core
 import re
 import asyncio
 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
 
 # add docutils css to webpage
@@ -16,11 +16,9 @@ page_stack[0].head_html += docutils.core.publish_parts('', writer_name='html')['
 
 @contextmanager
 def example(content: Union[Element, str]):
-
     callFrame = inspect.currentframe().f_back.f_back
     begin = callFrame.f_lineno
     with ui.row().classes('flex w-full'):
-
         if isinstance(content, str):
             ui.markdown(content).classes('mr-8 w-4/12')
         else:
@@ -35,7 +33,8 @@ def example(content: Union[Element, str]):
                 ui.label(content.__name__).classes('text-h5')
 
         with ui.card().classes('mt-12 w-2/12'):
-            yield
+            with ui.column():
+                yield
         callFrame = inspect.currentframe().f_back.f_back
         end = callFrame.f_lineno
         code = inspect.getsource(sys.modules[__name__])
@@ -43,6 +42,8 @@ def example(content: Union[Element, str]):
         code = [l[4:] for l in code]
         code.insert(0, '```python')
         code.insert(1, 'from nicegui import ui')
+        if code[2].split()[0] not in ['from', 'import']:
+            code.insert(2, '')
         code.append('')
         code.append('ui.run()')
         code.append('```')
@@ -75,73 +76,16 @@ with ui.row().classes('flex w-full'):
                 ui.label('Output:')
                 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):
-
     ui.label('some label')
 
 with example(ui.image):
-
     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='
     ui.image(base64).style('width:30px')
 
 with example(ui.svg):
-
     svg_content = '''
         <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" />
@@ -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.
 '''
 with example(overlay):
-
     with ui.image('http://placeimg.com/640/360/nature'):
         ui.label('nice').classes('absolute-bottom text-subtitle2 text-center')
 
@@ -171,15 +114,12 @@ with example(overlay):
         ui.svg(svg_content).style('background:transparent')
 
 with example(ui.markdown):
-
     ui.markdown('### Headline\nWith hyperlink to [GitHub](https://github.com/zauberzeug/nicegui).')
 
 with example(ui.html):
-
     ui.html('<p>demo paragraph in <strong>html</strong></p>')
 
 with example(ui.button):
-
     def button_increment():
         global button_count
         button_count += 1
@@ -190,26 +130,22 @@ with example(ui.button):
     button_result = ui.label('pressed: 0')
 
 with example(ui.checkbox):
-
     ui.checkbox('check me', on_change=lambda e: checkbox_state.set_text(e.value))
     with ui.row():
         ui.label('the checkbox is:')
         checkbox_state = ui.label('False')
 
 with example(ui.switch):
-
     ui.switch('switch me', on_change=lambda e: switch_state.set_text("ON" if e.value else'OFF'))
     with ui.row():
         ui.label('the switch is:')
         switch_state = ui.label('OFF')
 
 with example(ui.slider):
-
     slider = ui.slider(min=0, max=100, value=50).props('label')
     ui.label().bind_text_from(slider.value)
 
 with example(ui.input):
-
     ui.input(
         label='Text',
         placeholder='press ENTER to apply',
@@ -218,30 +154,25 @@ with example(ui.input):
     result = ui.label('')
 
 with example(ui.number):
-
     number_input = ui.number(label='Number', value=3.1415927, format='%.2f')
     with ui.row():
         ui.label('underlying value: ')
         ui.label().bind_text_from(number_input.value)
 
 with example(ui.radio):
-
     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)
 
 with example(ui.toggle):
-
     toggle = ui.toggle([1, 2, 3], value=1)
     ui.toggle({1: 'A', 2: 'B', 3: 'C'}, value=1).bind_value(toggle.value)
 
 with example(ui.select):
-
     with ui.row():
         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)
 
 with example(ui.upload):
-
     ui.upload(on_upload=lambda files: content.set_text(files))
     content = ui.label()
 
@@ -257,7 +188,6 @@ with example(ui.plot):
         plt.ylabel('Damped oscillation')
 
 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)
     line_updates = ui.timer(0.1, lambda: lines.push([datetime.now()], [
         [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]))
 
 with example(ui.scene):
-
     with ui.scene(width=200, height=200) as scene:
         scene.sphere().material('#4488ff')
         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)
 
 with example(ui.joystick):
-
     ui.joystick(
         color='blue',
         size=50,
@@ -302,7 +230,6 @@ with example(ui.joystick):
     coordinates = ui.label('0, 0')
 
 with example(ui.dialog):
-
     with ui.dialog() as dialog:
         with ui.card():
             ui.label('Hello world!')
@@ -311,19 +238,90 @@ with example(ui.dialog):
     ui.button('Open dialog', on_click=dialog.open)
 
 with example(ui.menu):
-
+    choice = ui.label('Try the 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')
 
 with example(ui.notify):
-
     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
 
 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`.
 '''
 with example(lifecycle):
-
     with ui.row() as row:
         ui.label('count:')
         count_label = ui.label('0')
@@ -348,11 +345,41 @@ with example(lifecycle):
     ui.on_startup(counter())
 
 with example(ui.page):
-
     with ui.page('/other_page') as other:
         ui.label('Welcome to the other side')
         ui.link('Back to main 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()

+ 1 - 1
nicegui/config.py

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

+ 0 - 1
nicegui/elements/bool_element.py

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

+ 0 - 6
nicegui/elements/button.py

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

+ 21 - 2
nicegui/elements/card.py

@@ -1,10 +1,29 @@
 import justpy as jp
+from .element import Design
 from .group import 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)

+ 0 - 1
nicegui/elements/choice_element.py

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

+ 14 - 2
nicegui/elements/column.py

@@ -1,10 +1,22 @@
 import justpy as jp
+from .element import Design
 from .group import 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)

+ 0 - 4
nicegui/elements/custom_example.py

@@ -4,7 +4,6 @@ from .element import Element
 class CustomExampleView(CustomView):
 
     def __init__(self, on_change):
-
         super().__init__('custom_example', __file__, value=0)
 
         self.on_change = on_change
@@ -12,7 +11,6 @@ class CustomExampleView(CustomView):
         self.initialize(temp=False, onAdd=self.handle_add)
 
     def handle_add(self, msg):
-
         self.options.value += msg.number
         if self.on_change is not None:
             return self.on_change(self.options.value)
@@ -21,10 +19,8 @@ class CustomExampleView(CustomView):
 class CustomExample(Element):
 
     def __init__(self, *, on_change=None):
-
         super().__init__(CustomExampleView(on_change))
 
     def add(self, number: str):
-
         self.view.options.value += number
         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
 
 class CustomView(jp.JustpyBaseComponent):
-
     vue_dependencies = []
 
     def __init__(self, vue_type, filepath, dependencies=[], **options):
-
         self.vue_type = vue_type
         self.vue_filepath = os.path.realpath(filepath).replace('.py', '.js')
         self.vue_dependencies = dependencies
@@ -21,7 +19,6 @@ class CustomView(jp.JustpyBaseComponent):
         super().__init__(temp=False)
 
     def add_page(self, wp: jp.WebPage):
-
         for dependency in self.vue_dependencies:
             is_remote = dependency.startswith('http://') or dependency.startswith('https://')
             src = dependency if is_remote else f'lib/{dependency}'
@@ -40,11 +37,9 @@ class CustomView(jp.JustpyBaseComponent):
         super().add_page(wp)
 
     def react(self, _):
-
         pass
 
     def convert_object_to_dict(self):
-
         return {
             'vue_type': self.vue_type,
             '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)
         """
-
         view = jp.QDialog(
             value=value,
             classes='row items-start bg-red-400',
@@ -23,9 +22,7 @@ class Dialog(Group):
         super().__init__(view)
 
     def open(self):
-
         self.view.value = True
 
     def close(self):
-
         self.view.value = False

+ 21 - 10
nicegui/elements/element.py

@@ -1,15 +1,14 @@
 import justpy as jp
+from enum import Enum
 from binding.binding import BindableProperty
 from ..globals import view_stack, page_stack
 
 class Element:
-
     visible = BindableProperty
 
     def __init__(self,
                  view: jp.HTMLBaseComponent,
                  ):
-
         self.parent_view = view_stack[-1]
         self.parent_view.add(view)
         self.view = view
@@ -20,22 +19,18 @@ class Element:
 
     @property
     def visible(self):
-
         return self.visible_
 
     @visible.setter
     def visible(self, visible: bool):
-
         self.visible_ = visible
         (self.view.remove_class if self.visible_ else self.view.set_class)('hidden')
 
     def bind_visibility_to(self, target, forward=lambda x: x):
-
         self.visible.bind_to(target, forward=forward, nesting=1)
         return self
 
     def bind_visibility_from(self, target, backward=lambda x: x, *, value=None):
-
         if value is not None:
             def backward(x): return x == value
 
@@ -43,7 +38,6 @@ class Element:
         return self
 
     def bind_visibility(self, target, forward=lambda x: x, backward=None, *, value=None):
-
         if value is not None:
             def backward(x): return x == value
 
@@ -51,7 +45,11 @@ class Element:
         return self
 
     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 = [c for c in class_list if c not in remove]
         class_list += add.split()
@@ -61,7 +59,11 @@ class Element:
         return self
 
     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 = [c for c in style_list if c not in remove.split(';')]
         style_list += add.split(';')
@@ -71,7 +73,12 @@ class Element:
         return self
 
     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():
             setattr(self.view, prop.split('=')[0], None)
 
@@ -82,3 +89,7 @@ class Element:
                 setattr(self.view, prop, True)
 
         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,
                  on_change: Callable,
                  ):
-
         self.format = format
 
         super().__init__(view, value=value, on_change=on_change)
 
     def value_to_view(self, value: float):
-
         if value is None:
             return None
         elif self.format is None:

+ 0 - 2
nicegui/elements/group.py

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

+ 0 - 4
nicegui/elements/html.py

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

+ 0 - 1
nicegui/elements/icon.py

@@ -6,7 +6,6 @@ class Icon(Element):
     def __init__(self,
                  name: str,
                  ):
-
         view = jp.QIcon(name=name, classes=f'q-pt-xs', size='20px')
 
         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
         """
-
         view = jp.QImg(src=source)
 
         super().__init__(view)
 
     @property
     def source(self):
-
         return self.view.src
 
     @source.setter
     def source(self, source: any):
-
         self.view.src = source
 
     def set_source(self, source: str):
-
         self.source = source
 
     def bind_source_to(self, target, forward=lambda x: x):
-
         self.source.bind_to(target, forward=forward, nesting=1)
         return self
 
     def bind_source_from(self, target, backward=lambda x: x):
-
         self.source.bind_from(target, backward=backward, nesting=1)
         return self
 
     def bind_source(self, target, forward=lambda x: x, backward=lambda x: x):
-
         self.source.bind(target, forward=forward, backward=backward, nesting=1)
         return self

+ 0 - 5
nicegui/elements/joystick.py

@@ -5,7 +5,6 @@ from .element import Element
 class JoystickView(CustomView):
 
     def __init__(self, on_start, on_move, on_end, **options):
-
         super().__init__('joystick', __file__, ['nipplejs.min.js'], **options)
 
         self.on_start = on_start
@@ -18,19 +17,16 @@ class JoystickView(CustomView):
                         onEnd=self.handle_end)
 
     def handle_start(self, msg):
-
         if self.on_start is not None:
             return self.on_start(msg)
         return False
 
     def handle_move(self, msg):
-
         if self.on_move is not None:
             return self.on_move(msg)
         return False
 
     def handle_end(self, msg):
-
         if self.on_end is not None:
             return self.on_end(msg)
         return False
@@ -53,5 +49,4 @@ class Joystick(Element):
         :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>`_
         """
-
         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
         """
-
         view = jp.Div(text=text)
 
         super().__init__(view)
 
     @property
     def text(self):
-
         return self.view.text
 
     @text.setter
     def text(self, text: any):
-
         self.view.text = text
 
     def set_text(self, text: str):
-
         self.text = text
 
     def bind_text_to(self, target, forward=lambda x: x):
-
         self.text.bind_to(target, forward=forward, nesting=1)
         return self
 
     def bind_text_from(self, target, backward=lambda x: x):
-
         self.text.bind_from(target, backward=backward, nesting=1)
         return self
 
     def bind_text(self, target, forward=lambda x: x, backward=lambda x: x):
-
         self.text.bind(target, forward=forward, backward=backward, nesting=1)
         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 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)
 
         self.x = []
@@ -32,13 +31,11 @@ class LinePlot(Plot):
         self.push_counter = 0
 
     def with_legend(self, titles: List[str], **kwargs):
-
         self.fig.gca().legend(titles, **kwargs)
         self.view.set_figure(self.fig)
         return self
 
     def push(self, x: List[float], Y: List[List[float]]):
-
         self.push_counter += 1
 
         self.x = [*self.x, *x][self.slice]

+ 0 - 1
nicegui/elements/link.py

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

+ 0 - 4
nicegui/elements/log.py

@@ -7,7 +7,6 @@ from .element import Element
 class LogView(CustomView):
 
     def __init__(self, max_lines: int):
-
         super().__init__('log', __file__, max_lines=max_lines)
 
 class Log(Element):
@@ -19,18 +18,15 @@ class Log(Element):
 
         :param max_lines: maximum number of lines before dropping oldest ones (default: None)
         """
-
         super().__init__(LogView(max_lines=max_lines))
 
         self.classes('border whitespace-pre font-mono').style('opacity: 1 !important; cursor: text !important')
 
     async def push_async(self, line: str):
-
         await asyncio.gather(*[
             self.view.run_method(f'push("{urllib.parse.quote(line)}")', socket)
             for socket in WebPage.sockets[self.page.page_id].values()
         ])
 
     def push(self, line: str):
-
         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
         """
-
         super().__init__(content)
 
     def set_content(self, content: str):
-
         html = markdown2.markdown(content, extras=['fenced-code-blocks'])
         # we need explicit markdown styling because tailwind css removes all default styles
         html = Markdown.apply_tailwind(html)
@@ -25,7 +23,6 @@ class Markdown(Html):
 
     @staticmethod
     def apply_tailwind(html: str):
-
         rep = {
             '<h1': '<h1 class="text-5xl mb-4 mt-6"',
             '<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,
                  *,
-                 value: bool = False
+                 value: bool = False,
                  ):
         """Menu
 
@@ -13,15 +13,12 @@ class Menu(Group):
 
         :param value: whether the menu is already opened (default: False)
         """
-
         view = jp.QMenu(value=value)
 
         super().__init__(view)
 
     def open(self):
-
         self.view.value = True
 
     def close(self):
-
         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,
                  *,
                  position: str = 'bottom',
-                 close_button: str = None
+                 close_button: str = None,
                  ):
         """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 close_button: optional label of a button to dismiss the notification (default: None)
         """
-
         view = jp.QNotify(message=message, position=position, closeBtn=close_button)
 
         super().__init__(view)
         asyncio.get_event_loop().create_task(self.notify_async())
 
     async def notify_async(self):
-
         self.view.notify = True
         await self.parent_view.update()
         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 on_change: callback to execute when the input is confirmed by leaving the focus
         """
-
         view = jp.QInput(
             type='number',
             label=label,
@@ -31,7 +30,6 @@ class Number(FloatElement):
         super().__init__(view, value=value, format=format, on_change=on_change)
 
     def handle_change(self, msg):
-
         msg['value'] = float(msg['value'])
 
         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 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.fig = plt.figure(**kwargs)
 
@@ -26,13 +25,11 @@ class Plot(Element):
         super().__init__(view)
 
     def __enter__(self):
-
         plt.figure(self.fig)
 
         return self
 
     def __exit__(self, *_):
-
         self.view.set_figure(plt.gcf())
 
         if self.close:

+ 0 - 1
nicegui/elements/radio.py

@@ -16,7 +16,6 @@ class Radio(ChoiceElement):
         :param value: the inital value
         :param on_change: callback to execute when selection changes
         """
-
         view = jp.QOptionGroup(options=options, input=self.handle_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
+from .element import Design
 from .group import 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)

+ 0 - 1
nicegui/elements/scene.py

@@ -34,7 +34,6 @@ class SceneView(CustomView):
             traceback.print_exc()
 
 class Scene(Element):
-
     from .scene_objects import Group as group
     from .scene_objects import Box as box
     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
 
 class Object3D:
-
     stack: list[Object3D] = []
 
     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 .scene_object3d import Object3D
 
@@ -46,7 +47,7 @@ class Cylinder(Object3D):
 class Extrusion(Object3D):
 
     def __init__(self,
-                 outline: list[tuple[float, float]],
+                 outline: list[list[float, float]],
                  height: float,
                  wireframe: bool = False,
                  ):
@@ -63,18 +64,18 @@ class Stl(Object3D):
 class Line(Object3D):
 
     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)
 
 class Curve(Object3D):
 
     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,
                  ):
         super().__init__('curve', start, control1, control2, end, num_points)
@@ -83,6 +84,6 @@ class Texture(Object3D):
 
     def __init__(self,
                  url: str,
-                 coordinates: list[list[Optional[tuple[float]]]],
+                 coordinates: list[list[Optional[list[float]]]],
                  ):
         super().__init__('texture', url, coordinates)

+ 0 - 3
nicegui/elements/select.py

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

+ 0 - 1
nicegui/elements/string_element.py

@@ -10,5 +10,4 @@ class StringElement(ValueElement):
                  value: float,
                  on_change: Callable,
                  ):
-
         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
         """
-
         view = jp.Div(style="padding:0;width:100%;height:100%")
         super().__init__(view)
         self.content = content
 
     @property
     def content(self):
-
         return self.view.inner_html()
 
     @content.setter
     def content(self, content: any):
-
         self.view.components = []
         jp.parse_html(content, a=self.view)
 
     def set_content(self, content: str):
-
         self.content = content
 
     def bind_content_to(self, target, forward=lambda x: x):
-
         self.content.bind_to(target, forward=forward, nesting=1)
         return self
 
     def bind_content_from(self, target, backward=lambda x: x):
-
         self.content.bind_from(target, backward=backward, nesting=1)
         return self
 
     def bind_content(self, target, forward=lambda x: x, backward=lambda x: x):
-
         self.content.bind(target, forward=forward, backward=backward, nesting=1)
         return self

+ 0 - 1
nicegui/elements/toggle.py

@@ -16,7 +16,6 @@ class Toggle(ChoiceElement):
         :param value: the inital value
         :param on_change: callback to execute when selection changes
         """
-
         view = jp.QBtnToggle(input=self.handle_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)
 
     def submit(self, _, msg):
-
         for form_data in msg.form_data:
             if form_data.type == 'file':
                 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
 
 class ValueElement(Element):
-
     value = BindableProperty()
 
     def __init__(self,
@@ -15,7 +14,6 @@ class ValueElement(Element):
                  value: Any,
                  on_change: Callable,
                  ):
-
         super().__init__(view)
 
         self.on_change = on_change
@@ -23,11 +21,9 @@ class ValueElement(Element):
         self.value.bind_to(self.view.value, forward=self.value_to_view)
 
     def value_to_view(self, value):
-
         return value
 
     def handle_change(self, msg):
-
         self.value = msg['value']
 
         if self.on_change is not None:
@@ -40,16 +36,13 @@ class ValueElement(Element):
                 traceback.print_exc()
 
     def bind_value_to(self, target, forward=lambda x: x):
-
         self.value.bind_to(target, forward=forward, nesting=1)
         return self
 
     def bind_value_from(self, target, backward=lambda x: x):
-
         self.value.bind_from(target, backward=backward, nesting=1)
         return self
 
     def bind_value(self, target, forward=lambda x: x, backward=lambda x: x):
-
         self.value.bind(target, forward=forward, backward=backward, nesting=1)
         return self

+ 0 - 2
nicegui/lifecycle.py

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

+ 16 - 0
nicegui/routes.py

@@ -1,4 +1,20 @@
+from starlette.routing import Route
 from . import globals
 
 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)
+
+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:
         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()
 
-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 show:
             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:
 
 ```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:

+ 0 - 3
nicegui/timer.py

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

+ 3 - 2
nicegui/ui.py

@@ -1,5 +1,4 @@
 class Ui:
-
     from .config import config  # NOTE: before run
     from .run import run  # NOTE: before justpy
 
@@ -17,6 +16,7 @@ class Ui:
     from .elements.log import Log as log
     from .elements.markdown import Markdown as markdown
     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.number import Number as number
     from .elements.page import Page as page
@@ -35,9 +35,10 @@ class Ui:
     from .elements.row import Row as row
     from .elements.column import Column as column
     from .elements.card import Card as card
+    from .elements.card import CardSection as card_section
 
     from .timer import Timer as timer
 
     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:
 
     def __init__(self, sender, **kwargs):
-
         self.sender = sender
         for key, value in kwargs.items():
             setattr(self, key, value)
 
+
 def provide_arguments(func, *keys):
     def inner_function(sender, event):
         try:
@@ -17,6 +17,7 @@ def provide_arguments(func, *keys):
             return func(EventArguments(sender, **{key: event[key] for key in keys}))
     return inner_function
 
+
 def handle_exceptions(func):
     def inner_function(*args, **kwargs):
         try:
@@ -25,6 +26,7 @@ def handle_exceptions(func):
             traceback.print_exc()
     return inner_function
 
+
 def handle_awaitable(func):
     async def inner_function(*args, **kwargs):
         if asyncio.iscoroutinefunction(func):