Ver Fonte

Merge pull request #128 from zauberzeug/page_layout

Page layout
Rodja Trappe há 2 anos atrás
pai
commit
f0e398ef6d

+ 26 - 0
api_docs_and_examples.py

@@ -661,6 +661,32 @@ Also it is possible to do async stuff while the user already sees the content ad
 
         ui.link('show page-ready code after yield', '/yield_page_ready')
 
+    page_layout = '''#### Page Layout
+
+With `ui.header`, `ui.footer`, `ui.left_drawer` and `ui.right_drawer` you can add additional layout elements to a page.
+The `fixed` argument controls whether the element should scroll or stay fixed on the screen.
+The `top_corner` and `bottom_corner` arguments indicate whether a drawer should expand to the top or bottom of the page.
+See <https://quasar.dev/layout/header-and-footer> and <https://quasar.dev/layout/drawer> for more information about possible props like
+`elevated`, `bordered` and many more.
+With `ui.page_sticky` you can place an element "sticky" on the screen.
+See <https://quasar.dev/layout/page-sticky> for more information.
+    '''
+    with example(page_layout):
+        @ui.page('/page_layout')
+        async def page_layout():
+            ui.label('CONTENT')
+            [ui.label(f'Line {i}') for i in range(100)]
+            with ui.header().style('background-color: #3874c8').props('elevated'):
+                ui.label('HEADER')
+            with ui.left_drawer(top_corner=True, bottom_corner=True).style('background-color: #d7e3f4'):
+                ui.label('LEFT DRAWER')
+            with ui.right_drawer(fixed=False).style('background-color: #ebf1fa').props('bordered'):
+                ui.label('RIGHT DRAWER')
+            with ui.footer().style('background-color: #3874c8'):
+                ui.label('FOOTER')
+
+        ui.link('show page with fancy layout', page_layout)
+
     with example(ui.open):
         @ui.page('/yet_another_page')
         def yet_another_page():

+ 6 - 6
examples/customization/custom.py

@@ -10,15 +10,15 @@ class page(ui.page):
         super().__init__(route, classes='fit column items-start', title='Modularization Demo')
         self.kwargs = kwargs
 
-    async def header(self) -> None:
-        await super().header()
+    async def before_content(self) -> None:
+        await super().before_content()
         navbar(**self.kwargs)
-        # start using a ui row to let all content between header and footer be centered
+        # enter a ui.row to center all content
         self.content = ui.row().classes('justify-center fit mt-10').__enter__()
 
-    async def footer(self) -> None:
-        await super().footer()
-        # closing the row which was opened in header
+    async def after_content(self) -> None:
+        await super().after_content()
+        # close the row which was opened in before_content()
         self.content.__exit__(None, None, None)
 
 

+ 8 - 5
main.py

@@ -35,11 +35,14 @@ async def index():
     ui.add_head_html('<style>p a img {display: inline; vertical-align: baseline}</style>')
     ui.add_head_html('<meta name="viewport" content="width=device-width, initial-scale=1" />')
 
-    ui.html(
-        '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />'
-        '<style>.github-fork-ribbon:before { background-color: #999; }</style>'
-        '<a class="github-fork-ribbon" href="https://github.com/zauberzeug/nicegui" data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>'
-    )
+    ui.html('''
+        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />
+        <style>
+            .github-fork-ribbon { top: -16px; }
+            .github-fork-ribbon:before { background-color: #999; }
+        </style>
+        <a class="github-fork-ribbon" href="https://github.com/zauberzeug/nicegui" data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
+    ''')
 
     installation_start = README.find('<h2 class="text-4xl mb-3 mt-5">Installation</h2>')
     documentation_start = README.find('The API reference is hosted at')

+ 7 - 5
nicegui/page.py

@@ -59,7 +59,9 @@ class Page(jp.QuasarPage):
         self.on('result_ready', self.handle_javascript_result)
         self.on('page_ready', self.handle_page_ready)
 
-        self.view = jp.Div(a=self, classes=classes, temp=False)
+        self.layout = jp.QLayout(a=self, view='HHH LpR FFF', temp=False)
+        container = jp.QPageContainer(a=self.layout, temp=False)
+        self.view = jp.Div(a=container, classes=classes, temp=False)
         self.view.add_page(self)
 
     def set_favicon(self, favicon: Optional[str]) -> None:
@@ -213,7 +215,7 @@ class page:
                         if self.shared:
                             raise RuntimeError('Cannot use `request` argument in shared page')
                     await self.connected(request)
-                    await self.header()
+                    await self.before_content()
                     args = {**kwargs, **convert_arguments(request, self.converters, func)}
                     result = await func(**args) if is_coroutine(func) else func(**args)
                     if isinstance(result, types.GeneratorType):
@@ -225,7 +227,7 @@ class page:
                             raise RuntimeError('Yielding for page_ready is not supported on shared pages')
                         await result.__anext__()
                     self.page.page_ready_generator = result
-                    await self.footer()
+                    await self.after_content()
                 return self.page
             except Exception as e:
                 globals.log.exception(e)
@@ -239,10 +241,10 @@ class page:
     async def connected(self, request: Optional[Request]) -> None:
         pass
 
-    async def header(self) -> None:
+    async def before_content(self) -> None:
         pass
 
-    async def footer(self) -> None:
+    async def after_content(self) -> None:
         pass
 
 

+ 68 - 0
nicegui/page_layout.py

@@ -0,0 +1,68 @@
+import justpy as jp
+
+from .elements.group import Group
+from .page import find_parent_page
+
+
+class Header(Group):
+
+    def __init__(self, fixed: bool = True) -> None:
+        view = jp.QHeader(classes='q-pa-md row items-start gap-4', temp=False)
+        super().__init__(view)
+        code = list(find_parent_page().layout.view)
+        code[1] = 'H' if fixed else 'h'
+        find_parent_page().layout.view = ''.join(code)
+
+
+class Drawer(Group):
+
+    def __init__(self, side: str, *, fixed: bool = True, top_corner: bool = False, bottom_corner: bool = False) -> None:
+        assert side in ['left', 'right']
+        view = jp.QDrawer(side=side, content_class='q-pa-md', content_style='', temp=False)
+        super().__init__(view)
+        code = list(find_parent_page().layout.view)
+        code[0 if side == 'left' else 2] = side[0].lower() if top_corner else 'h'
+        code[4 if side == 'left' else 6] = side[0].upper() if fixed else side[0].lower()
+        code[8 if side == 'left' else 10] = side[0].lower() if bottom_corner else 'f'
+        find_parent_page().layout.view = ''.join(code)
+
+    def classes(self, add: str = '', *, replace: str = ''):
+        if replace:
+            self.view.content_class = replace
+        self.view.content_class += f' {add}'
+        return self
+
+    def style(self, add: str = '', *, replace: str = ''):
+        if replace:
+            self.view.content_style = replace
+        self.view.content_style += f';{add}'
+        return self
+
+
+class LeftDrawer(Drawer):
+
+    def __init__(self, fixed: bool = True, top_corner: bool = False, bottom_corner: bool = False) -> None:
+        super().__init__('left', fixed=fixed, top_corner=top_corner, bottom_corner=bottom_corner)
+
+
+class RightDrawer(Drawer):
+
+    def __init__(self, fixed: bool = True, top_corner: bool = False, bottom_corner: bool = False) -> None:
+        super().__init__('right', fixed=fixed, top_corner=top_corner, bottom_corner=bottom_corner)
+
+
+class Footer(Group):
+
+    def __init__(self, fixed: bool = True) -> None:
+        view = jp.QFooter(classes='q-pa-md row items-start gap-4', temp=False)
+        super().__init__(view)
+        code = list(find_parent_page().layout.view)
+        code[1] = 'F' if fixed else 'f'
+        find_parent_page().layout.view = ''.join(code)
+
+
+class PageSticky(Group):
+
+    def __init__(self) -> None:
+        view = jp.QPageSticky(temp=False)
+        super().__init__(view)

+ 5 - 0
nicegui/ui.py

@@ -6,6 +6,11 @@ class Ui:
     from .run import run  # NOTE: before justpy
 
     from .page import page, add_head_html, add_body_html, run_javascript
+    from .page_layout import Header as header
+    from .page_layout import Footer as footer
+    from .page_layout import LeftDrawer as left_drawer
+    from .page_layout import RightDrawer as right_drawer
+    from .page_layout import PageSticky as page_sticky
     from .update import update
 
     from .elements.button import Button as button

+ 1 - 1
tests/test_auto_context.py

@@ -39,7 +39,7 @@ def test_adding_elements_with_async_await(screen: Screen):
 
     screen.open('/')
     for _ in range(100):
-        if 'card\n  A\ncard\n  B' in screen.render_content():
+        if '    card\n      A\n    card\n      B' in screen.render_content():
             return
         screen.wait(0.1)
     raise AssertionError(f'{screen.render_content()} should show cards with "A" and "B"')

+ 7 - 7
tests/test_pages.py

@@ -148,15 +148,15 @@ def test_customized_page(screen: Screen):
             assert isinstance(request, Request)
             trace.append('connected')
 
-        async def header(self) -> None:
+        async def before_content(self) -> None:
             assert isinstance(self.page.view, justpy.htmlcomponents.Div), \
                 'we should be able to access the underlying JustPy view'
-            await super().header()
-            trace.append('header')
+            await super().before_content()
+            trace.append('before_content')
 
-        async def footer(self) -> None:
-            await super().footer()
-            trace.append('footer')
+        async def after_content(self) -> None:
+            await super().after_content()
+            trace.append('after_content')
 
     @custom_page('/', dark=True)
     def mainpage():
@@ -167,7 +167,7 @@ def test_customized_page(screen: Screen):
     screen.should_contain('Hello, world!')
     screen.should_contain('My Customized Page')
     assert 'body--dark' in screen.get_tags('body')[0].get_attribute('class')
-    assert trace == ['init', 'connected', 'header', 'content', 'footer']
+    assert trace == ['init', 'connected', 'before_content', 'content', 'after_content']
 
 
 def test_shared_page_with_request_parameter_raises_exception(screen: Screen):

+ 18 - 18
tests/test_screen.py

@@ -17,26 +17,26 @@ def test_rendering_page(screen: Screen):
     screen.open('/')
     assert screen.render_content() == '''Title: NiceGUI
 
-test label
-row
-  test input: some placeholder
-column
-  1
-  2
-  3
-  card
-    some text
+    test label
+    row
+      test input: some placeholder
+    column
+      1
+      2
+      3
+      card
+        some text
 '''
 
     assert screen.render_content(with_extras=True) == '''Title: NiceGUI
 
-test label
-row [class: items-start gap-4 positive]
-  test input: some placeholder [class: no-wrap items-start standard labeled]
-column [class: items-start gap-4]
-  1
-  2
-  3
-  card [class: items-start q-pa-md gap-4]
-    some text
+    test label
+    row [class: items-start gap-4 positive]
+      test input: some placeholder [class: no-wrap items-start standard labeled]
+    column [class: items-start gap-4]
+      1
+      2
+      3
+      card [class: items-start q-pa-md gap-4]
+        some text
 '''