responds-forms 0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ from pyflutter_ui.ui import (
2
+ BaseMaterialApp, BuildContext, ChangeNotifier, CommandNotifier,
3
+ Consumer, Element, FlutterWidget, ConsumerState, LayoutData,
4
+ Navigator, Provider, StatefulWidget, StatelessWidget, State,
5
+ ValueNotifier, ValueListenableBuilder, Provider, AlignElement,
6
+ Align, AppBar, BottomNavigationBar, Center, Card, Checkbox,
7
+ CircularProgress, Container, Divider, Drawer, Column, Row,
8
+ DropdownButton, ElevatedButton, Flexible, Expanded,
9
+ FloatingActionButton, Icon, IconButton, LinearProgress, Provider,
10
+ Padding, Image, ListBuilder, MainAxisAlignment, CrossAxisAlignment,
11
+ TableView, TkMaterialApp, Scaffold, Text, SizedBox, Alignment,
12
+ ScrollView,
13
+ )
14
+
15
+ from .theme import (
16
+ ColorScheme, CardTheme, InputDecorationTheme, TextStyle,
17
+ Theme, ThemeData, ThemeMode, ThemeProvider, TextTheme,
18
+ make_themed_app,
19
+ )
20
+
21
+ from .file_picker import (
22
+ FilePickerController, FilePickerResult, FilePickerType, PlatformFile,
23
+ )
24
+
25
+ from pyflutter_ui.ui.list_builder import (
26
+ ScrollableColumn, ScrollableColumnElement, ScrollableColumnOrProgress
27
+ )
28
+
29
+ from .navigator import NavigatorApp, PageRoute, Route
30
+ from .app import run_app, SnackBar, SnackBarType
pyflutter_ui/app.py ADDED
@@ -0,0 +1,289 @@
1
+ # app.py
2
+ from __future__ import annotations
3
+ from enum import Enum, auto
4
+ from tkinter import Tk
5
+ import tkinter as tk
6
+ from typing import Callable, Optional
7
+ from pyflutter_ui.ui import BuildContext, TkMaterialApp, Expanded
8
+ from pyflutter_ui.theme import ThemeData, ThemeMode, ThemeProvider,ColorScheme
9
+
10
+
11
+ # ============================================================
12
+ # SNACKBAR
13
+ # ============================================================
14
+
15
+ class SnackBarType(Enum):
16
+ INFO = auto()
17
+ SUCCESS = auto()
18
+ WARNING = auto()
19
+ ERROR = auto()
20
+
21
+
22
+ class SnackBar:
23
+ """
24
+ Exibe uma mensagem temporária na parte inferior da janela,
25
+ similar ao SnackBar do Flutter.
26
+
27
+ Uso via contexto (recomendado):
28
+ SnackBar.of(context).show("Arquivo salvo!", type=SnackBarType.SUCCESS)
29
+
30
+ Uso direto com root Tk:
31
+ SnackBar.show_on(root, "Erro ao carregar", type=SnackBarType.ERROR)
32
+ """
33
+
34
+ # cores por tipo — funcionam tanto em tema claro quanto escuro
35
+ _COLORS = {
36
+ SnackBarType.INFO: {"bg": "#323232", "fg": "#FFFFFF", "accent": "#90CAF9"},
37
+ SnackBarType.SUCCESS: {"bg": "#1B5E20", "fg": "#FFFFFF", "accent": "#A5D6A7"},
38
+ SnackBarType.WARNING: {"bg": "#E65100", "fg": "#FFFFFF", "accent": "#FFE082"},
39
+ SnackBarType.ERROR: {"bg": "#B71C1C", "fg": "#FFFFFF", "accent": "#EF9A9A"},
40
+ }
41
+
42
+ _ICONS = {
43
+ SnackBarType.INFO: "ℹ",
44
+ SnackBarType.SUCCESS: "✓",
45
+ SnackBarType.WARNING: "⚠",
46
+ SnackBarType.ERROR: "✕",
47
+ }
48
+
49
+ def __init__(self, root: tk.Tk) -> None:
50
+ self._root = root
51
+ self._current: Optional[tk.Toplevel] = None
52
+ self._after_id: Optional[str] = None
53
+
54
+ @classmethod
55
+ def of(cls, context: BuildContext) -> SnackBar:
56
+ """Obtém o SnackBar a partir de um BuildContext."""
57
+ root = context.app.root
58
+ # reutiliza instância já anexada ao root
59
+ if not hasattr(root, "_snackbar"):
60
+ root._snackbar = cls(root)
61
+ return root._snackbar
62
+
63
+ @classmethod
64
+ def show_on(
65
+ cls,
66
+ root: tk.Tk,
67
+ message: str,
68
+ *,
69
+ type: SnackBarType = SnackBarType.INFO,
70
+ duration: int = 3000,
71
+ action_label: Optional[str] = None,
72
+ on_action: Optional[Callable[[], None]] = None,
73
+ ) -> None:
74
+ """Atalho estático: não precisa instanciar."""
75
+ if not hasattr(root, "_snackbar"):
76
+ root._snackbar = cls(root)
77
+ root._snackbar.show(
78
+ message,
79
+ type=type,
80
+ duration=duration,
81
+ action_label=action_label,
82
+ on_action=on_action,
83
+ )
84
+
85
+ def show(
86
+ self,
87
+ message: str,
88
+ *,
89
+ type: SnackBarType = SnackBarType.INFO,
90
+ duration: int = 3000,
91
+ action_label: Optional[str] = None,
92
+ on_action: Optional[Callable[[], None]] = None,
93
+ ) -> None:
94
+ """Exibe o SnackBar. Descarta o anterior se ainda estiver visível."""
95
+ self._dismiss()
96
+
97
+ colors = self._COLORS[type]
98
+ icon = self._ICONS[type]
99
+
100
+ # ── janela sem borda flutuando sobre o root ──────────
101
+ popup = tk.Toplevel(self._root)
102
+ popup.title("Aviso")
103
+ popup.geometry('400x50')
104
+ popup.overrideredirect(True) # sem barra de título
105
+ popup.attributes("-topmost", True)
106
+ popup.configure(bg=colors["bg"])
107
+
108
+ self._current = popup
109
+
110
+ # ── frame interno com padding ─────────────────────────
111
+ frame = tk.Frame(popup, bg=colors["bg"], padx=12, pady=10)
112
+ frame.pack(fill="both", expand=True)
113
+
114
+ # ícone
115
+ tk.Label(
116
+ frame,
117
+ text=icon,
118
+ bg=colors["bg"],
119
+ fg=colors["accent"],
120
+ font=("Helvetica", 14, "bold"),
121
+ ).pack(side="left", padx=(0, 8))
122
+
123
+ # mensagem
124
+ tk.Label(
125
+ frame,
126
+ text=message,
127
+ bg=colors["bg"],
128
+ fg=colors["fg"],
129
+ font=("Helvetica", 10),
130
+ wraplength=360,
131
+ justify="left",
132
+ anchor="w",
133
+ ).pack(side="left", fill="x", expand=True)
134
+
135
+ # botão de ação opcional (ex.: "DESFAZER")
136
+ if action_label and on_action:
137
+ def _action():
138
+ on_action()
139
+ self._dismiss()
140
+
141
+ tk.Button(
142
+ frame,
143
+ text=action_label.upper(),
144
+ bg=colors["bg"],
145
+ fg=colors["accent"],
146
+ font=("Helvetica", 9, "bold"),
147
+ relief="flat",
148
+ cursor="hand2",
149
+ bd=0,
150
+ highlightthickness=0,
151
+ command=_action,
152
+ ).pack(side="right", padx=(12, 0))
153
+
154
+ # botão X para fechar manualmente
155
+ tk.Button(
156
+ frame,
157
+ text="✕",
158
+ bg=colors["bg"],
159
+ fg=colors["fg"],
160
+ font=("Helvetica", 8),
161
+ relief="flat",
162
+ cursor="hand2",
163
+ bd=0,
164
+ highlightthickness=0,
165
+ command=self._dismiss,
166
+ ).pack(side="right", padx=(8, 0))
167
+
168
+ # ── posicionamento: canto inferior esquerdo do root ──
169
+ popup.update_idletasks()
170
+ self._reposition(popup)
171
+
172
+ # reposiciona se o root for movido ou redimensionado
173
+ self._root.bind("<Configure>", lambda e: self._reposition(popup), add="+")
174
+
175
+ # ── animação de entrada (slide up) ───────────────────
176
+ self._animate_in(popup)
177
+
178
+ # ── auto-dismiss ──────────────────────────────────────
179
+ if duration > 0:
180
+ self._after_id = self._root.after(duration, self._dismiss)
181
+
182
+ def _reposition(self, popup: tk.Toplevel) -> None:
183
+ if popup is None or not popup.winfo_exists():
184
+ return
185
+
186
+ root = self._root
187
+ rx = root.winfo_x()
188
+ ry = root.winfo_y()
189
+ rw = root.winfo_width()
190
+ rh = root.winfo_height()
191
+
192
+ pw = popup.winfo_width()
193
+ ph = popup.winfo_height()
194
+
195
+ margin = 16
196
+ x = rx + margin
197
+ y = ry + rh - ph - margin
198
+
199
+ popup.geometry(f"+{x}+{y}")
200
+
201
+ def _animate_in(self, popup: tk.Toplevel, steps: int = 8) -> None:
202
+ """Desliza o popup de baixo para cima em `steps` passos."""
203
+ if not popup.winfo_exists():
204
+ return
205
+
206
+ root = self._root
207
+ rh = root.winfo_height()
208
+ ry = root.winfo_y()
209
+ ph = popup.winfo_reqheight()
210
+ margin = 16
211
+
212
+ target_y = ry + rh - ph - margin
213
+ start_y = ry + rh # começa fora da janela
214
+
215
+ def _step(current_y: int, step: int) -> None:
216
+ if not popup.winfo_exists():
217
+ return
218
+ if step >= steps:
219
+ popup.geometry(f"+{popup.winfo_x()}+{target_y}")
220
+ return
221
+ new_y = current_y + (target_y - current_y) // 2
222
+ popup.geometry(f"+{popup.winfo_x()}+{new_y}")
223
+ popup.after(16, lambda: _step(new_y, step + 1))
224
+
225
+ popup.geometry(f"+{popup.winfo_x()}+{start_y}")
226
+ _step(start_y, 0)
227
+
228
+ def _dismiss(self) -> None:
229
+ if self._after_id:
230
+ self._root.after_cancel(self._after_id)
231
+ self._after_id = None
232
+
233
+ if self._current and self._current.winfo_exists():
234
+ self._animate_out(self._current)
235
+ self._current = None
236
+
237
+ def _animate_out(self, popup: tk.Toplevel, steps: int = 6) -> None:
238
+ """Desliza o popup para baixo antes de destruir."""
239
+ if not popup.winfo_exists():
240
+ return
241
+
242
+ root = self._root
243
+ rh = root.winfo_height()
244
+ ry = root.winfo_y()
245
+ target = ry + rh + 10 # fora da janela
246
+
247
+ def _step(current_y: int, step: int) -> None:
248
+ if not popup.winfo_exists():
249
+ return
250
+ if step >= steps:
251
+ popup.destroy()
252
+ return
253
+ new_y = current_y + (target - current_y) // 2 + 2
254
+ popup.geometry(f"+{popup.winfo_x()}+{new_y}")
255
+ popup.after(16, lambda: _step(new_y, step + 1))
256
+
257
+ _step(popup.winfo_y(), 0)
258
+
259
+
260
+ def create_theme_provider(mode: ThemeMode = ThemeMode.DARK) -> ThemeProvider:
261
+
262
+ return ThemeProvider(
263
+ light_theme=ThemeData.light(color_scheme=ColorScheme.light()),
264
+ dark_theme=ThemeData.dark(color_scheme=ColorScheme.dark()),
265
+ mode=ThemeMode.DARK,
266
+ )
267
+
268
+
269
+ def include_theme_app(app: TkMaterialApp) -> TkMaterialApp:
270
+
271
+ if app.root is None:
272
+ app.root = Tk()
273
+
274
+ try:
275
+ notifier: ThemeProvider = app.read_state(ThemeProvider)
276
+ if notifier is not None:
277
+ notifier.attach_root(app.root)
278
+ except Exception as e:
279
+ print(e)
280
+
281
+ app.get_navigator().push(app.home)
282
+ return app
283
+
284
+
285
+ def run_app(app: TkMaterialApp):
286
+ theme = create_theme_provider()
287
+ app.register_notifier(theme)
288
+
289
+ include_theme_app(app).run()
@@ -0,0 +1,274 @@
1
+ # file_picker.py
2
+ # Implementação de FilePicker para PyFlutter no padrão do Flutter real.
3
+ #
4
+ # No Flutter, FilePicker não é um widget — é um controller/serviço que
5
+ # expõe um método pickFiles() e notifica os listeners com o resultado.
6
+ # Este arquivo replica essa arquitetura usando os primitivos do PyFlutter.
7
+ #
8
+ # ─────────────────────────────────────────────────────────────
9
+ # USO BÁSICO
10
+ # ─────────────────────────────────────────────────────────────
11
+ #
12
+ # picker = FilePickerController(
13
+ # type=FilePickerType.image, # filtro de tipo
14
+ # allow_multiple=True, # seleção múltipla
15
+ # )
16
+ #
17
+ # # Abrir o diálogo e aguardar o resultado:
18
+ # result: FilePickerResult | None = picker.pick_files()
19
+ # if result:
20
+ # for f in result.files:
21
+ # print(f.name, f.path, f.size)
22
+ #
23
+ # # Ou de forma reativa (integrado ao build do widget):
24
+ # ValueListenableBuilder(
25
+ # notifier=picker.result_notifier,
26
+ # builder=lambda ctx, result: Text(
27
+ # result.files[0].name if result else "Nenhum arquivo"
28
+ # ),
29
+ # )
30
+ # ─────────────────────────────────────────────────────────────
31
+
32
+ from __future__ import annotations
33
+ import os
34
+ from dataclasses import dataclass, field
35
+ from enum import Enum
36
+ from tkinter import filedialog
37
+ from typing import Optional
38
+ from pyflutter_ui.ui.core import ValueNotifier
39
+
40
+
41
+ # ============================================================
42
+ # TIPOS DE ARQUIVO — espelha o FileType do flutter_file_picker
43
+ # ============================================================
44
+
45
+ class FilePickerType(Enum):
46
+ """Filtro de tipo de arquivo para o diálogo."""
47
+ any = "any" # sem filtro
48
+ image = "image" # png, jpg, jpeg, gif, bmp, webp, svg
49
+ pdf = "pdf" # pdf
50
+ spreadsheet = "spreadsheet" # xlsx, xls, csv, ods
51
+ video = "video" # mp4, mov, avi, mkv, webm
52
+ audio = "audio" # mp3, wav, ogg, flac, aac
53
+ custom = "custom" # usa allowed_extensions fornecido pelo dev
54
+
55
+
56
+ # Mapeamento interno: tipo → lista de extensões tk-compatíveis
57
+ _TYPE_FILTERS: dict[FilePickerType, list[tuple[str, str]]] = {
58
+ FilePickerType.any: [("Todos os arquivos", "*.*")],
59
+ FilePickerType.image: [
60
+ ("Imagens", "*.png *.jpg *.jpeg *.gif *.bmp *.webp *.svg"),
61
+ ("Todos os arquivos", "*.*"),
62
+ ],
63
+ FilePickerType.pdf: [
64
+ ("PDF", "*.pdf"),
65
+ ("Todos os arquivos", "*.*"),
66
+ ],
67
+ FilePickerType.spreadsheet: [
68
+ ("Planilhas", "*.xlsx *.xls *.csv *.ods"),
69
+ ("Todos os arquivos", "*.*"),
70
+ ],
71
+ FilePickerType.video: [
72
+ ("Vídeos", "*.mp4 *.mov *.avi *.mkv *.webm"),
73
+ ("Todos os arquivos", "*.*"),
74
+ ],
75
+ FilePickerType.audio: [
76
+ ("Áudios", "*.mp3 *.wav *.ogg *.flac *.aac"),
77
+ ("Todos os arquivos", "*.*"),
78
+ ],
79
+ }
80
+
81
+
82
+ # ============================================================
83
+ # MODELO DE RESULTADO — espelha PlatformFile do Flutter
84
+ # ============================================================
85
+
86
+ @dataclass(frozen=True)
87
+ class PlatformFile:
88
+ """Representa um arquivo selecionado pelo usuário."""
89
+ path: str # caminho absoluto no disco
90
+ name: str = field(init=False) # nome do arquivo (sem diretório)
91
+ extension: str = field(init=False) # extensão sem ponto, ex: "pdf"
92
+ size: int = field(init=False) # tamanho em bytes (0 se inacessível)
93
+
94
+ def __post_init__(self) -> None:
95
+ # dataclass frozen=True exige object.__setattr__ para init tardio
96
+ object.__setattr__(self, "name", os.path.basename(self.path))
97
+ ext = os.path.splitext(self.path)[1].lstrip(".")
98
+ object.__setattr__(self, "extension", ext.lower())
99
+ try:
100
+ object.__setattr__(self, "size", os.path.getsize(self.path))
101
+ except OSError:
102
+ object.__setattr__(self, "size", 0)
103
+
104
+ @property
105
+ def size_readable(self) -> str:
106
+ """Tamanho formatado (bytes / KB / MB / GB)."""
107
+ n = self.size
108
+ for unit in ("B", "KB", "MB", "GB"):
109
+ if n < 1024:
110
+ return f"{n:.1f} {unit}" if unit != "B" else f"{n} {unit}"
111
+ n /= 1024
112
+ return f"{n:.1f} TB"
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class FilePickerResult:
117
+ """Resultado de uma seleção de arquivos. Equivale a FilePickerResult do Flutter."""
118
+ files: list[PlatformFile]
119
+
120
+ @property
121
+ def paths(self) -> list[str]:
122
+ """Atalho: lista de caminhos absolutos."""
123
+ return [f.path for f in self.files]
124
+
125
+ @property
126
+ def names(self) -> list[str]:
127
+ """Atalho: lista de nomes de arquivo."""
128
+ return [f.name for f in self.files]
129
+
130
+ @property
131
+ def is_single(self) -> bool:
132
+ return len(self.files) == 1
133
+
134
+ def single(self) -> PlatformFile | None:
135
+ """Retorna o único arquivo ou None se houver 0 ou mais de 1."""
136
+ return self.files[0] if self.is_single else None
137
+
138
+
139
+ # ============================================================
140
+ # CONTROLLER — o equivalente ao FilePicker.platform do Flutter
141
+ # ============================================================
142
+
143
+ class FilePickerController:
144
+ """
145
+ Controller que encapsula a lógica de seleção de arquivos.
146
+
147
+ Não é um widget — vive no State do seu widget pai, exatamente
148
+ como FilePickerController no Flutter.
149
+
150
+ Parâmetros
151
+ ----------
152
+ type : FilePickerType
153
+ Filtro de tipo de arquivo. Use FilePickerType.custom junto com
154
+ allowed_extensions para filtros personalizados.
155
+ allow_multiple : bool
156
+ Se True, permite selecionar vários arquivos de uma vez.
157
+ allowed_extensions : list[str] | None
158
+ Usado apenas quando type=FilePickerType.custom.
159
+ Ex.: ["json", "toml", "yaml"]
160
+ dialog_title : str
161
+ Título da janela de diálogo do SO.
162
+ initial_directory : str | None
163
+ Diretório inicial do diálogo. Padrão: pasta home do usuário.
164
+ """
165
+
166
+ def __init__(
167
+ self,
168
+ type: FilePickerType = FilePickerType.any,
169
+ allow_multiple: bool = False,
170
+ allowed_extensions: Optional[list[str]] = None,
171
+ dialog_title: str = "Selecionar arquivo",
172
+ initial_directory: Optional[str] = None,
173
+ ) -> None:
174
+
175
+ self.type = type
176
+ self.allow_multiple = allow_multiple
177
+ self.allowed_extensions = allowed_extensions
178
+ self.dialog_title = dialog_title
179
+ self.initial_directory = initial_directory #or os.path.expanduser("~")
180
+
181
+ if self.initial_directory is None:
182
+ self.initial_directory = os.path.join(f"{os.path.expanduser("~")}", "Downloads")
183
+
184
+ # Expõe o resultado de forma reativa — use em ValueListenableBuilder
185
+ self.result_notifier: ValueNotifier[FilePickerResult | None] = ValueNotifier(None)
186
+
187
+ # ── API pública ───────────────────────────────────────────
188
+
189
+ def pick_files(self) -> FilePickerResult | None:
190
+ """
191
+ Abre o diálogo de seleção de arquivos do SO.
192
+
193
+ Retorna FilePickerResult se o usuário confirmou, None se cancelou.
194
+ Também atualiza result_notifier para notificar widgets ouvintes.
195
+ """
196
+ filters = self._resolve_filters()
197
+
198
+ if self.allow_multiple:
199
+ paths = filedialog.askopenfilenames(
200
+ title=self.dialog_title,
201
+ filetypes=filters,
202
+ initialdir=self.initial_directory,
203
+ )
204
+ raw = list(paths) if paths else []
205
+ else:
206
+ path = filedialog.askopenfilename(
207
+ title=self.dialog_title,
208
+ filetypes=filters,
209
+ initialdir=self.initial_directory,
210
+ )
211
+ raw = [path] if path else []
212
+
213
+ result = FilePickerResult(files=[PlatformFile(p) for p in raw]) if raw else None
214
+ if raw is not None:
215
+ first = raw[0] if len(raw) > 0 else None
216
+ if first is not None:
217
+ self.initial_directory = os.path.dirname(first)
218
+ print(f'{__class__.__name__} [INITIAL_DIR] ... {self.initial_directory}')
219
+
220
+ self.result_notifier.value = result
221
+ return result
222
+
223
+ def pick_directory(self) -> str | None:
224
+ """
225
+ Abre o diálogo para selecionar uma pasta.
226
+
227
+ Retorna o caminho absoluto ou None se cancelado.
228
+ """
229
+ path = filedialog.askdirectory(
230
+ title=self.dialog_title,
231
+ initialdir=self.initial_directory,
232
+ mustexist=True,
233
+ )
234
+ return path or None
235
+
236
+ def save_file(
237
+ self,
238
+ suggested_name: str = "",
239
+ ) -> str | None:
240
+ """
241
+ Abre o diálogo "Salvar como".
242
+
243
+ Retorna o caminho escolhido ou None se cancelado.
244
+ """
245
+ filters = self._resolve_filters()
246
+ default_ext = (
247
+ "." + self.allowed_extensions[0]
248
+ if self.allowed_extensions
249
+ else ""
250
+ )
251
+ path = filedialog.asksaveasfilename(
252
+ title=self.dialog_title,
253
+ filetypes=filters,
254
+ initialdir=self.initial_directory,
255
+ initialfile=suggested_name,
256
+ defaultextension=default_ext,
257
+ )
258
+ return path or None
259
+
260
+ def clear_result(self) -> None:
261
+ """Limpa o resultado atual e notifica os ouvintes."""
262
+ self.result_notifier.value = None
263
+
264
+ # ── privado ───────────────────────────────────────────────
265
+
266
+ def _resolve_filters(self) -> list[tuple[str, str]]:
267
+ if self.type == FilePickerType.custom:
268
+ if not self.allowed_extensions:
269
+ return [("Todos os arquivos", "*.*")]
270
+ pattern = " ".join(f"*.{ext}" for ext in self.allowed_extensions)
271
+ label = ", ".join(self.allowed_extensions).upper()
272
+ return [(label, pattern), ("Todos os arquivos", "*.*")]
273
+ return _TYPE_FILTERS.get(self.type, [("Todos os arquivos", "*.*")])
274
+
@@ -0,0 +1,93 @@
1
+ # navigator.py
2
+ from __future__ import annotations
3
+ from abc import ABC, abstractmethod
4
+ from typing import Callable
5
+ from pyflutter_ui.ui.core import (
6
+ BuildContext, Element, FlutterWidget, BaseMaterialApp, Navigator
7
+ )
8
+
9
+
10
+ # ============================================================
11
+ # NAVIGATOR
12
+ # ============================================================
13
+ class Route(ABC):
14
+
15
+ def __init__(self) -> None:
16
+ super().__init__()
17
+
18
+ @abstractmethod
19
+ def build(self, context: BuildContext) -> FlutterWidget:
20
+ pass
21
+
22
+
23
+ class PageRoute(Route):
24
+
25
+ def __init__(self, builder: Callable[
26
+ [BuildContext],
27
+ FlutterWidget
28
+ ]) -> None:
29
+ super().__init__()
30
+ self.builder = builder
31
+
32
+ def build(self, context: BuildContext) -> FlutterWidget:
33
+ return self.builder(context)
34
+
35
+
36
+ class NavigatorApp(Navigator):
37
+
38
+ def __init__(self, app: BaseMaterialApp) -> None:
39
+ super().__init__(app)
40
+ self.stack: list[Element] = []
41
+
42
+ def push(self, page: FlutterWidget) -> None:
43
+
44
+ if self.stack:
45
+ current = self.stack[-1]
46
+
47
+ if current.widget_tk is not None:
48
+ current.widget_tk.pack_forget()
49
+
50
+ element = page.create_element()
51
+ context = BuildContext(self.app, parent_widget=self.app.root)
52
+ element.mount(context)
53
+ self.stack.append(element)
54
+ self.current_element = element
55
+
56
+ def pop(self) -> None:
57
+
58
+ if len(self.stack) <= 1:
59
+ return
60
+
61
+ current = self.stack.pop()
62
+
63
+ current.unmount()
64
+
65
+ previous = self.stack[-1]
66
+
67
+ if previous.widget_tk is not None:
68
+ previous.widget_tk.pack(fill="both", expand=True)
69
+
70
+ self.current_element = previous
71
+
72
+ def replace(self, page: FlutterWidget) -> None:
73
+
74
+ if self.stack:
75
+ current = self.stack.pop()
76
+
77
+ current.unmount()
78
+
79
+ element = page.create_element()
80
+
81
+ context = BuildContext(
82
+ app=self.app,
83
+ parent_widget=self.app.root
84
+ )
85
+
86
+ element.mount(context)
87
+
88
+ self.stack.append(element)
89
+
90
+ self.current_element = element
91
+
92
+ def render(self) -> None:
93
+ pass