nice_gui.py 6.4 KB

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