Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use with Enaml #359

Closed
frmdstryr opened this issue Feb 16, 2017 · 19 comments
Closed

Use with Enaml #359

frmdstryr opened this issue Feb 16, 2017 · 19 comments

Comments

@frmdstryr
Copy link

Is there any way this could be implemented as a toolkit for use with Enaml?

Flexx does have a nice declarative way of defining the structure, however the data binding aspect is separated into different handlers, which (from experience) quickly gets tedious in larger applications. Enaml nicely keeps everything contained.

Just wondering... I think flexx is pretty awesome 👍

@almarklein
Copy link
Member

almarklein commented Feb 16, 2017

Is there any way this could be implemented as a toolkit for use with Enaml?

Not sure, I don't have sufficient experience with Enaml to anwer this. But if you consider Flexx as a GUI backend, and if any GUI backend can be used with Enaml, than why not ...

You might be interested to know that the author of Enaml (Chris Colbert) is also the author of PhosphorJS, which is the GUI library (and more) that Flexx uses for most of its layout.

which (from experience) quickly gets tedious in larger applications. Enaml nicely keeps everything contained.

Yes, this is a common problem. I think Flexx makes connecting things up really easy, but for larger applications / more complex interactions, we've felt that there might be room for improvement. Which is why I am interested to hear from you how Enaml keeps things more contained? Is it the declarative way to connect certain model attributed to views?

@frmdstryr
Copy link
Author

You might be interested to know that the author of Enaml (Chris Colbert) is also the author of PhosphorJS, which is the GUI library (and more) that Flexx uses for most of its layout.

Yes, I did see that. Perhaps he would know if this is feasible.

I do know it's easy to add a toolkit for creating the widget tree and a different application/event loop to Enaml, but I'm unsure if it's possible to swap the data binding part (atom) of Enaml with an alternative.

interested to hear from you how Enaml keeps things more contained? Is it the declarative way to connect certain model attributed to views?

Basically, yes. Instead of manually having to connect signals the Enaml parser does it for you, and it's all defined within the actual declarative widget tree, so handlers are not scattered all over. I've used Enaml for several projects and it's by far the cleanest way I've ever seen to do a model based UI.

@frmdstryr
Copy link
Author

frmdstryr commented Feb 17, 2017

If I were to mock up a few examples rewritten using an "enaml flexx toolkit".

This example http://flexx.readthedocs.io/en/latest/ui/lineedit.html
when converted to enaml it would look something like:

from enaml.flexx.api import App, VBox, Label, LineEdit

enamldef Example(App):
    VBox:
        LineEdit: line:
             placeholder_text = 'type here'
             autocomp = ['foo','bar']
        Label:
             text << line.text # this is the @event.connect('line.text') and _change_label in one line

Enaml uses << to indicate a read only binding.

Likewise
http://flexx.readthedocs.io/en/latest/ui/progressbar.html

Would be something like:

from enaml.flexx.api import App, HBox, Button, ProgressBar

enamldef Example(App):
    HBox:
        Button:
            text = "Less"
            clicked :: prog.value -= 0.1 # @event.connect(b1.mouse_down) to prog.value
        Button:
            text = "More"
            clicked :: prog.value += 0.1 # @event.connect(b2.mouse_down) to prog.value
        ProgressBar: prog:
            value = 0.1 # = sets Initial value

Enaml uses :: or >> to indicate an outgoing event, emit, or write only binding.

The plotwidget example:
http://flexx.readthedocs.io/en/latest/ui/plotwidget.html

from enaml.flexx.api import App, VBox, HBox, Label, Slider, PlotWidget

enamldef Example(App):
    VBox:
        HBox:
            Label:
                text = "Frequency"
            Slider: s1:
                min = 1
                max = 10
                value = 5
                flex = 1
            Label:
                text = "Phase"
            Slider: s2:
                min = 0
                max = 6
                value = 0
                flex = 1
        PlotWidget:
            flex = 1
            xlabel = 'time'
            ylabel = 'amplitude'
            title = 'a sinusoid'
            xdata = [i/100 for i in range(100)]
            ydata << [s1.value * x * 2 * window.Math.PI + s2.value] # connects ydata to s1.value and s2.value 
           

There's no examples I could find but you can also do two way data binding in enaml using the := operator.

from enaml.flexx.api import App, LineEdit, VBox

enamldef Example(App):
    VBox:
        LineEdit: line1:
            pass
        LineEdit: line2:
            text := line1.text # Would keep both in sync, or they could both be tied to a model property

In all the examples they would need to be started in an enaml like way.

if __name__=='__main__':
    from enaml.flexx.app import FlexApplication
    app = FlexApplication()
    view = Example()
    view.show() 
    app.start() # starts flexx io loop

It's a bit more ugly/verbose when defining multiple properties but you can see it keeps all the handlers / signals within the widget using them which really keeps large UI's organized well.

@almarklein
Copy link
Member

Thanks for these! I think we can distinguish between two approaches that make Enaml look so declarative. The first is the structure of the code, which is something that we can achieve pretty well in vanilla Flexx as it is, though its somewhat more verbose. For example the progressbar example:

class Example(Widget):
    def init(self):
        with HBox():
                 Button(
                     text = "Less",
                     clicked = ??
                 )
                 Button(
                       text = "More",
                       clicked = ??
                )
                self.prog = ProgressBar(
                    value = 0.1 # = sets Initial value
                )

The other approach (the main point of this discussion, I think) is the way that Enaml allows declaring connectivity in-line at the place where a widget is defined. I see two variants of this: a property having a calculated value (from one or more other properties), and an in-line event handler.

Enaml solves these by defining a Python/yaml -like format. One approach (as you suggested in the initial post) could be to make Flexx work with Enaml.

Another path could be to keep writing Python and come up with something to put at the question marks above. Something that is both feasible and easy enough to get that "declarative" feel ...

@almarklein
Copy link
Member

Thinking about the first case (a property that has a calculated value), let's look at the simplest example:

In Enaml:

LineEdit: line:
    ...
Label:
    text << line.text

Current Flexx inline approach (yes, this looks verbose in comparison!):

label = Label()
line = LineEdit(...)

@line.connect('text')
def on_line_change(self, *events):
     label.text =line.text

First things that popped to mind:

label = Label(text=lambda: line.text)  # a prop with a function as a value is a calculated prop
label = Label(text='<<line.text')  # some kind of prefix to indicate a calculated value

The problem with the above lambda is that there is no way for the code to know what events it should connect to. Unless we create an AST for the code and see what variables the lambda uses, which could work in theory, but only in Python, because in JS we cannot parse Python code.

The string is problematic because it refers to 'line.text', meaning that the variable 'line' should somehow be evaluated in the namespace that defines the string, which is possible with sys._getframe() but hacky/ugly/fragile.

Perhaps something like this could work though:

label = Label(text=follow(line, 'text'))

The follow() function does not have to perform any black magic; it just returns a special object that hold the object and event name that the label should connect to. The mechanics that sets properties can detect such an object and wire things up. I can even see such a pattern working for more complex cases:

plot = PlotWidget(...., xdata = [i/100 for i in range(100)],
                        ydata=follow(s1, 'value', s2, 'value', 'xdata',
                          lambda s1_val, s2_val, xdata: [s1_val * x + 2 * PI + s2_val for x in xdata]))

@almarklein
Copy link
Member

As for in-line event handlers, I think this would be more difficult, since lambdas are not really suited (cannot set an attribute in a lambda, for instance). I can imagine something like this:

def on_click(*events):
    prog.value += 1

button1 = Button(on_mouse_click=on_click)

where keyword args starting with on_ can be provided to easily provide handlers. Though this would safe only one line compared to how you'd write it now, so this might not be worth it.

@Korijn
Copy link

Korijn commented Feb 21, 2017

To me, it seems like the simplest syntax for the user is most desirable.

label = Label(text=line.text)

line.text is an @event.prop proxy object, and Label(text= also maps to an @event.prop proxy object. It seems kind of weird to me that that is not enough for Flexx to connect the two?

@almarklein
Copy link
Member

line.text returns the value of the property, not the prop object.

@Korijn
Copy link

Korijn commented Feb 21, 2017

Well, that's what I'm thinking; it should perhaps be the prop object instead. That's how libraries like vue.js and react.js accomplish it, I think.

@almarklein
Copy link
Member

I am not sure if I follow. That would mean that you'd have to write line.text.value everywhere. This brings us more or less back to the Signal system that we had a year and a half ago.

But thinking in this direction, maybe something like Label(text=line.prop('text')) could work as an alternative to Label(text=follow(line 'text')) ...

@niki-sp
Copy link

niki-sp commented Feb 21, 2017 via email

@Korijn
Copy link

Korijn commented Feb 21, 2017

You can make a prop object and its value behave like they are the same thing by implementing the Object Proxy pattern.

@frmdstryr
Copy link
Author

The follow() function does not have to perform any black magic; it just returns a special object that hold the object and event name that the label should connect to.

At minute 10 in this video Enaml - A Framework for Building Declarative User Interfaces , he explains cases in which using something like follow wouldn't work. However I think most cases would be fine.

As for in-line event handlers ... where keyword args starting with on_ can be provided to easily provide handlers

This is similar to how kivy does it: https://kivy.org/docs/guide/lang.html. While it does not really save many lines (unless using lambdas with setattr) it does somewhat help keep the code more organized, and is nicer than doing separate bindings. Not sure how scoping would work with js handlers, aren't they in separate classes?

@frmdstryr
Copy link
Author

Regarding a potential enaml toolkit implementation...

I found out that with enaml can you can override the operators and define code tracers that can watch any object that can be observed with callbacks (ie so it could be made to observe changes to flexx.app.Model).

What is also interesting is that NEW custom operators can be defined (as is done here). So for instance you could define separate js handlers and py handlers by using different operators for each.

Since enaml manipulates python bytecode I doubt it can run in js. I'm not sure how that would impact it's use with flexx as I don't quite understand what get's converted to js and pushed to the browser and what doesn't.


On a side note (possibly should move to another discussion), is there a reason why flexx uses annotations for properties instead of a declarative property pattern (like traitlets, atom, kivy properties, django model fields, etc...)? If properties then could be tagged with metadata, it would be easy to know if a property is or is a not a js property.

class MyModel(Model):

   # String, List, and Float would all be a subclass of some base Property object (ie an event.prop)
    baz = String().tag(js=False) # Py only property
    foo = Float(0) # defaults to both js and py
    bar = List([1,2,3]).tag(py=False)
    fee = Int(28).tag(writable=False) # Read only
    sig = Emitter() # Emitter

    @event.connect('foo')
    def handle_changes_to_foo_in_python(self, *events):
        ...  # knows it's a binding for both since foo is tagged for py and js by default

   @event.connect('bar')
   def handle_changes_to_foo_in_js(self, *events):
        ...  # knows it's a js only binding because bar is tagged py=False

Don't know, just wondering..

@almarklein
Copy link
Member

almarklein commented Feb 22, 2017

Thanks for that video link. Very interesting how Enaml achieves this feat of not having to setup any connections. Unfortunately, like you expected, this approach cannot be used in the JS part of our models, which makes it not well suited for Flexx.

I did some experimenting yesterday, and I think that via another approach we can achieve this:

# Simply track one property
label = Label(text=lambda: line.text)

# More complex case
plotwidget = PlotWidget(...,
    ydata=lambda:[slider1.value * x**2 + slider2.value * x for x in plotwidget.xdata]
    )

This would set up a handler wrapping the lambda, and connect that handler automatically to any properties used in the handler. Similarly, this allows writing handlers without having to specify any connection strings. The method below would get automatically called when any of the properties it uses
change, and its connections are also updated accordingly.

class MyModel(ui.Widget):
    @trackthis
    def some_method(self):
         self.label.text = 'The names: ' + ', '.join([c.name.value for c in self.children])

I'd need to look into this some more. I am not sure yet how this will affect performance, for instance. But I do like the idea of this. Also, how do events that don't originate from a property fit into this (perhaps just connect these as is done now)?

Edit, some background on how I think we can achive this: we can make the property getters call into a "tracker object" which is turned on right before calling the function. When the function is done, we thus have a description of the used properties, and can connect these up. It does mean some over head for each time that the handler is called, but perhaps that overhead can be made small for cases when there are no reconnections to be made.

@almarklein
Copy link
Member

About the declarative property pattern: that is something that I've kept in mind, and I think this could still be implemented without having to change any of the existing behavior. I've been reluctant to do this though, because then we'd have 2 ways to do the same thing, and although the pattern is way more compact for simple cases, this advantage gets less when one adds proper documentation, or when you get complex enum structures, which are often solved much easier in a more imperative way. If you'd like to discuss this more, let's open a new issue.

@Korijn
Copy link

Korijn commented Feb 22, 2017

when you get complex enum structures, which are often solved much easier in a more imperative way

I'd love to dive into a few specific examples and see how declarative UI libraries measure up against imperative approaches.

@almarklein
Copy link
Member

Some more thoughts ... which you may not like :)

Although I was enthusiastic at first, I now think that I do not like the inline lambda pattern:

Label(text=lambda: line.text)

The reason is that such a pattern can only be applied Python side, because models are only instantiated from Python. Propagating such an approach would make people write apps that cannot be exported to standalone HTML.

You could use a pattern of setting a property to a function in JS ... but I think that somewhat defeats the purpose. :

class MyModel(app.Widget):
    def init(self):
        self.line = TextEdit()
        self.label = Label()
    
    class JS:
        def init(self):
            self.label.text = lambda: self.line,text

What is seems to come down to, is that by its nature of classes that define Python and JS behavior, Flexx likes the handlers to be methods. Either on the main class (for Python-side behavior) or in the nested JS class (for JS-side behavior).

What we could make work (as I mentioned earlier in a slightly different form):

@event.react
def _react_line(self):
    self.label.text = self.line.text

Which would be a handler, that you can define either for Py or JS, and that gets automatically called whenever line.text changes. And it can be made as complex as you want. We could even consider leaving the decorator and make all methods with a certain prefix reactive.

In the same line, it could allow computed properties:

@event.computed
def full_name(self):
    return self.first_name + ' ' + self.last_name

This pattern would co-exists with the current @event.connect(...) pattern, I think, because I cannot think of a matching way to hook up to e.g. mouse events.

@almarklein
Copy link
Member

To clarify my previous comment. I think that if we go with the @react methods, we can also allow setting a property with a (reactive) function, and thus allow Enaml-like syntax. Its just that I don't think we should use that pattern in the examples (except specific ones).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants