From 3f37900d126cc2da52e96d190e731d20ea118b6a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 25 Jun 2021 10:57:25 +0100 Subject: [PATCH] Fix widget metrics. --- README.md | 33 +++++++++++++++++++-------------- gui/core/ugui.py | 12 ++++++------ gui/demos/aclock.py | 13 +++++++------ gui/demos/active.py | 3 ++- gui/demos/various.py | 3 +++ gui/demos/vtest.py | 5 ++++- gui/widgets/dial.py | 5 +++-- gui/widgets/meter.py | 3 +++ gui/widgets/sliders.py | 3 +++ 9 files changed, 50 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 57f7b70..dc0f375 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,9 @@ target and a C device driver (unless you can acquire a suitable binary). # Project status Code has been tested on ESP32, Pi Pico and Pyboard. The API shuld be stable. -Code is new and issues are likely: please report any found. This document is -under review. I plan to add further demos and to upgrade the performance of -some display drivers. - -An issue under investigation is that a soft reset is required after a GUI -application is run and before running another. Otherwise the second application -displays correctly but is unresponsive. +Code is new and issues are likely: please report any found. The project is +under development so check for updates. I also plan to upgrade the performance +of some display drivers. # 0. Contents @@ -392,6 +388,11 @@ Demos are run by issuing (for example): ```python >>> import gui.demos.simple ``` +If shut down cleanly with the "close" button a demo can be re-run with (e.g.): +```python +gui.demos.simple.test() +``` +Before running a different demo the host should be reset (ctrl-d) to clear RAM. These will run on screens of 128x128 pixels or above. The initial ones are minimal and aim to demonstrate a single technique. @@ -2170,7 +2171,10 @@ class TSeq(Screen): The "tab order" of widgets on a `Screen` is the order with which they acquire focus with successive presses of the `Next` button. It is determined by the -order in which they are instantiated. +order in which they are instantiated. Tab order is important for usability but +instantiating in the best order can conflict with program logic. This happens +if a widget's callback refers to others not yet instantiated. See demos +`dropdown.py` and `linked_sliders.py` for one solution. The obvious layout for the physical buttons is as per a joystick: @@ -2201,13 +2205,12 @@ have the following bound variables, which should be considered read-only: * `height` As specified. Does not include border. * `width` Ditto. - * `rows` Height including borders. - * `cols` Width with borders. + * `mrow` Maximum absolute row occupied by the widget. + * `mcol` Maximum absolute col occupied by the widget. -This support is fairly "micro" and does not take account of labels and legends. -This means that `rows` and `cols` for `Dial`, `Meter`, `Slider` and -`HorizSlider` do not necessarily reflect the full amount of space used by the -control. +The `mrow` and `mcol` values enable other widgets to be positioned relative to +the one previously instantiated. In the cases of sliders, `Dial` and `Meter` +widgets these take account of space ocupied by legends or labels. The `aclock.py` demo provides a simple example of this approach. @@ -2254,4 +2257,6 @@ Hopefully these are self explanatory. The `Display` methods use the `framebuf` convention of `x, y` coordinates rather than the `row, col` system used by micro-gui. +The `primitives.py` demo provides a simple example. + ###### [Contents](./README.md#0-contents) diff --git a/gui/core/ugui.py b/gui/core/ugui.py index d29378b..f19b7f7 100644 --- a/gui/core/ugui.py +++ b/gui/core/ugui.py @@ -274,10 +274,9 @@ class Screen: cs_new._do_open(cs_old) # Clear and redraw cs_new.after_open() # Optional subclass method if cs_old is None: # Initialising - try: - asyncio.run(Screen.monitor()) # Starts and ends uasyncio - finally: - asyncio.new_event_loop() + asyncio.run(Screen.monitor()) # Starts and ends uasyncio + # Don't do asyncio.new_event_loop() as it prevents re-running + # the same app. @classmethod async def monitor(cls): @@ -550,8 +549,9 @@ class Widget: self.col = col self.height = height self.width = width - self.rows = height + 4 # For metrics. Default: allow for border. - self.cols = width + 4 + # Maximum row and col. Defaults for user metrics. May be overridden + self.mrow = row + height + 2 # in subclass. Allow for border. + self.mcol = col + width + 2 self.visible = True # Used by ButtonList class for invisible buttons self.draw = True # Signals that obect must be redrawn self._value = value diff --git a/gui/demos/aclock.py b/gui/demos/aclock.py index 8a1cf1f..af42159 100644 --- a/gui/demos/aclock.py +++ b/gui/demos/aclock.py @@ -25,9 +25,9 @@ from gui.core.colors import * async def aclock(dial, lbldate, lbltim): # Return a unit vector of phase phi. Multiplying by this will # rotate a vector anticlockwise which is mathematically correct. - # Alas clocks modelled on sundials were invented in the northern - # hemisphere. Otherwise they would have rotated widdershins - # in accordance with maths. Hence negative sign when called. + # Alas clocks, modelled on sundials, were invented in the northern + # hemisphere. Otherwise they would have rotated widdershins like + # the maths. Hence negative sign when called. def uv(phi): return rect(1, phi) @@ -70,13 +70,14 @@ class BaseScreen(Screen): } wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) - gap = 4 # Vertical gap between widgets dial = Dial(wri, 2, 2, height = 70, ticks = 12, fgcolor = GREEN, pip = GREEN) # Set up clock display: instantiate labels - row = dial.rows + gap + # Demo of relative positioning. + gap = 4 # Vertical gap between widgets + row = dial.mrow + gap lbldate = Label(wri, row, 2, 100, **labels) - row += lbldate.rows + gap + row = lbldate.mrow + gap lbltim = Label(wri, row, 2, '00.00.00', **labels) self.reg_task(aclock(dial, lbldate, lbltim)) CloseButton(wri) diff --git a/gui/demos/active.py b/gui/demos/active.py index f3bb313..66818c3 100644 --- a/gui/demos/active.py +++ b/gui/demos/active.py @@ -47,12 +47,13 @@ class BaseScreen(Screen): self.vslider = Slider(wri, 2, 2, callback=self.slider_cb, bdcolor=RED, slotcolor=BLUE, legends=('0.0', '0.5', '1.0'), value=0.5) - + #Label(wri, 2, self.vslider.mcol, 'FF') col = 80 row = 15 self.hslider = HorizSlider(wri, row, col, callback=self.slider_cb, bdcolor=GREEN, slotcolor=BLUE, legends=('0.0', '0.5', '1.0'), value=0.7) + Label(wri, self.hslider.mrow, self.hslider.mcol, 'FF') row += 30 self.scale = Scale(wri, row, col, width = 150, tickcb = tickcb, pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN, diff --git a/gui/demos/various.py b/gui/demos/various.py index 4271857..02c6267 100644 --- a/gui/demos/various.py +++ b/gui/demos/various.py @@ -61,8 +61,11 @@ class FooScreen(Screen): m0 = Meter(wri, 10, 240, divisions = 4, ptcolor=YELLOW, height=80, width=15, label='Meter example', style=Meter.BAR, legends=('0.0', '0.5', '1.0')) + #Label(wri, 2, m0.mcol, 'FF') # Instantiate displayable objects. bgcolor forces complete redraw. dial = Dial(wri, 2, 2, height = 75, ticks = 12, bgcolor=BLACK, bdcolor=None, label=120) # Border in fg color + #Label(wri, dial.mrow, 2, 'FF') + #Label(wri, dial.mrow, dial.mcol, 'FF') scale = Scale(wri, 2, 100, width = 124, tickcb = tickcb, pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN) diff --git a/gui/demos/vtest.py b/gui/demos/vtest.py index 06cd5bb..7294389 100644 --- a/gui/demos/vtest.py +++ b/gui/demos/vtest.py @@ -41,7 +41,10 @@ async def ptr_test(dial): ptr = Pointer(dial) v = 0j steps = 20 # No. of interpolation steps - grv = lambda : urandom.getrandbits(16) / 2**15 - 1 # Random: range -1.0 to +1.0 + # BUG getting a weird visual flicker on occasion, with yellow + # being briefly displayed. Where is that coming from? + # Does not seem to be affected by max value. TODO + grv = lambda : urandom.getrandbits(16) / 2**15 - 1 # Random: range -1.0 to +0.999 while True: v1 = grv() + 1j * grv() # Random vector dv = (v1 - v) / steps # Interpolation vector diff --git a/gui/widgets/dial.py b/gui/widgets/dial.py index 11d061f..c15123c 100644 --- a/gui/widgets/dial.py +++ b/gui/widgets/dial.py @@ -67,8 +67,9 @@ class Dial(Widget): self.pip = self.fgcolor if pip is None else pip if label is not None: self.label = Label(writer, row + height + 3, col, label) - #self.cols = max(self.cols, self.label.cols) - #self.rows += 3 + self.label.rows + # Adjust metrics + self.mrow = self.label.mrow - 2 # Label never has border + self.mcol = max(self.mcol, self.label.mcol - 2) radius = int(height / 2) self.radius = radius self.ticks = ticks diff --git a/gui/widgets/meter.py b/gui/widgets/meter.py index 260cd12..428d521 100644 --- a/gui/widgets/meter.py +++ b/gui/widgets/meter.py @@ -24,6 +24,7 @@ class Meter(Widget): self.style = style self.ptcolor = ptcolor if ptcolor is not None else self.fgcolor if legends is not None: # Legends are static + mcol = 0 x = col + width + 4 y = row + height dy = 0 if len(legends) <= 1 else height / (len(legends) -1) @@ -31,6 +32,8 @@ class Meter(Widget): for legend in legends: l = Label(writer, round(yl), x, legend) yl -= dy + mcol = max(mcol, l.mcol) + self.mcol = mcol - 2 # For metrics. Legends never have border. self.value(value) def value(self, n=None, color=None): diff --git a/gui/widgets/sliders.py b/gui/widgets/sliders.py index 8f7b8f6..7b477c9 100644 --- a/gui/widgets/sliders.py +++ b/gui/widgets/sliders.py @@ -28,6 +28,9 @@ class Slider(LinearIO): super()._set_callbacks(callback, args) self.divisions = divisions self.legends = legends + if legends is not None: # Adjust column metric + ml = max((writer.stringlen(l) for l in legends)) + self.mcol += ml + 2 # Strings are rendered 2 pixels right of border self.fontcolor = self.fgcolor if fontcolor is None else fontcolor self.slotcolor = self.bgcolor if slotcolor is None else slotcolor # Define slider