Ver código fonte

split ui elements into individual classes; typing

Falko Schindler 4 anos atrás
pai
commit
f4e395f9e1

+ 1 - 1
nice_gui/__init__.py

@@ -1 +1 @@
-from nice_gui.nice_gui import ui
+from nice_gui.nice_gui import app, ui

+ 19 - 0
nice_gui/elements/button.py

@@ -0,0 +1,19 @@
+from typing import Callable
+import justpy as jp
+from .element import Element
+from ..utils import handle_exceptions, provide_arguments
+
+class Button(Element):
+
+    def __init__(self, text: str, icon: str = None, icon_right: str = None, on_click: Callable = None):
+
+        view = jp.QButton(label=text, color='primary')
+
+        if icon is not None:
+            view.icon = icon
+        if icon_right is not None:
+            view.icon_right = icon_right
+        if on_click is not None:
+            view.on('click', handle_exceptions(provide_arguments(on_click)))
+
+        super().__init__(view)

+ 10 - 0
nice_gui/elements/card.py

@@ -0,0 +1,10 @@
+import justpy as jp
+from .group import Group
+
+class Card(Group):
+
+    def __init__(self):
+
+        view = jp.QCard(classes='column items-start q-pa-md', style='gap: 1em', delete_flag=False)
+
+        super().__init__(view)

+ 14 - 0
nice_gui/elements/checkbox.py

@@ -0,0 +1,14 @@
+from typing import Callable
+import justpy as jp
+from .element import Element
+from ..utils import handle_exceptions, provide_arguments
+
+class Checkbox(Element):
+
+    def __init__(self, text: str, on_change: Callable = None):
+
+        view = jp.QCheckbox(text=text)
+        if on_change is not None:
+            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
+
+        super().__init__(view)

+ 10 - 0
nice_gui/elements/column.py

@@ -0,0 +1,10 @@
+import justpy as jp
+from .group import Group
+
+class Column(Group):
+
+    def __init__(self):
+
+        view = jp.QDiv(classes='column items-start', style='gap: 1em', delete_flag=False)
+
+        super().__init__(view)

+ 12 - 0
nice_gui/elements/element.py

@@ -0,0 +1,12 @@
+import justpy as jp
+
+class Element:
+
+    wp: None
+    view_stack = []
+
+    def __init__(self, view: jp.HTMLBaseComponent):
+
+        self.view_stack[-1].add(view)
+        view.add_page(self.wp)
+        self.view = view

+ 11 - 0
nice_gui/elements/group.py

@@ -0,0 +1,11 @@
+from .element import Element
+
+class Group(Element):
+
+    def __enter__(self):
+
+        self.view_stack.append(self.view)
+
+    def __exit__(self, *_):
+
+        self.view_stack.pop()

+ 10 - 0
nice_gui/elements/icon.py

@@ -0,0 +1,10 @@
+import justpy as jp
+from .element import Element
+
+class Icon(Element):
+
+    def __init__(self, name: str, size: str = '20px', color: str = 'dark'):
+
+        view = jp.QIcon(name=name, classes=f'q-pt-xs text-{color}', size=size)
+
+        super().__init__(view)

+ 17 - 0
nice_gui/elements/input.py

@@ -0,0 +1,17 @@
+import justpy as jp
+from typing import Callable, Union
+from .element import Element
+from ..utils import handle_exceptions, provide_arguments
+
+class Input(Element):
+
+    def __init__(self, placeholder: str = None, value: Union[str, float] = None, type: str = 'text', on_change: Callable = None):
+
+        view = jp.QInput(placeholder=placeholder, type=type)
+
+        if value is not None:
+            view.value = value
+        if on_change is not None:
+            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
+
+        super().__init__(view)

+ 19 - 0
nice_gui/elements/label.py

@@ -0,0 +1,19 @@
+import justpy as jp
+from typing import List
+from .element import Element
+
+class Label(Element):
+
+    def __init__(self, text: str = '', typography: List[str] = []):
+
+        if isinstance(typography, str):
+            typography = [typography]
+        classes = ' '.join('text-' + t for t in typography)
+
+        view = jp.Div(text=text, classes=classes)
+
+        super().__init__(view)
+
+    def set_text(self, text: str):
+
+        self.view.text = text

+ 31 - 0
nice_gui/elements/line_plot.py

@@ -0,0 +1,31 @@
+from typing import List
+from .plot import Plot
+
+class LinePlot(Plot):
+
+    def __init__(self, n: int = 1, limit: int = 100, close: bool = True):
+
+        super().__init__(close)
+
+        self.x = []
+        self.Y = [[] for _ in range(n)]
+        self.lines = [self.fig.gca().plot([], [])[0] for _ in range(n)]
+        self.slice = slice(0 if limit is None else -limit, None)
+
+    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.x = [*self.x, *x][self.slice]
+        for i in range(len(self.lines)):
+            self.Y[i] = [*self.Y[i], *Y[i]][self.slice]
+            self.lines[i].set_xdata(self.x)
+            self.lines[i].set_ydata(self.Y[i])
+        flat_y = [y_i for y in self.Y for y_i in y]
+        self.fig.gca().set_xlim(min(self.x), max(self.x))
+        self.fig.gca().set_ylim(min(flat_y), max(flat_y))
+        self.view.set_figure(self.fig)

+ 15 - 0
nice_gui/elements/link.py

@@ -0,0 +1,15 @@
+import justpy as jp
+from typing import List
+from .element import Element
+
+class Link(Element):
+
+    def __init__(self, text: str = '', href: str = '#', typography: List[str] = []):
+
+        if isinstance(typography, str):
+            typography = [typography]
+
+        classes = ' '.join('text-' + t for t in typography)
+        view = jp.A(text=text, href=href, classes=classes)
+
+        super().__init__(view)

+ 29 - 0
nice_gui/elements/plot.py

@@ -0,0 +1,29 @@
+from nice_gui.elements.element import Element
+import justpy as jp
+import matplotlib.pyplot as plt
+from .element import Element
+
+class Plot(Element):
+
+    def __init__(self, close: bool = True):
+
+        self.close = close
+        self.fig = plt.figure()
+
+        view = jp.Matplotlib()
+        view.set_figure(self.fig)
+
+        super().__init__(view)
+
+    def __enter__(self):
+
+        plt.figure(self.fig)
+
+        return self
+
+    def __exit__(self, *_):
+
+        self.view.set_figure(plt.gcf())
+
+        if self.close:
+            self.fig.close()

+ 15 - 0
nice_gui/elements/radio.py

@@ -0,0 +1,15 @@
+import justpy as jp
+from typing import Callable, List
+from .element import Element
+from ..utils import handle_exceptions, provide_arguments
+
+class Radio(Element):
+
+    def __init__(self, options: List[str], value: str = None, on_change: Callable = None):
+
+        view = jp.QOptionGroup(value=value, options=[{'label': o, 'value': o} for o in options])
+
+        if on_change is not None:
+            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
+
+        super().__init__(view)

+ 10 - 0
nice_gui/elements/row.py

@@ -0,0 +1,10 @@
+import justpy as jp
+from .group import Group
+
+class Row(Group):
+
+    def __init__(self):
+
+        view = jp.QDiv(classes='row items-start', style='gap: 1em', delete_flag=False)
+
+        super().__init__(view)

+ 15 - 0
nice_gui/elements/select.py

@@ -0,0 +1,15 @@
+import justpy as jp
+from typing import Callable, List
+from .element import Element
+from ..utils import handle_exceptions, provide_arguments
+
+class Select(Element):
+
+    def __init__(self, options: List[str], value: str = None, on_change: Callable = None):
+
+        view = jp.QSelect(value=value, options=options)
+
+        if on_change is not None:
+            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
+
+        super().__init__(view)

+ 15 - 0
nice_gui/elements/slider.py

@@ -0,0 +1,15 @@
+from typing import Callable
+import justpy as jp
+from .element import Element
+from ..utils import handle_exceptions, provide_arguments
+
+class Slider(Element):
+
+    def __init__(self, min: float, max: float, on_change: Callable = None):
+
+        view = jp.QSlider(min=min, max=max)
+
+        if on_change is not None:
+            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
+
+        super().__init__(view)

+ 15 - 0
nice_gui/elements/switch.py

@@ -0,0 +1,15 @@
+from typing import Callable
+import justpy as jp
+from .element import Element
+from ..utils import handle_exceptions, provide_arguments
+
+class Switch(Element):
+
+    def __init__(self, text: str = '', on_change: Callable = None):
+
+        view = jp.QToggle(text=text)
+
+        if on_change is not None:
+            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
+
+        super().__init__(view)

+ 16 - 222
nice_gui/nice_gui.py

@@ -1,242 +1,36 @@
 #!/usr/bin/env python3
-import traceback
 import justpy as jp
-from starlette.applications import Starlette
 import uvicorn
 import sys
 import inspect
-import time
-import asyncio
-from contextlib import contextmanager
-from matplotlib import pyplot as plt
-from .utils import handle_exceptions, provide_arguments
+from .ui import Ui
+from .timer import Timer
+from .elements.element import Element
 
 # start uvicorn with auto-reload; afterwards the auto-reloaded process should not start uvicorn again
 if not inspect.stack()[-2].filename.endswith('spawn.py'):
-    uvicorn.run('nice_gui:ui', host='0.0.0.0', port=80, lifespan='on', reload=True)
+    uvicorn.run('nice_gui:app', host='0.0.0.0', port=80, lifespan='on', reload=True)
     sys.exit()
 
 wp = jp.QuasarPage(delete_flag=False, title='Nice GUI', favicon='favicon.png')
-wp.head_html = '<script>confirm = () => true;</script>'  # HACK: avoid confirmation dialog for reload
+wp.head_html = '<script>confirm = () => true;</script>'  # avoid confirmation dialog for reload
 
 main = jp.Div(a=wp, classes='q-ma-md column items-start', style='row-gap: 1em')
 main.add_page(wp)
 jp.justpy(lambda: wp, start_server=False)
 
-view_stack = [main]
+@jp.app.on_event('startup')
+def startup():
+    [jp.run_task(t) for t in Timer.tasks]
 
-class Element:
+Element.wp = wp
+Element.view_stack = [main]
 
-    def __init__(self, view: jp.HTMLBaseComponent):
-
-        view_stack[-1].add(view)
-        view.add_page(wp)
-        self.view = view
-
-    @property
-    def text(self):
-        return self.view.text
-
-    @text.setter
-    def text(self, text):
-        self.view.text = text
-
-    def set_text(self, text):
-        self.view.text = text
-
-    def __enter__(self):
-
-        view_stack.append(self.view)
-
-    def __exit__(self, *_):
-
-        view_stack.pop()
-
-class Plot(Element):
-
-    def __init__(self, view, fig):
-
-        super().__init__(view)
-        self.fig = fig
-
-    def __enter__(self):
-
-        plt.figure(self.fig)
-
-    def __exit__(self, *_):
-
-        self.view.set_figure(plt.gcf())
-
-class LinePlot(Plot):
-
-    def __init__(self, view, fig, n, limit):
-
-        super().__init__(view, fig)
-        self.x = []
-        self.Y = [[] for _ in range(n)]
-        self.lines = [self.fig.gca().plot([], [])[0] for _ in range(n)]
-        self.slice = slice(0 if limit is None else -limit, None)
-
-    def with_legend(self, titles, **kwargs):
-
-        self.fig.gca().legend(titles, **kwargs)
-        self.view.set_figure(self.fig)
-        return self
-
-    def push(self, x, Y):
-
-        self.x = [*self.x, *x][self.slice]
-        for i in range(len(self.lines)):
-            self.Y[i] = [*self.Y[i], *Y[i]][self.slice]
-            self.lines[i].set_xdata(self.x)
-            self.lines[i].set_ydata(self.Y[i])
-        flat_y = [y_i for y in self.Y for y_i in y]
-        self.fig.gca().set_xlim(min(self.x), max(self.x))
-        self.fig.gca().set_ylim(min(flat_y), max(flat_y))
-        self.view.set_figure(self.fig)
-
-class Ui(Starlette):
-
-    def __init__(self):
-        # NOTE: we enhance our own ui object with all capabilities of jp.app
-        self.__dict__.update(jp.app.__dict__)
-
-        self.tasks = []
-
-        @self.on_event('startup')
-        def startup():
-            [jp.run_task(t) for t in self.tasks]
-
-    def label(self, text='', typography=[]):
-
-        if isinstance(typography, str):
-            typography = [typography]
-        classes = ' '.join('text-' + t for t in typography)
-        view = jp.Div(text=text, classes=classes)
-        return Element(view)
-
-    def link(self, text='', href='#', typography=[]):
-
-        if isinstance(typography, str):
-            typography = [typography]
-        classes = ' '.join('text-' + t for t in typography)
-        view = jp.A(text=text, href=href, classes=classes)
-        return Element(view)
-
-    def icon(self, name, size='20px', color='dark'):
-
-        view = jp.QIcon(name=name, classes=f'q-pt-xs text-{color}', size=size)
-        return Element(view)
-
-    def button(self, text, icon=None, icon_right=None, on_click=None):
-
-        view = jp.QBtn(label=text, color='primary')
-        if icon is not None:
-            view.icon = icon
-        if icon_right is not None:
-            view.icon_right = icon_right
-        if on_click is not None:
-            view.on('click', handle_exceptions(provide_arguments(on_click)))
-        return Element(view)
-
-    def checkbox(self, text, on_change=None):
-
-        view = jp.QCheckbox(text=text)
-        if on_change is not None:
-            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
-        return Element(view)
-
-    def switch(self, text, on_change=None):
-
-        view = jp.QToggle(text=text)
-        if on_change is not None:
-            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
-        return Element(view)
-
-    def radio(self, options, value=None, on_change=None):
-
-        view = jp.QOptionGroup(value=value, options=[{'label': o, 'value': o} for o in options])
-        if on_change is not None:
-            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
-        return Element(view)
-
-    def select(self, options, value=None, on_change=None):
-
-        view = jp.QSelect(value=value, options=options)
-        if on_change is not None:
-            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
-        return Element(view)
-
-    def slider(self, min, max, on_change=None):
-
-        view = jp.QSlider(min=min, max=max)
-        if on_change is not None:
-            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
-        return Element(view)
-
-    def input(self, placeholder=None, value=None, type='text', on_change=None):
-
-        view = jp.QInput(placeholder=placeholder, type=type)
-        if value is not None:
-            view.value = value
-        if on_change is not None:
-            view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
-        return Element(view)
-
-    @contextmanager
-    def plot(self, close=True):
-
-        fig = plt.figure()
-        view = jp.Matplotlib()
-        yield Plot(view, fig)
-        view.set_figure(fig)
-        if close:
-            fig.close()
-
-    def line_plot(self, n=1, limit=20):
-
-        fig = plt.figure()
-        view = jp.Matplotlib(fig=fig)
-        return LinePlot(view, fig, n=n, limit=limit)
-
-    def row(self):
-
-        view = jp.QDiv(classes='row items-start', style='gap: 1em', delete_flag=False)
-        return Element(view)
-
-    def column(self):
-
-        view = jp.QDiv(classes='column items-start', style='gap: 1em', delete_flag=False)
-        return Element(view)
-
-    def card(self):
-
-        view = jp.QCard(classes='column items-start q-pa-md', style='gap: 1em', delete_flag=False)
-        return Element(view)
-
-    def timer(self, interval, callback, *, once=False):
-
-        parent = view_stack[-1]
-
-        async def timeout():
-
-            await asyncio.sleep(interval)
-            handle_exceptions(callback)()
-            await parent.update()
-
-        async def loop():
-
-            while True:
-                try:
-                    start = time.time()
-                    handle_exceptions(callback)()
-                    await parent.update()
-                    dt = time.time() - start
-                    await asyncio.sleep(interval - dt)
-                except:
-                    traceback.print_exc()
-                    await asyncio.sleep(interval)
+app = jp.app
+ui = Ui()
 
-        self.tasks.append((timeout() if once else loop()))
+# def line_plot(self, n=1, limit=20):
 
-ui = Ui()
+#     fig = plt.figure()
+#     view = jp.Matplotlib(fig=fig)
+#     return LinePlot(view, fig, n=n, limit=limit)

+ 34 - 0
nice_gui/timer.py

@@ -0,0 +1,34 @@
+import asyncio
+import time
+import traceback
+from .elements.element import Element
+from .utils import handle_exceptions
+
+class Timer:
+
+    tasks = []
+
+    def __init__(self, interval, callback, *, once=False):
+
+        parent = Element.view_stack[-1]
+
+        async def timeout():
+
+            await asyncio.sleep(interval)
+            handle_exceptions(callback)()
+            await parent.update()
+
+        async def loop():
+
+            while True:
+                try:
+                    start = time.time()
+                    handle_exceptions(callback)()
+                    await parent.update()
+                    dt = time.time() - start
+                    await asyncio.sleep(interval - dt)
+                except:
+                    traceback.print_exc()
+                    await asyncio.sleep(interval)
+
+        self.tasks.append(timeout() if once else loop())

+ 21 - 0
nice_gui/ui.py

@@ -0,0 +1,21 @@
+class Ui:
+
+    from .elements.button import Button as button
+    from .elements.checkbox import Checkbox as checkbox
+    from .elements.icon import Icon as icon
+    from .elements.input import Input as input
+    from .elements.label import Label as label
+    from .elements.link import Link as link
+    from .elements.radio import Radio as radio
+    from .elements.select import Select as select
+    from .elements.slider import Slider as slider
+    from .elements.switch import Switch as switch
+
+    from .elements.plot import Plot as plot
+    from .elements.line_plot import LinePlot as line_plot
+
+    from .elements.row import Row as row
+    from .elements.column import Column as column
+    from .elements.card import Card as card
+
+    from .timer import Timer as timer