From a9d52f17e0c42028a91438bbbfd9fac129e23fa8 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 9 Apr 2019 09:33:06 +0200 Subject: [PATCH 1/6] Use css grid for form layout (instead of table) --- flexx/ui/layouts/_form.py | 195 +++++--------------------------------- 1 file changed, 24 insertions(+), 171 deletions(-) diff --git a/flexx/ui/layouts/_form.py b/flexx/ui/layouts/_form.py index 57b08e1d..2bf4246b 100644 --- a/flexx/ui/layouts/_form.py +++ b/flexx/ui/layouts/_form.py @@ -18,201 +18,54 @@ def init(self): """ -from pscript import window, undefined +from pscript import window -from ... import event from . import Layout from .. import create_element -class BaseTableLayout(Layout): - """ Abstract base class for layouts that use an HTML table. - - Layouts that use this approach don't have good performance when - resizing. This is not so much a problem when it is used as a leaf - layout, but it's not recommended to embed such layouts in each-other. - """ - - CSS = """ - - /* Clear any styling on this table (rendered_html is an IPython thing) */ - .flx-BaseTableLayout, .flx-BaseTableLayout td, .flx-BaseTableLayout tr, - .rendered_html .flx-BaseTableLayout { - border: 0px; - padding: initial; - margin: initial; - background: initial; - } - - /* Behave well inside hbox/vbox, - we assume no layouts to be nested inside a table layout */ - .flx-box.flx-horizontal > .flx-BaseTableLayout { - width: auto; - } - .flx-box.flx-vertical > .flx-BaseTableLayout { - height: auto; - } - - td.flx-vflex, td.flx-hflex { - padding: 2px; - } - - /* In flexed cells, occupy the full space */ - td.flx-vflex > .flx-Widget { - height: 100%; - } - td.flx-hflex > .flx-Widget { - width: 100%; - } - """ - - - def _apply_table_layout(self): - table = self.node - AUTOFLEX = 729 # magic number unlikely to occur in practice - - # Get table dimensions - nrows = len(table.children) - ncols = 0 - for i in range(len(table.children)): - row = table.children[i] - ncols = max(ncols, len(row.children)) - if ncols == 0 and nrows == 0: - return - - # Collect flexes - vflexes = [] - hflexes = [] - for i in range(nrows): - row = table.children[i] - for j in range(ncols): - col = row.children[j] - if (col is undefined) or (len(col.children) == 0): - continue - vflexes[i] = max(vflexes[i] or 0, col.children[0].vflex or 0) - hflexes[j] = max(hflexes[j] or 0, col.children[0].hflex or 0) - - # What is the cumulative "flex-value"? - cum_vflex = vflexes.reduce(lambda pv, cv: pv + cv, 0) - cum_hflex = hflexes.reduce(lambda pv, cv: pv + cv, 0) - - # If no flexes are given; assign each equal - if (cum_vflex == 0): - for i in range(len(vflexes)): - vflexes[i] = AUTOFLEX - cum_vflex = len(vflexes) * AUTOFLEX - if (cum_hflex == 0): - for i in range(len(hflexes)): - hflexes[i] = AUTOFLEX - cum_hflex = len(hflexes) * AUTOFLEX - - # Assign css class and height/weight to cells - for i in range(nrows): - row = table.children[i] - row.vflex = vflexes[i] or 0 # Store for use during resizing - for j in range(ncols): - col = row.children[j] - if (col is undefined) or (col.children.length == 0): - continue - self._apply_cell_layout(row, col, vflexes[i], hflexes[j], - cum_vflex, cum_hflex) - - @event.reaction('size') - def _adapt_to_size_change(self, *events): - """ This function adapts the height (in percent) of the flexible rows - of a layout. This is needed because the percent-height applies to the - total height of the table. This function is called whenever the - table resizes, and adjusts the percent-height, taking the available - remaining table height into account. This is not necesary for the - width, since percent-width in colums *does* apply to available width. - """ - table = self.node # or event.target - #print('heigh changed', event.heightChanged, event.owner.__id) - - if events[-1].new_value[1] != events[0].old_value[1]: - - # Set one flex row to max, so that non-flex rows have their - # minimum size. The table can already have been stretched - # a bit, causing the total row-height in % to not be - # sufficient from keeping the non-flex rows from growing. - for i in range(len(table.children)): - row = table.children[i] - if (row.vflex > 0): - row.style.height = '100%' - break - - # Get remaining height: subtract height of each non-flex row - remainingHeight = table.clientHeight - cum_vflex = 0 - for i in range(len(table.children)): - row = table.children[i] - cum_vflex += row.vflex - if (row.vflex == 0) and (row.children.length > 0): - remainingHeight -= row.children[0].clientHeight - - # Apply height % for each flex row - remainingPercentage = 100 * remainingHeight / table.clientHeight - for i in range(len(table.children)): - row = table.children[i] - if row.vflex > 0: - row.style.height = round(row.vflex /cum_vflex * - remainingPercentage) + 1 + '%' - - def _apply_cell_layout(self, row, col, vflex, hflex, cum_vflex, cum_hflex): - raise NotImplementedError() - - - -class FormLayout(BaseTableLayout): +class FormLayout(Layout): """ A layout widget that vertically alligns its child widgets in a form. A label is placed to the left of each widget (based on the widget's title). The ``node`` of this widget is a - ``_. - (This may be changed to use a CSS layout instead.) + `
`_, + which lays out it's child widgets and their labels using + `CSS grid `_. """ CSS = """ - .flx-FormLayout .flx-title { + .flx-FormLayout { + display: grid; + grid-template-columns: auto 1fr; + justify-content: stretch; + align-content: stretch; + justify-items: stretch; + align-items: center; + + } + .flx-FormLayout > .flx-title { text-align: right; padding-right: 5px; } """ def _create_dom(self): - return window.document.createElement('table') + return window.document.createElement('div') def _render_dom(self): rows = [] + row_templates = [] for widget in self.children: - row = create_element('tr', {}, - create_element('td', {'class': 'flx-title'}, widget.title), - create_element('td', {}, [widget.outernode]), - ) - widget.outernode.hflex = 1 - widget.outernode.vflex = widget.flex[1] - rows.append(row) - event.loop.call_soon(self._apply_table_layout) + rows.extend([ + create_element('div', {'class': 'flx-title'}, widget.title), + widget.outernode, + ]) + flex = widget.flex[1] + row_templates.append(flex + "fr" if flex > 0 else "auto") + self.node.style['grid-template-rows'] = " ".join(row_templates) return rows - def _apply_cell_layout(self, row, col, vflex, hflex, cum_vflex, cum_hflex): - AUTOFLEX = 729 - className = '' - if (vflex == AUTOFLEX) or (vflex == 0): - row.style.height = 'auto' - className += '' - else: - row.style.height = vflex * 100 / cum_vflex + '%' - className += 'flx-vflex' - className += ' ' - if (hflex == 0): - col.style.width = 'auto' - className += '' - else: - col.style.width = '100%' - className += 'flx-hflex' - col.className = className - def _query_min_max_size(self): """ Overload to also take child limits into account. """ From 02f1af00e9d8b17d3c32f1a3c54db55bf49912e5 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 9 Apr 2019 09:52:39 +0200 Subject: [PATCH 2/6] improve docs on node vs outernode --- flexx/ui/_widget.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/flexx/ui/_widget.py b/flexx/ui/_widget.py index 92c791a0..d23a7663 100644 --- a/flexx/ui/_widget.py +++ b/flexx/ui/_widget.py @@ -123,7 +123,8 @@ class Widget(app.JsComponent): equal to ``outernode``. For the ``Widget`` class, this is simply a `
`_ element. If you don't understand what this is about, don't worry; - you won't need it unless you are creating your own low-level widgets :) + you won't need it unless you are creating your own low-level widgets. + See ``_create_dom()`` for details. When implementing your own widget class, the class attribute ``DEFAULT_MIN_SIZE`` can be set to specify a sensible minimum size. @@ -402,6 +403,15 @@ def _create_dom(self): values. These attributes must remain unchanged throughout the lifetime of a widget. This method can be overloaded in subclasses. + + Most widgets have the same value for ``node`` and ``outernode``. + However, in some cases it helps to distinguish between the + semantic "actual node" and a wrapper. E.g. Flexx uses it to + properly layout the ``CanvasWidget`` and ``TreeWidget``. + Internally, Flexx uses the ``node`` attribute for tab-index, and + binding to mouse/touch/scroll/key events. If your ``outernode`` + already semantically represents your widget, you should probably + just use that. """ return create_element('div') From c24fa483ab8976f9d770aa7cbde0fa23ee5cb823 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 9 Apr 2019 12:20:30 +0200 Subject: [PATCH 3/6] Add GridLaout --- flexx/ui/_widget.py | 2 +- flexx/ui/layouts/__init__.py | 1 + flexx/ui/layouts/_grid.py | 105 +++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 flexx/ui/layouts/_grid.py diff --git a/flexx/ui/_widget.py b/flexx/ui/_widget.py index d23a7663..e06242d3 100644 --- a/flexx/ui/_widget.py +++ b/flexx/ui/_widget.py @@ -407,7 +407,7 @@ def _create_dom(self): Most widgets have the same value for ``node`` and ``outernode``. However, in some cases it helps to distinguish between the semantic "actual node" and a wrapper. E.g. Flexx uses it to - properly layout the ``CanvasWidget`` and ``TreeWidget``. + properly layout the ``CanvasWidget`` and ``TreeItem``. Internally, Flexx uses the ``node`` attribute for tab-index, and binding to mouse/touch/scroll/key events. If your ``outernode`` already semantically represents your widget, you should probably diff --git a/flexx/ui/layouts/__init__.py b/flexx/ui/layouts/__init__.py index d472cc13..7163c49a 100644 --- a/flexx/ui/layouts/__init__.py +++ b/flexx/ui/layouts/__init__.py @@ -11,3 +11,4 @@ from ._tabs import TabLayout from ._pinboard import PinboardLayout from ._form import FormLayout +from ._grid import GridLayout diff --git a/flexx/ui/layouts/_grid.py b/flexx/ui/layouts/_grid.py new file mode 100644 index 00000000..068c89ee --- /dev/null +++ b/flexx/ui/layouts/_grid.py @@ -0,0 +1,105 @@ +""" Grid layout. + +Layout a series of widgets in a grid. The grid has a specified number of columns. +Example: + +.. UIExample:: 300 + + from flexx import flx + + class Example(flx.Widget): + def init(self): + with flx.HSplit(): + with flx.GridLayout(ncolumns=3): + flx.Button(text='A') + flx.Button(text='B') + flx.Button(text='C') + flx.Button(text='D') + flx.Button(text='E') + flx.Button(text='F') + + with flx.GridLayout(ncolumns=2): + flx.Button(text='A', flex=(1, 1)) # Set flex for 1st row and col + flx.Button(text='B', flex=(2, 1)) # Set flex for 2nd col + flx.Button(text='C', flex=(1, 1)) # Set flex for 2nd row + flx.Button(text='D') + flx.Button(text='E', flex=(1, 2)) # Set flex for 3d row + flx.Button(text='F') + +""" + +from ... import event +from . import Layout + + +class GridLayout(Layout): + """ A layout widget that places its children in a grid with a certain number + of columns. The flex values of the children in the first row determine the + sizing of the columns. The flex values of the first child of each row + determine the sizing of the rows. + + The ``node`` of this widget is a + `
`_, + which lays out it's child widgets and their labels using + `CSS grid `_. + """ + + CSS = """ + .flx-GridLayout { + display: grid; + justify-content: stretch; + align-content: stretch; + justify-items: stretch; + align-items: stretch; + } + """ + + ncolumns = event.IntProp(2, settable=True, doc=""" + The number of columns of the grid. + """) + + @event.reaction + def _on_columns(self): + ncolumns = self.ncolumns + children = self.children + column_templates = [] + row_templates = [] + for i in range(min(ncolumns, len(children))): + flex = children[i].flex[0] + column_templates.append(flex + "fr" if flex > 0 else "auto") + for i in range(0, len(children), ncolumns): + flex = children[i].flex[1] + row_templates.append(flex + "fr" if flex > 0 else "auto") + + self.node.style['grid-template-rows'] = " ".join(row_templates) + self.node.style['grid-template-columns'] = " ".join(column_templates) + + def _query_min_max_size(self): + """ Overload to also take child limits into account. + """ + + # Collect contributions of child widgets + mima1 = [0, 1e9, 0, 0] + for child in self.children: + mima2 = child._size_limits + mima1[0] = max(mima1[0], mima2[0]) + mima1[1] = min(mima1[1], mima2[1]) + mima1[2] += mima2[2] + mima1[3] += mima2[3] + + # Dont forget padding and spacing + extra_padding = 2 + extra_spacing = 2 + for i in range(4): + mima1[i] += extra_padding + mima1[2] += extra_spacing + mima1[3] += extra_spacing + + # Own limits + mima3 = super()._query_min_max_size() + + # Combine own limits with limits of children + return [max(mima1[0], mima3[0]), + min(mima1[1], mima3[1]), + max(mima1[2], mima3[2]), + min(mima1[3], mima3[3])] From e10cf41fcdd48ecada48eab28d2ab19c7fa43faa Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 9 Apr 2019 13:27:05 +0200 Subject: [PATCH 4/6] Implement MultiLineEditr --- flexx/ui/widgets/__init__.py | 2 +- flexx/ui/widgets/_lineedit.py | 67 +++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/flexx/ui/widgets/__init__.py b/flexx/ui/widgets/__init__.py index be9fe1c2..509481f8 100644 --- a/flexx/ui/widgets/__init__.py +++ b/flexx/ui/widgets/__init__.py @@ -6,7 +6,7 @@ from .. import Widget from ._button import BaseButton, Button, ToggleButton, RadioButton, CheckBox -from ._lineedit import LineEdit +from ._lineedit import LineEdit, MultiLineEdit from ._label import Label from ._group import GroupWidget from ._iframe import IFrame diff --git a/flexx/ui/widgets/_lineedit.py b/flexx/ui/widgets/_lineedit.py index 606a31a7..ddca4cb3 100644 --- a/flexx/ui/widgets/_lineedit.py +++ b/flexx/ui/widgets/_lineedit.py @@ -1,4 +1,7 @@ -""" LineEdit +""" + +The ``LineEdit`` and ``MultiLineEdit`` widgets provide a way for the user +to input text. .. UIExample:: 100 @@ -35,7 +38,7 @@ def when_user_submits_text(self, *events): class LineEdit(Widget): """ An input widget to edit a line of text. - + The ``node`` of this widget is a text ` `_. """ @@ -179,3 +182,63 @@ def __disabled_changed(self): self.node.setAttribute("disabled", "disabled") else: self.node.removeAttribute("disabled") + + +class MultiLineEdit(Widget): + """ An input widget to edit multiple lines of text. + + The ``node`` of this widget is a + `