adafruit / Adafruit_CircuitPython_DisplayIO_Layout

A Circuitpython helper library for display element layout using displayio.
MIT License
10 stars 14 forks source link

Draft proposal of standard parameters and functions for DisplayIO Widgets #3

Closed kmatch98 closed 3 years ago

kmatch98 commented 3 years ago

The DisplayIO_Layout library will rely on having several standard parameters for any "widgets" so this function can place them in the desired layout. Additionally, for any touch-related objects, it is useful to have standard response functions.

The starting point for this proposal was the Adafruit_CircuitPython_Display_Text library with the label item along with the Adafruit_CircuitPython_Display_Button library with the button item.

I'd especially like proposals on the standardized "Response functions" for widgets.


Proposed widget parameters and naming:

Coordinate scheme

Here is a graphic with a proposal of some of the naming of the pixel coordinate schemes:

image

Draft code snippet: Switch widget

Here is a code snippet of the __init__ function for a draft of a switch widget that runs on the Adafruit PyPortal:

class SwitchRoundHorizontal(displayio.Group):

    def __init__(
        self,
        *,
        x=0, # Placement of upper left corner pixel position on the display
        y=0,
        width=None, # defaults to the 4*radius
        height=30,
        name="",
        value=False,
        touch_padding=0,
        anchor_point=None,
        anchored_position=None,
        fill_color_off=(66, 44, 66),
        fill_color_on=(0,100,0),
        outline_color_off=(30,30,30),
        outline_color_on=(0,60,0),
        background_color_off=(255,255,255),
        background_color_on=(0,60,0),
        background_outline_color_off=None, # default to background_color_off
        background_outline_color_on=None, # default to background_color_on
        switch_stroke=2,
        text_stroke=None, # default to switch_stroke
        display_button_text=True,
        animation_time=0.2, # animation duration (in seconds)
        ):

        super().__init__(x=x, y=y, max_size=3)

Widget positioning

As described above, the widget position on the screen can be defined directly by widget.x and widget.y or by the combination of widget.anchor_point and widget.anchored_position.
Here is a reference image for the usage of anchor_point and anchored_position as snipped from the Adafruit Learn guide's Candy Hearts Example:

HELLO
makermelissa commented 3 years ago

What if all of these widgets used a "Control" or "Widget" superclass rather than directly deriving from Group and the superclass handled these standard events?

FoamyGuy commented 3 years ago

@kmatch98 this is a really great start! Thanks for taking the time to get all of this down.

I am in agreement with the majority of what you've laid out. I'll list only the things I might add to or suggest different below.

I am thinking the same as Melissa as well about putting these things into a class and then having the specific widgets extend this instead of Group. I'll refer to it as "Control" here, I do like that name, but open to other ideas still as well.

For the response functions I wonder if there may be a way to leverage the contains() function along with the debouncer library to get the other property states concepts like just touched and just released.

Widget naming - I don't think a widget_label option should be done at the Control level. I do see the usefulness of this construct, but I think the way to go about it is making a specific widget / layout. Like LabeledThing class (hopefully better name) and it has a set_thing(control_obj) function. So it works a bit like the GridLayout (or any layout) in that you "add" or in this case "set" a control into it and it adds "something" to the control. This one will add the text off the side (or wherever configured) of the control. This way it will be still be able to work with all controls, but doesn't need anything else specific to exist on the Control class for it to work.

animation_time I'm not sure if we need this on Control. Some widgets may not need it, and I'm not sure if the layouts would need to access it.

orientation Same for this one. It could live on the specific widgets need it. As long as they handle width and height appropriately based on their orientation the layouts can use that and not have to worry about orientation.

flip is interesting. Is the use case for rear-projection, or mirrored screen? I'm not sure if we have an easy way to achieve it in disiplayio, but perhaps I'm unaware or something.

Coordinate scheme - This is an awesome illustration! Would love to see diagrams like this find a home in the documentation or learn guide.

kmatch98 commented 3 years ago

Thanks for the encouragement.

I’m not sure I follow the difference in purpose or function between a Group and a Control class, so I could use some clarification on how this would be organized.

Right now, the only thing that can be displayed using display.show() is a Group class element. And currently there are only a few things that can be held in a Group, I think only Group and TileGrid. What level would this “control” class fall into?

If y’all can sketch a block diagram of your proposal that will help me understand better.

@FoamyGuy thanks for the comments on the variables, I think some variables should be categorized as “mandatory” (so things like grid_layout can operate on them) and other variables as “optional”. And anytime we can use meaningful reuse of variable names that will probably make life easier, even for optional variables.

Also I like your idea about the labeling of a widget. I struggled to make sense of a good way of doing that, but I think you are on a good track about having a separate “thing” that does any labeling. I’m also thinking of the best way of putting text inside of a widget, like numbers on a dial and how it can use a common way of laying them out. Maybe that is the same as a “grid layout” task, but used inside a widget.

Regarding widget orientation, taking the example of a switch I could imagine someone wanting it to switch right/left (horizontal) or up/down (vertical with ON is up), then next someone will want eventually want the mirror image of that (vertical but ON being down), so I thought maybe “flip” would be a way to describe the mirror image orientation. (I think I saw that lingo used some other library, maybe for setting up matrix display?). Another example, if someone wants a Sparkline to scroll in the opposite direction, they could “flip” it. I was thinking the individual widget drawing routine could be designed to handle dealing with the orientations (if that feature is desired).

Thanks again for the feedback. I’ll do some more cleanup on the slider switch widget example (better color handling) and post it somewhere for general consumption.

FoamyGuy commented 3 years ago

I am imaging it like this: image

The main reason for Control existing is that Group itself doesn't have width and height or things that rely on them like anchor_point and anchored_position. But it also gives us a nice place to define the common interface that all of the layouts and other "enchancing" widgets like the LabeledThing will assume exists in order to do whatever they need.

FoamyGuy commented 3 years ago

Ah I think I see what you mean with flip, its more conceptual and the specifics can be up to each widget, not necessarily strictly mirroring the pixels. I do think that is a nice option to have.

Orientation I think I understand what you mean better as well. But I do think it can live on each widget that wants to have it. Doesn't need to be on Control

FoamyGuy commented 3 years ago

In the initial code I have a widgets directory that I'm thinking we can include things like the RoundSwitch that you made if you are interested in having it there.

makermelissa commented 3 years ago

That's the great thing about using a superclass. The subclasses can override any functions from the superclass for any specific implementation details.

TG-Techie commented 3 years ago

I agree with @makermelissa and @FoamyGuy on separating a "Control" or "Interactable" base widget. It also makes that functionality opt-in as opposed to required for every "Group", might be a ram/space saver on some of the more ram tight m4 boards (like the Nrf). Also groups don’t have dimensions where as Controls do is a great point.

There may be a be advantages to separating controls further? Not all widgets may need control. It could be something like Group -> Widget -> Control. Widget adds dimension and Control adds, well, control.

If I may add in two cents to the original post. Might I suggest that 'x' and 'y' be specified together as 'coord=', a tuple of (x, y). Same thing for width and height as (width, height) as 'dims='. From my experience having the user specify them as pairs allows the backend more flexibility to reason about where it should be, and generally it means the user can't input invalid or implicit data.

For the data/control interface I'd suggest separating selection from pressing from updating scroll coordinates, etc. As a small case example, I've had to decouple selection, pressing, and "scrolling" from each other as pressing and scrolling are mutually exclusive but a scroll may start with a finger on a button/selectable. when scrolling starts the button needs to be deselected without activating it's functionality. (That reasoning is handled by an input event loop)

Another useful debug widget may be frame, something to draw the exact borders around a widget.

kmatch98 commented 3 years ago

I'm trying to digest y'all's inputs so I made a chart to see if I'm getting it right. Also, I'm unsure how generic the Control class should be so I thought I'd make a graphic to see if I understanding it:

image

Here are some questions: Should the Control class be it's own separate Class, or a subclass of Widget? How generic do we want to make Control? I first envisioned it as related to dealing with touch screen inputs, but it could be more generic. For example, do we want the Control class to be able handle D-pad or joystick switch inputs to control a cursor widget on the screen?

Specifically regarding touch_screen related control functions, it seems like touch_screen capabilities differ a lot depending upon the type. The current Adafruit_CircuitPython_Touchscreen Library for the resistive touch screen seems to only provide single touch response. But I guess there could be added wrappers to provide touch_down, still_touched and touch_up actions. Also, some touch screens also provide other gesture response (multi-touch points, pinch zoom, etc.). How should this Control class be configured? How many Control functions should be required functions, or does the event loop just dump a Dictionary of touches and/or gestures to the Control, and each Control has to decide if it responds to those?

One more question about contains(). I was first envisioning that the event loop will check each widget using contains() and then call the widget.selected() function. In contrast, should the event loop just send the Touch/Gesture event dictionary to each widget and then they each return whether they handled that event?

As I'm fairly new to all of this, so I'm mixing up a lot of questions and concepts between the class structure and the event loop handling, so I appreciate your patience. Also, I'm sure this has been done before, so if you have a suggested reference design, please fire it over.

TG-Techie commented 3 years ago

As I have this on hand, Here is an event loop I've implemented. It scans through all the widgets that have certain control functionalities and then informs them of what to do. That way the widgets don’t have to parse raw data*

in this design widgets' control functionality is defined by the presence of specific methods but I could easily be defined as subclasses with overrides.

[*] scrolling is not finished, so the update coord does have to for now (It is lacking exclusive either press or scroll behavior atm)

https://github.com/TG-Techie/TG-Gui-Std-CircuitPython/blob/main/tg_gui_std/event_loops.py

kmatch98 commented 3 years ago

I updated the sliding switch widget now with classes as laid out in the diagram above. Here are the main updates:

Demo files are here, along with a pyPortal example.

To Clarify:

This is my first time making a more complicated set of interacting Python classes, so I welcome any suggestions that you have to improve this. It took me quite a bit of trial and error to figure out the use of *args and **kwargs along with the super().init details. If I can improve how to do these, I'm eager to learn how.

I appreciate your inputs!


The diagram above captures the general Class design. But here it is in a list, along with a few comments.

SwitchRoundHorizontal class:

Widget class:

Control class: Outlines response functions

WidgetLabel class: Positions the label with relative position on the widget.

This class positions the label's anchor_point at the widget's anchor_point_on_widget. Note that X and Y values for anchor_point_on_widget can be < 0.0 or can be > 1.0 to place it outside of the widget's bounding_box.

kmatch98 commented 3 years ago

I submitted a draft pull request with the initial version of the proposed class definitions and an example.

Feedback is welcome.

kmatch98 commented 3 years ago

This is superseded by the PRs and other discussions so I’m closing this.