flet-dev / flet

Flet enables developers to easily build realtime web, mobile and desktop apps in Python. No frontend experience required.
https://flet.dev
Apache License 2.0
11.38k stars 446 forks source link

Performance when visible is False #712

Open nottherealsanta opened 1 year ago

nottherealsanta commented 1 year ago

Two examples here, one with 100x100 container on stack ( only 20x20 are visible ) and the other with just 20x20.

https://user-images.githubusercontent.com/24403606/207773382-b7931b72-0745-43de-b1cd-dae32fa3f629.mov

https://user-images.githubusercontent.com/24403606/207773357-ee7101ca-8ba4-48b4-ab19-64b01e6053f5.mov

here is the code

for 100x100 ( only 20x20 visible )

from jupyflow import Cell, OutputArea
from flet import *
import random
import flet as ft

def main(page: ft.Page):
    page.title = "GridView Example"
    page.theme_mode = ft.ThemeMode.DARK
    # page.padding = 50
    page.update()

    container_grid = []
    for y in range(100):
        for x in range(100):
            container_grid.append(
                Container(
                    expand=False,
                    width=50,
                    height=50,
                    bgcolor=colors.LIGHT_BLUE_50,
                    border_radius=0,
                    margin=0,
                    padding=0,
                    top=y * (50 + 1),
                    left=x * (50 + 1),
                    clip_behavior=ClipBehavior.NONE,
                    visible=True if y < 20 and x < 20 else False,
                )
            )

    def on_pan_update(e: ft.DragUpdateEvent):
        stack_a.top = stack_a.top + e.delta_y
        stack_a.left = stack_a.left + e.delta_x
        stack_a.update()

    stack_a = Stack(
        controls=container_grid
        + [
            GestureDetector(
                on_pan_update=on_pan_update,
            )
        ],
        width=5000,
        height=5000,
        top=-50,
        left=-50,
    )
    stack_b = Stack(
        controls=[stack_a],
        width=2000,
        height=1500,
    )

    page.add(stack_b)

    page.update()

ft.app(target=main, view=ft.WEB_BROWSER)

for 20x20 visible

from jupyflow import Cell, OutputArea
from flet import *
import random
import flet as ft

def main(page: ft.Page):
    page.title = "GridView Example"
    page.theme_mode = ft.ThemeMode.DARK
    # page.padding = 50
    page.update()

    container_grid = []
    for y in range(20):
        for x in range(20):
            container_grid.append(
                Container(
                    expand=False,
                    width=50,
                    height=50,
                    bgcolor=colors.LIGHT_BLUE_50,
                    border_radius=0,
                    margin=0,
                    padding=0,
                    top=y * (50 + 1),
                    left=x * (50 + 1),
                    clip_behavior=ClipBehavior.NONE,
                    visible=True if y < 20 and x < 20 else False,
                )
            )

    def on_pan_update(e: ft.DragUpdateEvent):
        stack_a.top = stack_a.top + e.delta_y
        stack_a.left = stack_a.left + e.delta_x
        stack_a.update()

    stack_a = Stack(
        controls=container_grid
        + [
            GestureDetector(
                on_pan_update=on_pan_update,
            )
        ],
        width=5000,
        height=5000,
        top=-50,
        left=-50,
    )
    stack_b = Stack(
        controls=[stack_a],
        width=2000,
        height=1500,
    )

    page.add(stack_b)

    page.update()

ft.app(target=main, view=ft.WEB_BROWSER)
nottherealsanta commented 1 year ago

There may be more performant way to achieve the same effect. I hope this is true.

FeodorFitsner commented 1 year ago

It's 10,000 controls - my guess redux is getting slow... anyway, will take a look in the meantime - interesting case.

As a more performant way to do such kind of visualization I'd look at building SVG and then displaying it in Flet - its Image control supports SVGs.

FeodorFitsner commented 1 year ago

...and, yeah, it works faster when only 20x20 are visible because when visible=False the control is just not rendered at all.

nottherealsanta commented 1 year ago

it works faster when only 20x20 are visible because when visible=False the control is just not rendered at all

The laggy on is where 20x20 are visible but there are 100x100 controls. The smooth one only has 20x20 in total

FeodorFitsner commented 1 year ago

Ah, right, you're right. Then it looks more like redux (most probably my incorrect usage of redux :).

nottherealsanta commented 1 year ago

Thanks for your reply. 😄

I am working creating a 2d environment with drag-able containers ( which itself has other elements like buttons and textfield ). I would guess there would be few hundred of these containers. How would I go about doing this ?

nottherealsanta commented 1 year ago

What do you mean by redux ?

FeodorFitsner commented 1 year ago

It's a library Flet uses to maintain app state (controls tree).

nottherealsanta commented 1 year ago

Take a look at this https://user-images.githubusercontent.com/24403606/207780832-e21dbe2e-3fe5-4f4a-be38-f37b4e6592e6.mov When I drag the main stack, it lags quite a bit. Only few of those print hello world containers are set to visible.

FeodorFitsner commented 1 year ago

Drop me a code sample for that as well.

nottherealsanta commented 1 year ago
from flet import *
import flet as ft
from random import randint

class CellType:
    CODE = "code"
    MARKDOWN = "markdown"
    RAW = "raw"

class Cell(Container):
    def __init__(
        self,
        width: int,
        text: str,
        top: int = 0,
        left: int = 0,
        **kwargs,
    ):
        self.text = text
        self.focused = False
        self.side_bar_width = 40
        height = self._get_height()

        self.text_area = CellTextArea(
            text=self.text,
            cell_type=CellType.CODE,
            on_change=self.on_change,
            on_focus=self.on_focus,
            on_blur=self.on_blur,
            height=height - 20,
            width=width - self.side_bar_width,
        )

        self.side_bar = CellSideBar(
            height=height,
            width=self.side_bar_width,
            on_pan_update=self.on_pan_update,
            on_run_click=self.on_run_click,
        )

        self.stack = Stack(
            controls=[
                self.text_area,
                self.side_bar,
            ],
            height=height,
            width=width,
        )

        self.main_container = Container(
            content=self.stack,
            border_radius=10,
            bgcolor=colors.WHITE,
        )

        super().__init__(
            content=self.main_container,
            top=top,
            left=left,
            width=width,
            height=height,
            expand=True,
            margin=0,
            border_radius=10,
            clip_behavior=ft.ClipBehavior.HARD_EDGE,
            **kwargs,
        )

    def _get_height(
        self,
    ):
        return min(self.text.count("\n") * 25 + 60, 500)

    def _set_height(self):
        self.height = self._get_height()
        self.text_area.set_height(self.height - 20)
        self.side_bar.set_height(self.height)
        self.update()

    def on_pan_update(self, e: DragUpdateEvent):
        self.top = max(self.top + e.delta_y, 0)
        self.left = max(self.left + e.delta_x, 0)
        self.update()

    def on_run_click(self, e):
        print("Run Clicked")
        print(self.text_area.text)

    def on_change(self, e):
        self.text = e.control.value
        self._set_height()

    def on_focus(self, e):
        print("Focused")
        self.focused = True

    def on_blur(self, e):
        print("Blurred")
        self.focused = False

    def build(self):
        print("Building Cell")
        return self

class CellTextArea(Container):
    def __init__(
        self,
        text: str,
        height: int,
        width: int,
        cell_type: str,
        on_change,
        on_focus,
        on_blur,
        right: int = 0,
        top: int = 0,
    ):
        self.text = text
        self.cell_type = cell_type
        self.on_change = on_change
        self.on_focus = on_focus
        self.on_blur = on_blur

        self.text_field = TextField(
            value=self.text,
            on_change=self.on_change,
            on_focus=self.on_focus,
            on_blur=self.on_blur,
            border_width=0,
            multiline=True,
            border_radius=0,
            text_size=16,
            content_padding=10,
        )

        super().__init__(
            content=self.text_field,
            width=width,
            height=height,
            expand=True,
            margin=0,
            # border_radius=10,
            right=right,
            top=top,
            bgcolor=colors.BLUE_GREY_100,
        )

    def build(self):
        return self

    def set_height(self, height):
        self.height = height
        self.update()

class CellSideBar(Stack):
    def __init__(self, height: int, width: int, on_pan_update, on_run_click, **kwargs):
        self.on_pan_update = on_pan_update
        self.on_run_click = on_run_click
        self.left_border_color = colors.BLUE

        self.run_button = IconButton(
            icon=icons.PLAY_ARROW_SHARP,
            on_click=self.on_run_click,
        )
        self.container = Container(
            bgcolor=colors.WHITE,
            border=border.only(
                left=border.BorderSide(width=3, color=self.left_border_color),
                right=border.BorderSide(width=0, color=colors.TRANSPARENT),
                top=border.BorderSide(width=0, color=colors.TRANSPARENT),
                bottom=border.BorderSide(width=0, color=colors.TRANSPARENT),
            ),
            border_radius=10,
        )
        self.gesture_detector = GestureDetector(
            content=self.container,
            mouse_cursor=MouseCursor.GRAB,
            on_pan_update=self.on_pan_update,
            left=0,
            top=0,
            height=height,
            width=width,
        )
        super().__init__(
            controls=[
                self.gesture_detector,
                # self.run_button,
            ],
            height=height,
            width=width,
        )

    def set_height(self, height):
        self.height = height
        self.gesture_detector.height = height
        self.update()

    def build(self):
        return self

class OutputArea(Container):
    def __init__(self, input_cell: Cell, text: str, **kwargs):

        self.input_cell = input_cell

        self.text = text

        width = input_cell.width

        super().__init__(
            content=Text(value=self.text),
            width=width,
            height=1000,
            expand=True,
            margin=0,
            border_radius=10,
            bgcolor=colors.BLUE_GREY_100,
            top=input_cell.top + input_cell.height + 10,
            left=input_cell.left,
        )

class Canvas(UserControl):
    def __init__(self, width, height, **kwargs):
        super().__init__(**kwargs)

        self.C = []
        for i in range(500):
            self.C.append(
                Cell(
                    top=i * 100,
                    left=100,
                    width=700,
                    text='print("Hello World")',
                    visible=False if i > 100 else True,
                )
            )

        self.gd = GestureDetector(
            content=Stack(controls=self.C),
            on_pan_update=self.on_pan_update,
            mouse_cursor=MouseCursor.GRAB,
            top=0,
            left=0,
        )

        self.stack_b = Stack(
            controls=[self.gd],
            height=height,
            width=width,
        )

    def build(self):
        return self.stack_b

    def on_pan_update(self, event: DragUpdateEvent):
        self.gd.top = self.gd.top + event.delta_y
        self.gd.left = self.gd.left + event.delta_x
        self.update()

def main(page: Page):

    page.scroll = ScrollMode.ALWAYS
    # page.auto_scroll = True
    page.bgcolor = colors.BLACK12

    canvas = Canvas(width=page.width, height=page.height - 100)

    def page_resize(e):
        # print("New page size:", page.window_width, page.window_height)
        print("New page size:", page.width, page.height)
        canvas.width = page.width
        canvas.height = page.height - 100

    page.on_resize = page_resize

    page.add(canvas)
    View

app(target=main)

Excuse the import * I am just prototyping here.