diff --git a/README.md b/README.md index 13e4b8c..57f7b70 100644 --- a/README.md +++ b/README.md @@ -396,15 +396,19 @@ Demos are run by issuing (for example): These will run on screens of 128x128 pixels or above. The initial ones are minimal and aim to demonstrate a single technique. * `simple.py` Minimal demo discussed below. `Button` presses print to REPL. - * `checkbox` A `Checkbox` controlling an `LED`. + * `checkbox.py` A `Checkbox` controlling an `LED`. * `slider.py` A `Slider` whose color varies with its value. * `slider_label.py` A `Slider` updating a `Label`. Good for trying precision mode. - * `linked_slider.py` One `Slider` updating two others, and a coding "wrinkle" + * `linked_sliders.py` One `Slider` updating two others, and a coding "wrinkle" required for doing this. + * `dropdown.py` A dropdown list updates a `Label`. * `dialog.py` `DialogBox` demo. Illustrates the screen change mechanism. * `screen_change.py` A `Pushbutton` causing a screen change using a re-usable "forward" button. + * `primitives.py` Use of graphics primitives. + * `aclock.py` An analog clock using the `Dial` vector display. Also shows + screen layout using widget metrics. Has a simple `uasyncio` task. * `tbox.py` Text boxes and user-controlled scrolling. ### 1.11.2 Test scripts @@ -766,17 +770,21 @@ Constructor args: The constructor displays the string at the required location. -Methods: - 1. `value` Redraws the label. This takes the following args: - * `text=None` The text to display. If `None` displays last value. - * ` invert=False` If true, show inverse text. - * `fgcolor=None` Foreground color: if `None` the `Writer` default is used. - * `bgcolor=None` Background color, as per foreground. - * `bdcolor=None` Border color. As per above except that if `False` is - passed, no border is displayed. This clears a previously drawn border. - Returns the current text string. +Method: +`value` Redraws the label. This takes the following args: + * `text=None` The text to display. If `None` displays last value. + * `invert=False` If true, show inverse text. + * `fgcolor=None` Foreground color: if `None` the `Writer` default is used. + * `bgcolor=None` Background color, as per foreground. + * `bdcolor=None` Border color. As per above except that if `False` is + passed, no border is displayed. This clears a previously drawn border. +Returns the current text string. -If populating a label would cause it to extend beyond the screen boundary a +If the `value` method is called with a text string too long for the `Label` a +`ValueError` will be thrown. The width specified to the constructor should be +sufficient for all possible values. + +If constructing a label would cause it to extend beyond the screen boundary a warning is printed at the console. The label may appear at an unexpected place. The following is a complete "Hello world" script. ```python @@ -1368,6 +1376,11 @@ Methods: * `bdcolor=None` Border color. As per above except that if `False` is passed, no border is displayed. This clears a previously drawn border. +### Legends + +Depending on the font in use for legends additional space may be required above +and below the `Meter` to display the top and bottom legends. + ###### [Contents](./README.md#0-contents) # 17. Slider and HorizSlider widgets @@ -1441,6 +1454,11 @@ user supplied args. The callback can be a bound method, typically of a `Screen` subclass. The callback runs when the widget is instantiated and whenever the value changes. This enables dynamic color change. See `gui/demos/active.py`. +### Legends + +Depending on the font in use for legends additional space may be required +around sliders to display all legends. + ###### [Contents](./README.md#0-contents) # 18. Scale widget @@ -2173,8 +2191,30 @@ The apparently obvious solution of designing a vertical `Scale` is tricky owing to the fact that the length of the internal text can be substantial and variable. +## Screen layout + +Widgets are positioned using absolute `row` and `col` coordinates. These may +optionally be calculated using the metrics of other widgets. This facilitates +relative positioning which can make layouts easier to modify. Such layouts can +also automatically adapt to changes of fonts. To simplify this, all widgets +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. + +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 `aclock.py` demo provides a simple example of this approach. + ## Use of graphics primitives +See [demo primitives.py](./gui/demos/primitives.py). + These notes are for those wishing to draw directly to the `Screen` instance. This is done by providing the user `Screen` class with an `after_open()` method which is written to issue the display driver calls. diff --git a/gui/core/ugui.py b/gui/core/ugui.py index b7c28fd..d29378b 100644 --- a/gui/core/ugui.py +++ b/gui/core/ugui.py @@ -550,6 +550,8 @@ 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 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 new file mode 100644 index 0000000..8a1cf1f --- /dev/null +++ b/gui/demos/aclock.py @@ -0,0 +1,89 @@ +# aclock.py micro-gui analog clock demo. + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# Initialise hardware and framebuf before importing modules. +import hardware_setup # Create a display instance +from gui.core.ugui import Screen, ssd +from gui.widgets.label import Label +from gui.widgets.dial import Dial, Pointer +from gui.widgets.buttons import CloseButton + +# Now import other modules +from cmath import rect, pi +import uasyncio as asyncio +import time +from gui.core.writer import CWriter + +# Font for CWriter +import gui.fonts.arial10 as font +from gui.core.colors import * + + +# Adjust passed Dial and Label instances to show current time and date. +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. + def uv(phi): + return rect(1, phi) + + def suffix(n): + if n in (1, 21, 31): + return 'st' + if n in (2, 22): + return 'nd' + if n in (3, 23): + return 'rd' + return th + + days = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') + months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', + 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') + + hrs = Pointer(dial) + mins = Pointer(dial) + secs = Pointer(dial) + + hstart = 0 + 0.7j # Pointer lengths. Will rotate relative to top. + mstart = 0 + 1j + sstart = 0 + 1j + + while True: + t = time.localtime() + hrs.value(hstart * uv(-t[3] * pi/6 - t[4] * pi / 360), CYAN) + mins.value(mstart * uv(-t[4] * pi/30), CYAN) + secs.value(sstart * uv(-t[5] * pi/30), RED) + lbltim.value('{:02d}.{:02d}.{:02d}'.format(t[3], t[4], t[5])) + lbldate.value('{} {}{} {} {}'.format(days[t[6]], t[2], suffix(t[2]), months[t[1] - 1], t[0])) + await asyncio.sleep(1) + +class BaseScreen(Screen): + def __init__(self): + super().__init__() + labels = {'bdcolor' : RED, + 'fgcolor' : WHITE, + 'bgcolor' : DARKGREEN, + } + + 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 + lbldate = Label(wri, row, 2, 100, **labels) + row += lbldate.rows + gap + lbltim = Label(wri, row, 2, '00.00.00', **labels) + self.reg_task(aclock(dial, lbldate, lbltim)) + CloseButton(wri) + + +def test(): + print('Analog clock demo.') + Screen.change(BaseScreen) + +test() diff --git a/gui/demos/active.py b/gui/demos/active.py index fd37208..f3bb313 100644 --- a/gui/demos/active.py +++ b/gui/demos/active.py @@ -1,5 +1,8 @@ # active.py micro-gui demo of widgets that respond to user control +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + # Create SSD instance. Must be done first because of RAM use. import hardware_setup from gui.core.ugui import Screen, ssd diff --git a/gui/demos/checkbox.py b/gui/demos/checkbox.py index 1236a20..98a4714 100644 --- a/gui/demos/checkbox.py +++ b/gui/demos/checkbox.py @@ -1,5 +1,8 @@ # checkbox.py Minimal micro-gui demo showing a Checkbox updating an LED. +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + # hardware_setup must be imported before other modules because of RAM use. import hardware_setup # Create a display instance from gui.core.ugui import Screen, ssd diff --git a/gui/demos/dialog.py b/gui/demos/dialog.py index 682de56..d1551fb 100644 --- a/gui/demos/dialog.py +++ b/gui/demos/dialog.py @@ -1,5 +1,8 @@ # dialog.py micro-gui demo of the DialogBox class +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + # hardware_setup must be imported before other modules because of RAM use. import hardware_setup # Create a display instance from gui.core.ugui import Screen, Window, ssd diff --git a/gui/demos/dropdown.py b/gui/demos/dropdown.py new file mode 100644 index 0000000..29b464b --- /dev/null +++ b/gui/demos/dropdown.py @@ -0,0 +1,45 @@ +# dropdown.py micro-gui demo of Dropdown class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# hardware_setup must be imported before other modules because of RAM use. +import hardware_setup # Create a display instance +from gui.core.ugui import Screen, Window, ssd + +from gui.widgets.label import Label +from gui.widgets.buttons import CloseButton +from gui.widgets.dropdown import Dropdown +from gui.core.writer import CWriter + +# Font for CWriter +import gui.fonts.font10 as font +from gui.core.colors import * + + +class BaseScreen(Screen): + + def __init__(self): + + super().__init__() + wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) + + col = 2 + row = 2 + Dropdown(wri, row, col, + elements = ('hydrogen', 'helium', 'neon', 'xenon', 'radon'), + bdcolor = GREEN, bgcolor = DARKGREEN, + callback=self.ddcb) + row += 30 + self.lbl = Label(wri, row, col, 90, bdcolor=RED) + CloseButton(wri) # Quit the application + + def ddcb(self, dd): + if hasattr(self, 'lbl'): + self.lbl.value(dd.textvalue()) + +def test(): + print('Dropdown demo.') + Screen.change(BaseScreen) + +test() diff --git a/gui/demos/linked_sliders.py b/gui/demos/linked_sliders.py index 18e886a..2b8681d 100644 --- a/gui/demos/linked_sliders.py +++ b/gui/demos/linked_sliders.py @@ -1,5 +1,8 @@ # linked_sliders.py Minimal micro-gui demo one Slider controlling two others. +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + # hardware_setup must be imported before other modules because of RAM use. import hardware_setup # Create a display instance from gui.core.ugui import Screen, ssd diff --git a/gui/demos/primitives.py b/gui/demos/primitives.py new file mode 100644 index 0000000..deddb54 --- /dev/null +++ b/gui/demos/primitives.py @@ -0,0 +1,45 @@ +# primitives.py micro-gui demo of use of graphics primitives + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# hardware_setup must be imported before other modules because of RAM use. +import hardware_setup # Create a display instance +from gui.core.ugui import Screen, Window, ssd, display + +from gui.widgets.label import Label +from gui.widgets.buttons import CloseButton +from gui.core.writer import CWriter + +# Font for CWriter +import gui.fonts.font10 as font +from gui.core.colors import * + + +class BaseScreen(Screen): + + def __init__(self): + + super().__init__() + wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) + + col = 2 + row = 2 + Label(wri, row, col, 'Primitives') + CloseButton(wri) # Quit the application + + + def after_open(self): + display.usegrey(False) + # Coordinates are x, y as per framebuf + # circle method is in Display class only + display.circle(70, 70, 30, RED, 2) + # These methods exist in framebuf, so also in SSD and Display + ssd.hline(0, 127, 128, BLUE) + ssd.vline(127, 0, 128, BLUE) + +def test(): + print('Primitives demo.') + Screen.change(BaseScreen) + +test() diff --git a/gui/demos/screen_change.py b/gui/demos/screen_change.py index ea15bfd..e766506 100644 --- a/gui/demos/screen_change.py +++ b/gui/demos/screen_change.py @@ -1,5 +1,8 @@ # screen_change.py Minimal micro-gui demo showing a Button causing a screen change. +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + # hardware_setup must be imported before other modules because of RAM use. import hardware_setup # Create a display instance from gui.core.ugui import Screen, ssd diff --git a/gui/demos/screens.py b/gui/demos/screens.py index a9054d5..e4d6831 100644 --- a/gui/demos/screens.py +++ b/gui/demos/screens.py @@ -1,5 +1,8 @@ # screens.py micro-gui demo of multiple screens, dropdowns etc +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + # hardware_setup must be imported before other modules because of RAM use. import hardware_setup # Create a display instance from gui.core.ugui import Screen, Window, ssd diff --git a/gui/demos/simple.py b/gui/demos/simple.py index 03c2c8d..37003f8 100644 --- a/gui/demos/simple.py +++ b/gui/demos/simple.py @@ -1,5 +1,8 @@ # simple.py Minimal micro-gui demo. +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + # hardware_setup must be imported before other modules because of RAM use. import hardware_setup # Create a display instance from gui.core.ugui import Screen, ssd diff --git a/gui/demos/slider.py b/gui/demos/slider.py index c5e8190..b0543a4 100644 --- a/gui/demos/slider.py +++ b/gui/demos/slider.py @@ -1,5 +1,8 @@ # slider.py Minimal micro-gui demo showing a Slider with variable color. +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + # hardware_setup must be imported before other modules because of RAM use. import hardware_setup from gui.core.ugui import Screen, ssd diff --git a/gui/demos/slider_label.py b/gui/demos/slider_label.py index b020af9..425ea56 100644 --- a/gui/demos/slider_label.py +++ b/gui/demos/slider_label.py @@ -1,5 +1,8 @@ # slider_label.py Minimal micro-gui demo showing a Slider controlling a Label. +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + # hardware_setup must be imported before other modules because of RAM use. import hardware_setup # Create a display instance from gui.core.ugui import Screen, ssd diff --git a/gui/demos/various.py b/gui/demos/various.py index 10c14ff..4271857 100644 --- a/gui/demos/various.py +++ b/gui/demos/various.py @@ -1,5 +1,8 @@ # various.py micro-gui demo of multiple controls on a large display +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + # Initialise hardware and framebuf before importing modules. # Create SSD instance. Must be done first because of RAM use. import hardware_setup diff --git a/gui/demos/vtest.py b/gui/demos/vtest.py index f3f36db..06cd5bb 100644 --- a/gui/demos/vtest.py +++ b/gui/demos/vtest.py @@ -56,7 +56,7 @@ async def ptr_test(dial): ptr.value(v, RED) await asyncio.sleep_ms(200) -# Analog clock demo. Note this could also be achieved using the Dial class. +# Analog clock demo. async def aclock(dial, lbldate, lbltim): uv = lambda phi : rect(1, phi) # Return a unit vector of phase phi days = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', diff --git a/gui/widgets/checkbox.py b/gui/widgets/checkbox.py index 580100f..f776471 100644 --- a/gui/widgets/checkbox.py +++ b/gui/widgets/checkbox.py @@ -11,7 +11,8 @@ class Checkbox(Widget): def __init__(self, writer, row, col, *, height=30, fillcolor=None, fgcolor=None, bgcolor=None, bdcolor=False, callback=dolittle, args=[], value=False, active=True): - super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor, value, active) + super().__init__(writer, row, col, height, height, fgcolor, + bgcolor, bdcolor, value, active) super()._set_callbacks(callback, args) self.fillcolor = fillcolor diff --git a/gui/widgets/dial.py b/gui/widgets/dial.py index eb21b47..11d061f 100644 --- a/gui/widgets/dial.py +++ b/gui/widgets/dial.py @@ -67,6 +67,8 @@ 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 radius = int(height / 2) self.radius = radius self.ticks = ticks diff --git a/gui/widgets/meter.py b/gui/widgets/meter.py index 0ac9437..260cd12 100644 --- a/gui/widgets/meter.py +++ b/gui/widgets/meter.py @@ -23,13 +23,13 @@ class Meter(Widget): Label(writer, row + height + writer.height // 2, col, label) self.style = style self.ptcolor = ptcolor if ptcolor is not None else self.fgcolor - if legends is not None: # Legends + if legends is not None: # Legends are static x = col + width + 4 y = row + height dy = 0 if len(legends) <= 1 else height / (len(legends) -1) yl = y - writer.height / 2 # Start at bottom for legend in legends: - Label(writer, int(yl), x, legend) + l = Label(writer, round(yl), x, legend) yl -= dy self.value(value) diff --git a/gui/widgets/sliders.py b/gui/widgets/sliders.py index 211ff93..8f7b8f6 100644 --- a/gui/widgets/sliders.py +++ b/gui/widgets/sliders.py @@ -64,6 +64,7 @@ class Slider(LinearIO): display.rect(self.slot_x0, self.slot_y0, slot_w, slot_len, self.fgcolor) txtcolor = GREY if self.greyed_out() else self.fontcolor + # Dynamic processing of legends supports greying-out if self.legends is not None: if len(self.legends) <= 1: dy = 0