nice_gui.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. #!/usr/bin/env python3
  2. import traceback
  3. import justpy as jp
  4. from starlette.applications import Starlette
  5. import uvicorn
  6. import inspect
  7. import time
  8. import asyncio
  9. from contextlib import contextmanager
  10. from matplotlib import pyplot as plt
  11. from .utils import handle_exceptions, provide_arguments
  12. wp = jp.QuasarPage(delete_flag=False, title='Nice GUI', favicon='favicon.png')
  13. wp.head_html = '<script>confirm = () => true;</script>' # HACK: avoid confirmation dialog for reload
  14. main = jp.Div(a=wp, classes='q-ma-md column items-start', style='row-gap: 1em')
  15. main.add_page(wp)
  16. jp.justpy(lambda: wp, start_server=False)
  17. view_stack = [main]
  18. class Element:
  19. def __init__(self, view: jp.HTMLBaseComponent):
  20. view_stack[-1].add(view)
  21. view.add_page(wp)
  22. self.view = view
  23. @property
  24. def text(self):
  25. return self.view.text
  26. @text.setter
  27. def text(self, text):
  28. self.view.text = text
  29. def set_text(self, text):
  30. self.view.text = text
  31. def __enter__(self):
  32. view_stack.append(self.view)
  33. def __exit__(self, *_):
  34. view_stack.pop()
  35. class Plot(Element):
  36. def __init__(self, view, fig):
  37. super().__init__(view)
  38. self.fig = fig
  39. def __enter__(self):
  40. plt.figure(self.fig)
  41. def __exit__(self, *_):
  42. self.view.set_figure(plt.gcf())
  43. class LinePlot(Plot):
  44. def __init__(self, view, fig, n, limit):
  45. super().__init__(view, fig)
  46. self.x = []
  47. self.Y = [[] for _ in range(n)]
  48. self.lines = [self.fig.gca().plot([], [])[0] for _ in range(n)]
  49. self.slice = slice(0 if limit is None else -limit, None)
  50. def with_legend(self, titles, **kwargs):
  51. self.fig.gca().legend(titles, **kwargs)
  52. self.view.set_figure(self.fig)
  53. return self
  54. def push(self, x, Y):
  55. self.x = [*self.x, *x][self.slice]
  56. for i in range(len(self.lines)):
  57. self.Y[i] = [*self.Y[i], *Y[i]][self.slice]
  58. self.lines[i].set_xdata(self.x)
  59. self.lines[i].set_ydata(self.Y[i])
  60. flat_y = [y_i for y in self.Y for y_i in y]
  61. self.fig.gca().set_xlim(min(self.x), max(self.x))
  62. self.fig.gca().set_ylim(min(flat_y), max(flat_y))
  63. self.view.set_figure(self.fig)
  64. class Ui(Starlette):
  65. def label(self, text='', typography=[]):
  66. if isinstance(typography, str):
  67. typography = [typography]
  68. classes = ' '.join('text-' + t for t in typography)
  69. view = jp.Div(text=text, classes=classes)
  70. return Element(view)
  71. def link(self, text='', href='#', typography=[]):
  72. if isinstance(typography, str):
  73. typography = [typography]
  74. classes = ' '.join('text-' + t for t in typography)
  75. view = jp.A(text=text, href=href, classes=classes)
  76. return Element(view)
  77. def icon(self, name, size='20px', color='dark'):
  78. view = jp.QIcon(name=name, classes=f'q-pt-xs text-{color}', size=size)
  79. return Element(view)
  80. def button(self, text, icon=None, icon_right=None, on_click=None):
  81. view = jp.QBtn(label=text, color='primary')
  82. if icon is not None:
  83. view.icon = icon
  84. if icon_right is not None:
  85. view.icon_right = icon_right
  86. if on_click is not None:
  87. view.on('click', handle_exceptions(provide_arguments(on_click)))
  88. return Element(view)
  89. def checkbox(self, text, on_change=None):
  90. view = jp.QCheckbox(text=text)
  91. if on_change is not None:
  92. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  93. return Element(view)
  94. def switch(self, text, on_change=None):
  95. view = jp.QToggle(text=text)
  96. if on_change is not None:
  97. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  98. return Element(view)
  99. def radio(self, options, value=None, on_change=None):
  100. view = jp.QOptionGroup(value=value, options=[{'label': o, 'value': o} for o in options])
  101. if on_change is not None:
  102. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  103. return Element(view)
  104. def select(self, options, value=None, on_change=None):
  105. view = jp.QSelect(value=value, options=options)
  106. if on_change is not None:
  107. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  108. return Element(view)
  109. def slider(self, min, max, on_change=None):
  110. view = jp.QSlider(min=min, max=max)
  111. if on_change is not None:
  112. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  113. return Element(view)
  114. def input(self, placeholder=None, value=None, type='text', on_change=None):
  115. view = jp.QInput(placeholder=placeholder, type=type)
  116. if value is not None:
  117. view.value = value
  118. if on_change is not None:
  119. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  120. return Element(view)
  121. @contextmanager
  122. def plot(self, close=True):
  123. fig = plt.figure()
  124. view = jp.Matplotlib()
  125. yield Plot(view, fig)
  126. view.set_figure(fig)
  127. if close:
  128. fig.close()
  129. def line_plot(self, n=1, limit=20):
  130. fig = plt.figure()
  131. view = jp.Matplotlib(fig=fig)
  132. return LinePlot(view, fig, n=n, limit=limit)
  133. def row(self):
  134. view = jp.QDiv(classes='row items-start', style='gap: 1em', delete_flag=False)
  135. return Element(view)
  136. def column(self):
  137. view = jp.QDiv(classes='column items-start', style='gap: 1em', delete_flag=False)
  138. return Element(view)
  139. def card(self):
  140. view = jp.QCard(classes='column items-start q-pa-md', style='gap: 1em', delete_flag=False)
  141. return Element(view)
  142. def timer(self, interval, callback, *, once=False):
  143. parent = view_stack[-1]
  144. async def timeout():
  145. await asyncio.sleep(interval)
  146. handle_exceptions(callback)()
  147. await parent.update()
  148. async def loop():
  149. while True:
  150. try:
  151. start = time.time()
  152. handle_exceptions(callback)()
  153. await parent.update()
  154. dt = time.time() - start
  155. await asyncio.sleep(interval - dt)
  156. except:
  157. traceback.print_exc()
  158. await asyncio.sleep(interval)
  159. jp.run_task(timeout() if once else loop())
  160. def run(self):
  161. # NOTE: prevent reloader from restarting uvicorn
  162. if inspect.stack()[-2].filename.endswith('spawn.py'):
  163. return
  164. uvicorn.run('nice_gui:ui', host='0.0.0.0', port=80, lifespan='on', reload=True)
  165. # NOTE: instantiate our own ui object with all capabilities of jp.app
  166. ui = Ui()
  167. ui.__dict__.update(jp.app.__dict__)