nice_gui.py 6.7 KB

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