diff --git a/baukit/labwidget.py b/baukit/labwidget.py index 381f283..ca08ed8 100644 --- a/baukit/labwidget.py +++ b/baukit/labwidget.py @@ -217,7 +217,7 @@ class Widget(Model): Override to define the initial HTML view of the widget. Should define an element with id given by view_id(). ''' - with show.enter_tag() as t: + with show.enter() as t: return t.begin() + t.end() def view_id(self): @@ -558,7 +558,7 @@ class Button(Widget): ''') def widget_html(self): - return show.emit_tag('input', self.std_attrs(), + return show.emit('input', self.std_attrs(), type='button', value=self.label) class Label(Widget): @@ -579,7 +579,7 @@ class Label(Widget): def widget_html(self): out = [] - with show.enter_tag('label', self.std_attrs(), out=out): + with show.enter('label', self.std_attrs(), out=out): out.append(html.escape(str(self.value))) return ''.join(out) @@ -615,7 +615,7 @@ class Textbox(Widget): ''') def widget_html(self): - return show.emit_tag('input', self.std_attrs(), + return show.emit('input', self.std_attrs(), type='text', value=self.value, size=self.size) @@ -650,7 +650,7 @@ class Numberbox(Widget): ''') def widget_html(self): - return show.emit_tag('input', self.std_attrs(), + return show.emit('input', self.std_attrs(), type='numeric', value=self.value, size=self.size) class Textarea(Widget): @@ -680,7 +680,7 @@ class Textarea(Widget): def widget_html(self): out = [] - with show.enter_tag('textarea', self.std_attrs(), out=out): + with show.enter('textarea', self.std_attrs(), out=out): out.append(html.escape(self.value)) return ''.join(out) @@ -711,7 +711,7 @@ class Range(Widget): ''') def widget_html(self): - return show.emit_tag('input', self.std_attrs(), + return show.emit('input', self.std_attrs(), type='range', value=self.value, min=self.min, max=self.max, step=self.step) @@ -738,7 +738,7 @@ class ColorPicker(Widget): ''') def widget_html(self): - return show.emit_tag('input', self.std_attrs(), + return show.emit('input', self.std_attrs(), type='color', value=self.value) @@ -783,10 +783,10 @@ class Choice(Widget): def widget_html(self): out = [] - with show.enter_tag('form', self.std_attrs(), out=out): + with show.enter('form', self.std_attrs(), out=out): for value in self.choices: - with show.enter_tag('label', out=out): - show.emit_tag('input', + with show.enter('label', out=out): + show.emit('input', (show.attrs(checked=None) if value == self.selection else None), name='choice', type='radio', value=value, out=out) return ''.join(out) @@ -830,10 +830,10 @@ class Menu(Widget): def widget_html(self): out = [] - with show.enter_tag('form', self.std_attrs(), out=out): - with show.enter_tag(show.Tag('select', name='menu'), out=out): + with show.enter('form', self.std_attrs(), out=out): + with show.enter(show.Tag('select', name='menu'), out=out): for value in self.choices: - with show.enter_tag(show.Tag('option', + with show.enter(show.Tag('option', (show.attr(selected=None) if value == self.selection else None), value=value), out=out): out.append(html.escape(str(value))) @@ -898,13 +898,13 @@ class Datalist(Widget): def widget_html(self): out = [] - with show.enter_tag('form', self.std_attrs(), + with show.enter('form', self.std_attrs(), onsubmit='return false;', out=out): - show.emit_tag('input', name='inp', list=self.datalist_id(), + show.emit('input', name='inp', list=self.datalist_id(), autocomplete='off', out=out) - with show.enter_tag(show.Tag('datalist'), id=self.datalist_id()): + with show.enter(show.Tag('datalist'), id=self.datalist_id()): for value in self.choices: - show.emit_tag('option', value=str(value)) + show.emit('option', value=str(value)) class Div(Widget): @@ -953,7 +953,7 @@ class Div(Widget): def widget_html(self): out = [] - with emit.enter_tag(self.std_attrs(), out=out): + with show.enter(self.std_attrs(), out=out): out.append(self.innerHTML); return ''.join(out) @@ -1022,7 +1022,7 @@ class Image(Widget): ''') def widget_html(self): - return show.emit_tag('img', self.std_attrs(), show.style(margin=0), + return show.emit('img', self.std_attrs(), show.style(margin=0), src=self.src) diff --git a/baukit/paintwidget.py b/baukit/paintwidget.py index 08b4f3e..ac117f4 100644 --- a/baukit/paintwidget.py +++ b/baukit/paintwidget.py @@ -41,7 +41,7 @@ class PaintWidget(Widget): opacity: 0; transition: opacity .1s ease-in-out; }} #{v} .paintmask.vanishing:hover {{ opacity: { self.opacity }; }} ''')] - show.emit_tag(self.std_attrs(), out=out) + show.emit(self.std_attrs(), out=out) return ''.join(out) diff --git a/baukit/show.py b/baukit/show.py index bff6ed0..694e92a 100644 --- a/baukit/show.py +++ b/baukit/show.py @@ -53,7 +53,7 @@ def raw_html(*args): return HtmlRepr(''.join(str(x) for x in args)) @contextmanager -def enter_tag(*args, out=None, **kwargs): +def enter(*args, out=None, **kwargs): ''' Context manager for creating and emitting a tag and its matching end-tag. When the tag is created, any current defaults and options @@ -63,7 +63,7 @@ def enter_tag(*args, out=None, **kwargs): ``` out = [] - with show.enter_tag('div', id='d38', style(topMargin='8px'), out=out): + with show.enter('div', id='d38', style(topMargin='8px'), out=out): out.append('inside the div') ``` @@ -95,15 +95,15 @@ def enter_tag(*args, out=None, **kwargs): tag_modifications.clear() tag_stack.pop() -def emit_tag(*args, out=None, **kwargs): +def emit(*args, out=None, **kwargs): ''' Emits the specified tag, applying any current defaults and options - to the styles and attributes. Options are handled as with enter_tag. + to the styles and attributes. Options are handled as with enter. If no `out` array is provided, returns the tag as a string. ''' emit_out = [] if out is None else out - with enter_tag(*args, out=emit_out, **kwargs): + with enter(*args, out=emit_out, **kwargs): if out is None: return ''.join(emit_out) @@ -118,7 +118,7 @@ CSS_UNITS = dict([(k, unit) for keys, unit in [ ('border border-left border-right border-top border-bottom ' 'border-width border-left-width border-right-width ' 'border-top-width border-bottom-width', 'px'), - ('border-spacing letter-spacing word-spacing', 'px') + ('border-spacing letter-spacing word-spacing', 'px'), ('margin margin-left margin-right margin-top margin-bottom', 'px'), ('padding padding-left padding-right padding-top padding-bottom', 'px'), ] for k in keys.split(' ')]) @@ -127,6 +127,8 @@ def hyphenateCamelKeys(d): return {re.sub('([A-Z]+)', r'-\1', k).lower() : v for k, v in d.items()} def styleValue(v, k): + if callable(v): + v = v() if isinstance(v, (int, float)) and k in CSS_UNITS: return f'{v}{CSS_UNITS[k]}' return str(v) @@ -208,16 +210,27 @@ class Tag: def tag(*args, **kwargs): return Tag(*args, **kwargs) +def inherit_value(top, inner='inherit'): + ''' + A callable style value that resolves to one default at the top level + and a different value when nested inside tags. + ''' + def resolve(): + return top if len(tag_stack) <= 2 else inner + return resolve + # This is the default loop for nesting children: horizontal layout by default, # and then vertical layout for nested arrays; then horizontal within those, etc. -H = Tag( - style(display='flex', flex='1', flexFlow='row wrap', gap='3px', - alignItems='center')) V = Tag( - style(display='flex', flex='1', flexFlow='column', gap='3px'), - ChildTag(H)) -H.update(ChildTag(V)) + style(display='flex', flex='1', flexFlow='column', + gap=inherit_value(3))) + +H = Tag( + style(display='flex', flex='1', flexFlow='row wrap', + gap=inherit_value(3)), + ChildTag(V)) +V.update(ChildTag(H)) # Tables TD = Tag('td', ChildTag(H)) @@ -230,13 +243,14 @@ PLAIN = Tag() # The TIGHT style allows the content to provide the size, instead of # expanding to fill the available space. TIGHT = Tag( - style(display='inline-flex', flex=None, flexFlow='column', gap='3px'), + style(display='inline-flex', flex=None, flexFlow='column', + gap=inherit_value(3)), ChildTag(H)) # WRAP provides wrapping lines of TIGHT boxes, akin to layout of text WRAP = Tag( - style(display='flex', flex='1', flexFlow='row wrap', gap='3px', - alignItems='start'), + style(display='flex', flex='1', flexFlow='row wrap', + gap=inherit_value(3), alignItems='start'), ChildTag(TIGHT)) @@ -268,7 +282,7 @@ def render_str(obj, out): if '\n' in s: render_pre(s, out) return - with enter_tag(out=out): + with enter(out=out): out.append(escape(s)) def render_html(obj, out): @@ -299,7 +313,7 @@ def render_list(obj, out): ''' Lists are divs containin divs, alternating row-inline and column flex layout. ''' - with enter_tag(out=out): + with enter(out=out): for v in obj: render(v, out) @@ -307,12 +321,12 @@ def render_dict(obj, out): ''' Dicts become tables. ''' - with enter_tag(TABLE, out=out): + with enter(TABLE, out=out): for k, v in obj.items(): - with enter_tag(out=out): - with enter_tag(out=out): + with enter(out=out): + with enter(out=out): out.append(escape(str(k))) - with enter_tag(out=out): + with enter(out=out): render(v, out) def render_image(obj, out): @@ -332,14 +346,14 @@ def render_image(obj, out): buf.close() except: return False - emit_tag('img', attr(src=src), style(flex='0'), out=out) + emit('img', attr(src=src), style(flex='0'), out=out) def render_pre(obj, out): ''' Long multiline text data types are rendered in
 tags.
     '''
     s = str(obj)
-    with enter_tag('pre', out=out):
+    with enter('pre', out=out):
         out.append(escape(s))
 
 def render_modifications(obj, out):
@@ -353,7 +367,7 @@ def render_pandas(obj, out):
     '''
     Allows control of Pandas outer-level table CSS and HTML attributes.
     '''
-    with enter_tag(TABLE, style(display=None, flexFlow=None, gap=None, alignItems=None)) as t:
+    with enter(TABLE, style(display=None, flexFlow=None, gap=None, alignItems=None)) as t:
         styler = obj.style
         css = str(t.style)
         if css:
@@ -382,7 +396,7 @@ RENDERING_RULES = [
         ((Style, Attr, Tag, ChildTag), render_modifications),
         # Pandas dataframes even though they have a _repr_html_
         (subclass_of('pandas.core.frame.DataFrame'), render_pandas),
-        # Objects with an html repr
+        # Objects with an mimebundle repr, like altair charts
         ((lambda x: hasattr(x, '_repr_mimebundle_')), render_mimebundle),
         # Objects with an html repr
         ((lambda x: hasattr(x, '_repr_html_')), render_html),
@@ -394,7 +408,7 @@ RENDERING_RULES = [
         (subclass_of('PIL.Image.Image'), render_image),
         # Matplotlib figures
         (subclass_of('matplotlib.figure.Figure'), render_image),
-        # Numpy, pytorch arrays
+        # Numpy, pytorch arrays are often too big to render every item
         (lambda x: hasattr(x, 'shape') and hasattr(x, 'dtype'), render_pre),
         # Generators and lists: recurse
         ((lambda x: hasattr(x, '__iter__')), render_list),