Repository: zoofIO/flexx
Pull request #90 (2016-02-08 22:25:56)

Flexbox improvements, canvaswidget minsize, asset order 


Patch:
diff --git a/flexx/app/assetstore.py b/flexx/app/assetstore.py
index 8c42733..b9717ea 100644
--- a/flexx/app/assetstore.py
+++ b/flexx/app/assetstore.py
@@ -33,20 +33,69 @@ INDEX = """<!doctype html>
     <meta charset="utf-8">
     <title>Flexx UI</title>
 
-JS-HOOK
-    
-CSS-HOOK
+ASSET-LINK-HOOK
 
 </head>
 
 <body id='body'>
 
-INDEX-HOOK
+ASSET-CONTENT-HOOK
 
 </body>
 </html>
 """
 
+RESET = """
+/* http://meyerweb.com/eric/tools/css/reset/ 
+v2.0 | 20110126
+License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed, 
+figure, figcaption, footer, header, hgroup, 
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    font-size: 100%;
+    font: inherit;
+    vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure, 
+footer, header, hgroup, menu, nav, section {
+    display: block;
+}
+body {
+    line-height: 1;
+}
+ol, ul {
+    list-style: none;
+}
+blockquote, q {
+    quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+    content: '';
+    content: none;
+}
+table {
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+"""
+
 
 def modname_startswith(x, y):
     return (x + '.').startswith(y + '.')
@@ -75,6 +124,7 @@ class AssetStore:
         self._cache = {}
         self._assets = {}
         self._module_names = []
+        self.add_asset('reset.css', RESET.encode())
     
     def _cache_get(self, key):
         """ Get content using caching.
@@ -241,7 +291,7 @@ class SessionAssets:
         if url not in self._remote_asset_names:
             self._remote_asset_names.append(url)
     
-    def use_global_asset(self, fname):
+    def use_global_asset(self, fname, before=None):
         """ Make this session use a global asset.
         
         The given asset must be available in the global asset store
@@ -251,6 +301,8 @@ class SessionAssets:
         
         Parameters:
             fname (str): the (relative) filename for the asset.
+            before (str, None): If given, places the new asset before the
+                given asset.
         """
         if not isinstance(fname, str):
             raise ValueError('Asset name must be a string.')
@@ -267,9 +319,13 @@ class SessionAssets:
             self._send_command('DEFINE-%s %s' % (suffix, code))
             #logging.warn('Adding asset %r but the page was already "served".' % fname)
         
-        self._asset_names.append(fname)
+        if before:
+            index = self._asset_names.index(before)
+            self._asset_names.insert(index, fname)
+        else:
+            self._asset_names.append(fname)
     
-    def add_asset(self, fname, content):
+    def add_asset(self, fname, content, before=None):
         """ Add an asset specific for this session.
         
         Assets are global to the process (stored in AssetStore
@@ -281,6 +337,8 @@ class SessionAssets:
             content (str, bytes): the content of the asset. If a str,
                 it is interpreted as the filename or url to the asset.
                 If bytes, it is interpreted as the raw asset content.
+            before (str, None): If given, places the new asset before the
+                given asset.
         Returns:
             fname (str): A mangled version of the given ``fname``.
         """
@@ -288,10 +346,10 @@ class SessionAssets:
             raise ValueError('Asset name must be a string.')
         part1, dot, part2 = fname.rpartition('.')
         fname = '%s-%s%s%s' % (part1, self.id, dot, part2)
-        self.add_global_asset(fname, content)
+        self.add_global_asset(fname, content, before)
         return fname
     
-    def add_global_asset(self, fname, content):
+    def add_global_asset(self, fname, content, before=None):
         """ Add an asset that is global to this process.
         
         Use this for JS and CSS, but probably not for images and content
@@ -301,7 +359,7 @@ class SessionAssets:
         in the global assets, as long as it has the same content.
         """
         self._store.add_asset(fname, content)
-        self.use_global_asset(fname)
+        self.use_global_asset(fname, before)
     
     def register_model_class(self, cls):
         """ Ensure that the client knows the given class. A class can
@@ -353,7 +411,7 @@ class SessionAssets:
             if css.strip():
                 self._send_command('DEFINE-CSS ' + css)
     
-    def _get_js_and_css_assets(self):
+    def _get_js_and_css_assets(self, with_reset=False):
         """ Get an ordered dictionary with the JS and CSS assets.
         """
         # Create assets from our extra model classes
@@ -366,22 +424,40 @@ class SessionAssets:
         self._served = True
         # Collect assets
         d = OrderedDict()
+        if with_reset:
+            fname = 'reset.css'
+            d[fname] = self._store.load_asset(fname).decode()
         for fname in self.get_used_asset_names():
             if fname.endswith('.js') or fname.endswith('.css'):
                 d[fname] = self._store.load_asset(fname).decode()
         return d
     
-    def get_all_css_and_js(self):
-        """ Get a string with all css and a string with all JS.
+    def get_js_only(self):
+        """ Get all JS assets as a single string. Intended for apps
+        that make no use of CSS (like in Node).
         """
-        css, js = [], []
+        js = []
         for fname, code in self._get_js_and_css_assets().items():
-            if fname.endswith('.css'):
-                css.append(code)
-            elif fname.endswith('.js'):
+            if fname.endswith('.js'):
                 js.append(code)
-            
-        return '\n\n'.join(css), '\n\n'.join(js)
+        return '\n\n'.join(js)
+    
+    def get_assets_as_html(self):
+        """ Get a list of "<script>" and "<style>" html elements (as
+        strings) representing the assets. No reset.css is included, nor
+        any remote assets.
+        """
+        content_assets = []
+        for fname, code in self._get_js_and_css_assets(False).items():
+            if not code.strip():  # pragma: no cover
+                continue
+            elif fname.endswith('.css'):
+                t = "<style>\n/* CSS for %s */\n%s\n</style>"
+                content_assets.append(t % (fname, code))
+            else:
+                t = "<script>\n/* JS for %s */\n%s\n</script>"
+                content_assets.append(t % (fname, code))
+        return content_assets
     
     def get_page(self, single=False):
         """ Get the string for the HTML page to render this session's app.
@@ -407,43 +483,41 @@ class SessionAssets:
         composes an index page to serve/export.
         """
         # Init source code from template
-        js_elements = []  # links to external JS docs
-        css_elements = []  # links to external CSS docs
-        index_elements = []  # code to put in the index
+        link_assets = []
+        content_assets = []
         
         # Collect remote assets
         for url in self._remote_asset_names:
             if url.endswith('.css'):
                 t = "    <link rel='stylesheet' type='text/css' href='%s' />"
-                css_elements.append(t % url)
+                link_assets.append(t % url)
             else:
                 t = "    <script src='%s'></script>"
-                js_elements.append(t % url)
+                link_assets.append(t % url)
         
         # Collect JS and CSS
-        for fname, code in self._get_js_and_css_assets().items():
+        for fname, code in self._get_js_and_css_assets(True).items():
             if not code.strip():  # pragma: no cover
                 continue
             if single or fname.startswith('index-'):
                 if fname.endswith('.css'):
                     t = "<style>\n/* CSS for %s */\n%s\n</style>"
-                    index_elements.append(t % (fname, code))
+                    content_assets.append(t % (fname, code))
                 else:
                     t = "<script>\n/* JS for %s */\n%s\n</script>"
-                    index_elements.append(t % (fname, code))
+                    content_assets.append(t % (fname, code))
             else:
                 if fname.endswith('.css'):
                     t = "    <link rel='stylesheet' type='text/css' href='%s' />"
-                    css_elements.append(t % fname)
+                    link_assets.append(t % fname)
                 else:
                     t = "    <script src='%s'></script>"
-                    js_elements.append(t % fname)
+                    link_assets.append(t % fname)
         
         # Compose index page
         src = INDEX
-        src = src.replace('JS-HOOK', '\n'.join(js_elements))
-        src = src.replace('CSS-HOOK', '\n'.join(css_elements))
-        src = src.replace('INDEX-HOOK', '\n'.join(index_elements))
+        src = src.replace('ASSET-LINK-HOOK', '\n'.join(link_assets))
+        src = src.replace('ASSET-CONTENT-HOOK', '\n'.join(content_assets))
         
         return src
 
diff --git a/flexx/app/funcs.py b/flexx/app/funcs.py
index 8f2b210..56e3cc3 100644
--- a/flexx/app/funcs.py
+++ b/flexx/app/funcs.py
@@ -131,16 +131,16 @@ def init_notebook():
     # Open server - we only use websocket for JS-to-Py communication
     _server_open()
     host, port = server.serving_at
-    all_css, all_js = session.get_all_css_and_js()
+    asset_elements = session.get_assets_as_html()
     
     # Make the JS that we inject not take any vertical space when nbconverted
-    all_css += '\n.output_subarea.output_javascript { padding: 0px; }\n'
+    extra_css = '.output_subarea.output_javascript { padding: 0px; }'
     
     # Compose HTML to inject
     url = 'ws://%s:%i/%s/ws' % (host, port, session.app_name)
     t = "<i>Injecting Flexx JS and CSS</i>"
-    t += "<style>\n%s\n</style>\n" % all_css
-    t += "<script>\n%s\n</script>" % all_js
+    t += '\n\n'.join(asset_elements)
+    t += '\n\n<style>%s</style>\n' % extra_css
     t += "<script>flexx.ws_url=%r; flexx.is_notebook=true; flexx.init();</script>" % url
     
     display(HTML(t))
@@ -189,7 +189,7 @@ def launch(cls, runtime='xul', **runtime_kwargs):
     _server_open()
     host, port = server.serving_at
     if runtime == 'nodejs':
-        all_js = session.get_all_css_and_js()[1]
+        all_js = session.get_js_only()
         url = '%s:%i/%s/' % (host, port, session.app_name)
         session._runtime = launch('http://' + url, runtime=runtime, code=all_js)
     else:
diff --git a/flexx/ui/_widget.py b/flexx/ui/_widget.py
index 6196171..697f664 100644
--- a/flexx/ui/_widget.py
+++ b/flexx/ui/_widget.py
@@ -76,7 +76,7 @@ class Widget(Model):
         Model.__init__(self, **kwargs)
         
         # All widgets need phosphor
-        self._session.use_global_asset('phosphor-all.js')
+        self._session.use_global_asset('phosphor-all.js', before='flexx-ui.css')
         
         # todo: can I make widgets not need the session immediately?
         # The fact that session and app need each-other is a bit awkward?
@@ -478,3 +478,20 @@ class Widget(Model):
             """ Remove the DOM element.
             Called right after the child widget is removed. """
             self.p.removeChild(widget.p)
+    
+        ## Special
+        
+        @react.connect('children')
+        def __update_css(self, children):
+            
+            if 'flx-Layout' not in self.node.className:
+                # Ok, no layout, so maybe we need to take care of CSS.
+                # If we have a child that is a hbox/vbox, we need to be a
+                # good container.
+                self.node.style['height'] = ''
+                self.node.style['width'] = ''
+                if len(children) == 1:
+                    subClassName = children[0].node.className
+                    if 'flx-hbox' in subClassName or 'flx-vbox' in subClassName:
+                        self.node.style['height'] = '100%'
+                        self.node.style['width'] = '100%'
diff --git a/flexx/ui/layouts/_box.py b/flexx/ui/layouts/_box.py
index dfa8a5e..4088504 100644
--- a/flexx/ui/layouts/_box.py
+++ b/flexx/ui/layouts/_box.py
@@ -198,12 +198,11 @@ class BoxLayout(BaseBoxLayout):
     /* Make child widgets (and layouts) size correctly */
     .flx-hbox > .flx-Widget, .flx-hboxr > .flx-Widget {
         height: 100%;
-        width: auto;
     }
     .flx-vbox > .flx-Widget, .flx-vboxr > .flx-Widget {
         width: 100%;
-        height: auto;
     }
+    
     """
     
     @react.input
diff --git a/flexx/ui/widgets/_canvas.py b/flexx/ui/widgets/_canvas.py
index b2f91c9..8e7a474 100644
--- a/flexx/ui/widgets/_canvas.py
+++ b/flexx/ui/widgets/_canvas.py
@@ -11,6 +11,13 @@ class CanvasWidget(Widget):
     the available space.
     """
     
+    CSS = """
+    .flx-CanvasWidget {
+        min-width: 50px;
+        min-height: 50px;
+    }
+    """ 
+    
     class JS:
         
         def _init(self):
@@ -35,7 +42,7 @@ class CanvasWidget(Widget):
         
         @react.connect('real_size')
         def _update_canvas_size(self, size):
-            if size[0] and size[1]:
+            if size[0] or size[1]:
                 self.canvas.width = size[0]
                 self.canvas.height = size[1]
                 self.canvas.style.width = size[0] + 'px'


Test patch:
diff --git a/flexx/app/tests/test_assetstore.py b/flexx/app/tests/test_assetstore.py
index 296b77f..665a036 100644
--- a/flexx/app/tests/test_assetstore.py
+++ b/flexx/app/tests/test_assetstore.py
@@ -14,7 +14,7 @@ test_filename = os.path.join(tempfile.gettempdir(), 'flexx_asset_cache.test')
 def test_asset_store_simple():
     
     s = AssetStore()
-    assert not s.get_asset_names()
+    assert len(s.get_asset_names()) == 1  # reset.css
     assert not s._cache
     
     raises(IndexError, s.load_asset, 'foo.js')
@@ -25,7 +25,7 @@ def test_asset_store_simple():
     s.add_asset('foo.css', b'foo')
     s.add_asset('foo.js', test_filename)
     
-    assert s.get_asset_names() == ['foo.css', 'foo.js']  # alphabetically
+    assert s.get_asset_names() == ['foo.css', 'foo.js', 'reset.css']  # alphabetically
     assert s.load_asset('foo.css') == b'foo'
     assert s.load_asset('foo.js') == b'bar'
     # Check caching
@@ -63,12 +63,13 @@ def test_asset_store_export():
     s = AssetStore()
     
     s.export(dir)
-    assert not os.listdir(dir)
+    assert len(os.listdir(dir)) == 1
+    assert os.path.isfile(os.path.join(dir, 'reset.css'))
     
     s.add_asset('foo.js', b'xx')
     s.add_asset('foo.css', b'xx')
     s.export(dir)
-    assert len(os.listdir(dir)) == 2
+    assert len(os.listdir(dir)) == 3
     
     # Fail
     raises(ValueError, s.export, os.path.join(dir, 'doesnotexist'))
@@ -157,7 +158,7 @@ def test_session_registering_model_classes():
     s.register_model_class(ui.Button)
     
     # Get result
-    css, js = s.get_all_css_and_js()
+    js = s.get_js_only()
     assert js.count('.Button = function ') == 1
     assert js.count('.Slider = function ') == 1
     assert js.count('.Widget = function ') == 1