nice_gui.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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. import icecream
  13. icecream.install()
  14. wp = jp.QuasarPage(delete_flag=False, title='Nice GUI', favicon='favicon.png')
  15. main = jp.Div(a=wp, classes='q-ma-md column items-start', style='row-gap: 1em')
  16. main.add_page(wp)
  17. jp.justpy(lambda: wp, start_server=False)
  18. view_stack = [main]
  19. class Element:
  20. def __init__(self, view: jp.HTMLBaseComponent):
  21. view_stack[-1].add(view)
  22. view.add_page(wp)
  23. self.view = view
  24. @property
  25. def text(self):
  26. return self.view.text
  27. @text.setter
  28. def text(self, text):
  29. self.view.text = text
  30. def set_text(self, text):
  31. self.view.text = text
  32. def __enter__(self):
  33. view_stack.append(self.view)
  34. def __exit__(self, *_):
  35. view_stack.pop()
  36. class Plot(Element):
  37. def update(self, close=True):
  38. self.view.set_figure(plt.gcf())
  39. if close:
  40. plt.close()
  41. class Ui(Starlette):
  42. def label(self, text='', typography=[]):
  43. if isinstance(typography, str):
  44. typography = [typography]
  45. classes = ' '.join('text-' + t for t in typography)
  46. view = jp.Div(text=text, classes=classes)
  47. return Element(view)
  48. def button(self, text, on_click=None):
  49. view = jp.QBtn(text=text, color='primary')
  50. if on_click is not None:
  51. view.on('click', handle_exceptions(provide_arguments(on_click)))
  52. return Element(view)
  53. def checkbox(self, text, on_change=None):
  54. view = jp.QCheckbox(text=text)
  55. if on_change is not None:
  56. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  57. return Element(view)
  58. def switch(self, text, on_change=None):
  59. view = jp.QToggle(text=text)
  60. if on_change is not None:
  61. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  62. return Element(view)
  63. def radio(self, options, value=None, on_change=None):
  64. view = jp.QOptionGroup(value=value, options=[{'label': o, 'value': o} for o in options])
  65. if on_change is not None:
  66. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  67. return Element(view)
  68. def select(self, options, value=None, on_change=None):
  69. view = jp.QSelect(value=value, options=options)
  70. if on_change is not None:
  71. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  72. return Element(view)
  73. def slider(self, min, max, on_change=None):
  74. view = jp.QSlider(min=min, max=max)
  75. if on_change is not None:
  76. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  77. return Element(view)
  78. def input(self, placeholder=None, value=None, type='text', on_change=None):
  79. view = jp.QInput(placeholder=placeholder, type=type)
  80. if value is not None:
  81. view.value = value
  82. if on_change is not None:
  83. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  84. return Element(view)
  85. @contextmanager
  86. def plot(self, close=True):
  87. view = jp.Matplotlib()
  88. yield Plot(view)
  89. view.set_figure(plt.gcf())
  90. if close:
  91. plt.close()
  92. def row(self):
  93. view = jp.QDiv(classes='row items-start', style='gap: 1em', delete_flag=False)
  94. return Element(view)
  95. def column(self):
  96. view = jp.QDiv(classes='column items-start', style='gap: 1em', delete_flag=False)
  97. return Element(view)
  98. def card(self):
  99. view = jp.QCard(classes='column items-start q-pa-md', style='gap: 1em', delete_flag=False)
  100. return Element(view)
  101. def timer(self, interval, callback, *, once=False):
  102. parent = view_stack[-1]
  103. async def timeout():
  104. await asyncio.sleep(interval)
  105. handle_exceptions(callback)()
  106. await main.update()
  107. async def loop():
  108. while True:
  109. try:
  110. start = time.time()
  111. handle_exceptions(callback)()
  112. await parent.update()
  113. dt = time.time() - start
  114. await asyncio.sleep(interval - dt)
  115. except:
  116. traceback.print_exc()
  117. await asyncio.sleep(interval)
  118. jp.run_task(timeout() if once else loop())
  119. def run(self):
  120. # NOTE: prevent reloader from restarting uvicorn
  121. if inspect.stack()[-2].filename.endswith('spawn.py'):
  122. return
  123. uvicorn.run('nice_gui:ui', host='0.0.0.0', port=80, lifespan='on', reload=True)
  124. # NOTE: instantiate our own ui object with all capabilities of jp.app
  125. ui = Ui()
  126. ui.__dict__.update(jp.app.__dict__)