nice_gui.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. #!/usr/bin/env python3
  2. import justpy as jp
  3. from starlette.applications import Starlette
  4. import uvicorn
  5. import inspect
  6. import time
  7. import asyncio
  8. from contextlib import contextmanager
  9. from matplotlib import pyplot as plt
  10. from utils import handle_exceptions, provide_arguments
  11. wp = jp.QuasarPage(delete_flag=False, title='Nice GUI', favicon='favicon.png')
  12. main = jp.Div(a=wp, classes='q-ma-md column items-start', style='row-gap: 1em')
  13. jp.justpy(lambda: wp, start_server=False)
  14. view_stack = [main]
  15. class Element:
  16. def __init__(self, view: jp.HTMLBaseComponent):
  17. view_stack[-1].add(view)
  18. self.view = view
  19. @property
  20. def text(self):
  21. return self.view.text
  22. @text.setter
  23. def text(self, text):
  24. self.view.text = text
  25. def set_text(self, text):
  26. self.view.text = text
  27. def __enter__(self):
  28. view_stack.append(self.view)
  29. def __exit__(self, *_):
  30. view_stack.pop()
  31. class Plot(Element):
  32. def update(self, close=True):
  33. self.view.set_figure(plt.gcf())
  34. if close:
  35. plt.close()
  36. class Ui(Starlette):
  37. def label(self, text='', typography=[]):
  38. if isinstance(typography, str):
  39. typography = [typography]
  40. classes = ' '.join('text-' + t for t in typography)
  41. view = jp.Div(text=text, classes=classes)
  42. return Element(view)
  43. def button(self, text, on_click=None):
  44. view = jp.QBtn(text=text, color='primary')
  45. if on_click is not None:
  46. view.on('click', handle_exceptions(provide_arguments(on_click)))
  47. return Element(view)
  48. def checkbox(self, text, on_change=None):
  49. view = jp.QCheckbox(text=text)
  50. if on_change is not None:
  51. view.on('input', handle_exceptions(provide_arguments(on_change, 'value')))
  52. return Element(view)
  53. @contextmanager
  54. def plot(self, close=True):
  55. view = jp.Matplotlib()
  56. yield Plot(view)
  57. view.set_figure(plt.gcf())
  58. if close:
  59. plt.close()
  60. def row(self):
  61. view = jp.QDiv(classes='row items-start', style='column-gap: 1em')
  62. return Element(view)
  63. def column(self):
  64. view = jp.QDiv(classes='column items-start', style='row-gap: 1em')
  65. return Element(view)
  66. def card(self):
  67. view = jp.QCard(classes='q-pa-md column items-start', style='row-gap: 1em')
  68. return Element(view)
  69. def timer(self, interval, callback, *, once=False):
  70. async def timeout():
  71. await asyncio.sleep(interval)
  72. handle_exceptions(callback)()
  73. jp.run_task(wp.update())
  74. async def loop():
  75. while True:
  76. start = time.time()
  77. handle_exceptions(callback)()
  78. jp.run_task(wp.update())
  79. dt = time.time() - start
  80. await asyncio.sleep(interval - dt)
  81. jp.run_task(timeout() if once else loop())
  82. def run(self):
  83. # NOTE: prevent reloader from restarting uvicorn
  84. if inspect.stack()[-2].filename.endswith('spawn.py'):
  85. return
  86. uvicorn.run('nice_gui:ui', host='0.0.0.0', port=80, lifespan='on', reload=True)
  87. # NOTE: instantiate our own ui object with all capabilities of jp.app
  88. ui = Ui()
  89. ui.__dict__.update(jp.app.__dict__)