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.
pyflutter_ui/theme.py ADDED
@@ -0,0 +1,1058 @@
1
+ # theme.py
2
+ """
3
+ Sistema de temas para pyflutter — inspirado na ThemeData do Flutter.
4
+
5
+ Funcionalidades:
6
+ - ThemeMode: light | dark | system (detecta o SO automaticamente)
7
+ - ColorScheme: paleta completa com primary, secondary, surface, error, etc.
8
+ - TextTheme: estilos tipográficos (displayLarge → labelSmall)
9
+ - ButtonThemeData: estilo para ElevatedButton, OutlinedButton, TextButton
10
+ - ThemeData: objeto raiz que agrega tudo e aplica no ttk.Style
11
+ - ThemeProvider(ChangeNotifier): permite trocar o tema em runtime
12
+
13
+ Uso básico:
14
+ from pyflutter.theme import ThemeData, ThemeMode, ThemeProvider
15
+
16
+ provider = ThemeProvider(ThemeData.light())
17
+ app.register_provider(provider)
18
+
19
+ # Para mudar o tema em runtime:
20
+ provider.set_mode(ThemeMode.DARK)
21
+ """
22
+ from __future__ import annotations
23
+ import sys
24
+ import tkinter as tk
25
+ from dataclasses import dataclass, field
26
+ from enum import Enum
27
+ from tkinter import ttk
28
+ from typing import Optional, TypeVar
29
+ from pyflutter_ui.ui.core import ChangeNotifier, BaseMaterialApp, Navigator
30
+
31
+
32
+ T = TypeVar('T')
33
+
34
+ # ============================================================
35
+ # THEME MODE
36
+ # ============================================================
37
+
38
+ class ThemeMode(Enum):
39
+ """
40
+ Equivalente ao ThemeMode do Flutter.
41
+ SYSTEM detecta a preferência do sistema operacional.
42
+ """
43
+ LIGHT = "light"
44
+ DARK = "dark"
45
+ SYSTEM = "system"
46
+
47
+
48
+ def _detect_system_dark_mode() -> bool:
49
+ """
50
+ Tenta detectar se o SO está em modo escuro.
51
+ Suporta Windows, macOS e Linux (via tk).
52
+ Retorna True se dark mode estiver ativo.
53
+ """
54
+ try:
55
+ if sys.platform == "darwin":
56
+ # macOS: consulta via AppleScript
57
+ import subprocess
58
+ result = subprocess.run(
59
+ ["defaults", "read", "-g", "AppleInterfaceStyle"],
60
+ capture_output=True, text=True
61
+ )
62
+ return result.stdout.strip().lower() == "dark"
63
+
64
+ elif sys.platform == "win32":
65
+ # Windows: consulta o registro
66
+ import winreg
67
+ key = winreg.OpenKey(
68
+ winreg.HKEY_CURRENT_USER,
69
+ r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
70
+ )
71
+ value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
72
+ return value == 0 # 0 = dark, 1 = light
73
+
74
+ else:
75
+ # Linux: heurística via tema GTK (fallback: light)
76
+ root = tk.Tk()
77
+ root.withdraw()
78
+ style = ttk.Style(root)
79
+ bg = style.lookup("TFrame", "background") or "#ffffff"
80
+ root.destroy()
81
+ # Se o background for escuro, assume dark mode
82
+ r, g, b = int(bg[1:3], 16), int(bg[3:5], 16), int(bg[5:7], 16)
83
+ luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
84
+ return luminance < 0.5
85
+
86
+ except Exception:
87
+ return False # fallback seguro: light mode
88
+
89
+
90
+ # ============================================================
91
+ # COLOR SCHEME
92
+ # ============================================================
93
+
94
+ @dataclass
95
+ class ColorScheme:
96
+ """
97
+ Paleta de cores completa, espelhando o ColorScheme do Flutter (Material 3).
98
+
99
+ Convenção:
100
+ - `primary` : cor principal da marca
101
+ - `on_primary` : texto/ícone sobre primary
102
+ - `primary_container` : variante de fundo para primary
103
+ - `on_primary_container` : texto sobre primary_container
104
+ - `secondary` : cor de destaque secundária
105
+ - `on_secondary` : texto sobre secondary
106
+ - `secondary_container`
107
+ - `on_secondary_container`
108
+ - `tertiary` / `on_tertiary` / `tertiary_container` / `on_tertiary_container`
109
+ - `error` / `on_error` / `error_container` / `on_error_container`
110
+ - `surface` : fundo dos cards/dialogs
111
+ - `on_surface` : texto sobre surface
112
+ - `surface_variant` : variante de surface
113
+ - `on_surface_variant`
114
+ - `background` : cor de fundo da janela
115
+ - `on_background` : texto sobre background
116
+ - `outline` : bordas e divisores
117
+ - `shadow` : cor de sombra
118
+ - `inverse_surface` / `on_inverse_surface` / `inverse_primary`
119
+ - `scrim` : overlay semi-transparente (Drawer, Modal)
120
+ """
121
+
122
+ # ── Primary ──────────────────────────────────────────────
123
+ primary: str = "#6750A4"
124
+ on_primary: str = "#FFFFFF"
125
+ primary_container: str = "#EADDFF"
126
+ on_primary_container: str = "#21005D"
127
+
128
+ # ── Secondary ────────────────────────────────────────────
129
+ secondary: str = "#625B71"
130
+ on_secondary: str = "#FFFFFF"
131
+ secondary_container: str = "#E8DEF8"
132
+ on_secondary_container: str = "#1D192B"
133
+
134
+ # ── Tertiary ─────────────────────────────────────────────
135
+ tertiary: str = "#7D5260"
136
+ on_tertiary: str = "#FFFFFF"
137
+ tertiary_container: str = "#FFD8E4"
138
+ on_tertiary_container: str = "#31111D"
139
+
140
+ # ── Error ────────────────────────────────────────────────
141
+ error: str = "#B3261E"
142
+ on_error: str = "#FFFFFF"
143
+ error_container: str = "#F9DEDC"
144
+ on_error_container: str = "#410E0B"
145
+
146
+ # ── Surface / Background ─────────────────────────────────
147
+ surface: str = "#FFFBFE"
148
+ on_surface: str = "#1C1B1F"
149
+ surface_variant: str = "#E7E0EC"
150
+ on_surface_variant: str = "#49454F"
151
+
152
+ background: str = "#FFFBFE"
153
+ on_background: str = "#1C1B1F"
154
+
155
+ # ── Utilities ────────────────────────────────────────────
156
+ outline: str = "#79747E"
157
+ outline_variant: str = "#CAC4D0"
158
+ shadow: str = "#000000"
159
+ scrim: str = "#000000"
160
+
161
+ # ── Inverse ──────────────────────────────────────────────
162
+ inverse_surface: str = "#313033"
163
+ on_inverse_surface: str = "#F4EFF4"
164
+ inverse_primary: str = "#D0BCFF"
165
+
166
+ @classmethod
167
+ def light(cls) -> "ColorScheme":
168
+ """Paleta Material 3 Light padrão."""
169
+ return cls()
170
+
171
+ @classmethod
172
+ def dark(cls) -> "ColorScheme":
173
+ """Paleta Material 3 Dark padrão."""
174
+ return cls(
175
+ primary="#D0BCFF",
176
+ on_primary="#381E72",
177
+ primary_container="#4F378B",
178
+ on_primary_container="#EADDFF",
179
+
180
+ secondary="#CCC2DC",
181
+ on_secondary="#332D41",
182
+ secondary_container="#4A4458",
183
+ on_secondary_container="#E8DEF8",
184
+
185
+ tertiary="#EFB8C8",
186
+ on_tertiary="#492532",
187
+ tertiary_container="#633B48",
188
+ on_tertiary_container="#FFD8E4",
189
+
190
+ error="#F2B8B5",
191
+ on_error="#601410",
192
+ error_container="#8C1D18",
193
+ on_error_container="#F9DEDC",
194
+
195
+ surface="#1C1B1F",
196
+ on_surface="#E6E1E5",
197
+ surface_variant="#49454F",
198
+ on_surface_variant="#CAC4D0",
199
+
200
+ background="#1C1B1F",
201
+ on_background="#E6E1E5",
202
+
203
+ outline="#938F99",
204
+ outline_variant="#49454F",
205
+ shadow="#000000",
206
+ scrim="#000000",
207
+
208
+ inverse_surface="#E6E1E5",
209
+ on_inverse_surface="#313033",
210
+ inverse_primary="#6750A4",
211
+ )
212
+
213
+
214
+ # ============================================================
215
+ # TEXT STYLE & TEXT THEME
216
+ # ============================================================
217
+
218
+ @dataclass
219
+ class TextStyle:
220
+ """
221
+ Estilo tipográfico individual.
222
+ Mapeia para os parâmetros de fonte do ttk.Label / tk.font.Font.
223
+ """
224
+ font_family: str = "Helvetica"
225
+ font_size: int = 10
226
+ font_weight: str = "normal" # "normal" | "bold"
227
+ font_slant: str = "roman" # "roman" | "italic"
228
+ color: Optional[str] = None # None = herda do ColorScheme
229
+
230
+ def to_tk_font(self) -> tuple:
231
+ """Retorna tupla compatível com o parâmetro `font=` do Tk."""
232
+ style = []
233
+ if self.font_weight == "bold":
234
+ style.append("bold")
235
+ if self.font_slant == "italic":
236
+ style.append("italic")
237
+ return (self.font_family, self.font_size) + (tuple(style) if style else ())
238
+
239
+
240
+ @dataclass
241
+ class TextTheme:
242
+ """
243
+ Hierarquia tipográfica completa — espelha o TextTheme do Flutter (Material 3).
244
+
245
+ Display → headings grandes (splash screens, hero)
246
+ Headline → títulos de seção
247
+ Title → AppBar, dialogs
248
+ Body → texto corrido
249
+ Label → botões, captions, chips
250
+ """
251
+ display_large: TextStyle = field(default_factory=lambda: TextStyle(font_size=57, font_weight="normal"))
252
+ display_medium: TextStyle = field(default_factory=lambda: TextStyle(font_size=45, font_weight="normal"))
253
+ display_small: TextStyle = field(default_factory=lambda: TextStyle(font_size=36, font_weight="normal"))
254
+
255
+ headline_large: TextStyle = field(default_factory=lambda: TextStyle(font_size=32, font_weight="normal"))
256
+ headline_medium: TextStyle = field(default_factory=lambda: TextStyle(font_size=28, font_weight="normal"))
257
+ headline_small: TextStyle = field(default_factory=lambda: TextStyle(font_size=24, font_weight="normal"))
258
+
259
+ title_large: TextStyle = field(default_factory=lambda: TextStyle(font_size=22, font_weight="normal"))
260
+ title_medium: TextStyle = field(default_factory=lambda: TextStyle(font_size=14, font_weight="bold"))
261
+ title_small: TextStyle = field(default_factory=lambda: TextStyle(font_size=11, font_weight="bold"))
262
+
263
+ body_large: TextStyle = field(default_factory=lambda: TextStyle(font_size=16, font_weight="normal"))
264
+ body_medium: TextStyle = field(default_factory=lambda: TextStyle(font_size=14, font_weight="normal"))
265
+ body_small: TextStyle = field(default_factory=lambda: TextStyle(font_size=11, font_weight="normal"))
266
+
267
+ label_large: TextStyle = field(default_factory=lambda: TextStyle(font_size=14, font_weight="bold"))
268
+ label_medium: TextStyle = field(default_factory=lambda: TextStyle(font_size=12, font_weight="bold"))
269
+ label_small: TextStyle = field(default_factory=lambda: TextStyle(font_size=10, font_weight="bold"))
270
+
271
+
272
+ # ============================================================
273
+ # BUTTON THEME DATA
274
+ # ============================================================
275
+
276
+ @dataclass
277
+ class ButtonThemeData:
278
+ """
279
+ Configuração visual dos botões.
280
+ Espelha ElevatedButtonThemeData / OutlinedButtonThemeData / TextButtonThemeData.
281
+ """
282
+ # Padding interno (px)
283
+ padding_x: int = 16
284
+ padding_y: int = 8
285
+
286
+ # Raio da borda (aproximado via relief no Tk)
287
+ border_radius: int = 12
288
+
289
+ # Altura mínima (px)
290
+ min_height: int = 20
291
+
292
+ # Estilos para cada variante
293
+ elevated_relief: str = "raised"
294
+ outlined_relief: str = "groove"
295
+ text_relief: str = "flat"
296
+
297
+
298
+ # ============================================================
299
+ # INPUT DECORATION THEME
300
+ # ============================================================
301
+
302
+ @dataclass
303
+ class InputDecorationTheme:
304
+ """
305
+ Estilo dos campos de texto (TextField).
306
+ """
307
+ border_color: Optional[str] = None # None = usa outline do ColorScheme
308
+ focused_border_color: Optional[str] = None # None = usa primary
309
+ label_color: Optional[str] = None
310
+ hint_color: Optional[str] = None
311
+ fill_color: Optional[str] = None # None = usa surface_variant
312
+ filled: bool = True
313
+ border_width: int = 1
314
+ padding_x: int = 12
315
+ padding_y: int = 8
316
+
317
+
318
+ # ============================================================
319
+ # APP BAR THEME
320
+ # ============================================================
321
+
322
+ @dataclass
323
+ class AppBarTheme:
324
+ """
325
+ Estilo da AppBar.
326
+ """
327
+ background_color: Optional[str] = None # None = usa surface do ColorScheme
328
+ foreground_color: Optional[str] = None # None = usa on_surface
329
+ elevation: int = 0
330
+ center_title: bool = False
331
+ title_text_style: Optional[TextStyle] = None
332
+
333
+
334
+ # ============================================================
335
+ # CARD THEME
336
+ # ============================================================
337
+
338
+ @dataclass
339
+ class CardTheme:
340
+ """
341
+ Estilo dos Cards.
342
+ """
343
+ color: Optional[str] = None # None = usa surface
344
+ shadow_color: Optional[str] = None # None = usa shadow
345
+ elevation: int = 1
346
+ margin: int = 4
347
+
348
+
349
+ # ============================================================
350
+ # DIVIDER THEME
351
+ # ============================================================
352
+
353
+ @dataclass
354
+ class DividerTheme:
355
+ color: Optional[str] = None # None = usa outline_variant
356
+ thickness: int = 1
357
+ indent: int = 0
358
+ end_indent: int = 0
359
+
360
+
361
+ # ============================================================
362
+ # THEME DATA
363
+ # ============================================================
364
+
365
+ class ThemeData:
366
+ """
367
+ Objeto raiz do tema — equivalente ao ThemeData do Flutter.
368
+
369
+ Agrega ColorScheme, TextTheme, ButtonThemeData e sub-temas,
370
+ e aplica tudo no ttk.Style quando `apply()` é chamado.
371
+
372
+ Exemplo:
373
+ theme = ThemeData.light()
374
+ theme.apply(root)
375
+ """
376
+
377
+ def __init__(
378
+ self,
379
+ color_scheme: ColorScheme,
380
+ text_theme: Optional[TextTheme] = None,
381
+ button_theme: Optional[ButtonThemeData] = None,
382
+ input_decoration_theme: Optional[InputDecorationTheme] = None,
383
+ app_bar_theme: Optional[AppBarTheme] = None,
384
+ card_theme: Optional[CardTheme] = None,
385
+ divider_theme: Optional[DividerTheme] = None,
386
+ font_family: str = "Helvetica",
387
+ ) -> None:
388
+ self.color_scheme = color_scheme
389
+ self.text_theme = text_theme or TextTheme()
390
+ self.button_theme = button_theme or ButtonThemeData()
391
+ self.input_decoration_theme = input_decoration_theme or InputDecorationTheme()
392
+ self.app_bar_theme = app_bar_theme or AppBarTheme()
393
+ self.card_theme = card_theme or CardTheme()
394
+ self.divider_theme = divider_theme or DividerTheme()
395
+ self.font_family = font_family
396
+
397
+ # Propaga font_family padrão para todos os TextStyle sem família definida
398
+ self._propagate_font_family()
399
+
400
+ def _propagate_font_family(self) -> None:
401
+ for attr in vars(self.text_theme).values():
402
+ if isinstance(attr, TextStyle) and attr.font_family == "Helvetica":
403
+ attr.font_family = self.font_family
404
+
405
+ # ── Factory ──────────────────────────────────────────────
406
+
407
+ @classmethod
408
+ def light(
409
+ cls,
410
+ color_scheme: Optional[ColorScheme] = None,
411
+ font_family: str = "Helvetica",
412
+ **kwargs,
413
+ ) -> "ThemeData":
414
+ """Cria um ThemeData com paleta Light."""
415
+ return cls(
416
+ color_scheme=color_scheme or ColorScheme.light(),
417
+ font_family=font_family,
418
+ **kwargs,
419
+ )
420
+
421
+ @classmethod
422
+ def dark(
423
+ cls,
424
+ color_scheme: Optional[ColorScheme] = None,
425
+ font_family: str = "Helvetica",
426
+ **kwargs,
427
+ ) -> "ThemeData":
428
+ """Cria um ThemeData com paleta Dark."""
429
+ return cls(
430
+ color_scheme=color_scheme or ColorScheme.dark(),
431
+ font_family=font_family,
432
+ **kwargs,
433
+ )
434
+
435
+ # ── Atalhos rápidos para cores ───────────────────────────
436
+
437
+ @property
438
+ def primary(self) -> str:
439
+ return self.color_scheme.primary
440
+
441
+ @property
442
+ def background(self) -> str:
443
+ return self.color_scheme.background
444
+
445
+ @property
446
+ def surface(self) -> str:
447
+ return self.color_scheme.surface
448
+
449
+ @property
450
+ def error(self) -> str:
451
+ return self.color_scheme.error
452
+
453
+ # ── Aplicação no ttk.Style ───────────────────────────────
454
+
455
+ def apply(self, root: tk.Tk) -> None:
456
+ """
457
+ Aplica o tema inteiro no ttk.Style.
458
+ Deve ser chamado APÓS tk.Tk() ser criado — nunca antes,
459
+ pois ttk.Style() sem um root existente cria uma janela tk implícita.
460
+
461
+ Args:
462
+ root: instância do tk.Tk. Obrigatório quando ainda não há
463
+ uma instância Tk ativa no processo.
464
+ """
465
+ # Garante que existe um root antes de instanciar o Style.
466
+ # Se nenhum root foi passado, usa o root padrão do Tk, que
467
+ # só existe se tk.Tk() já foi chamado antes.
468
+ if root is None and not tk._default_root: # noqa: SLF001
469
+ # Sem root ativo: não há como aplicar estilos ttk com segurança.
470
+ # O chamador deve passar o root explicitamente.
471
+ return
472
+
473
+ cs = self.color_scheme
474
+ tt = self.text_theme
475
+ bt = self.button_theme
476
+ idt = self.input_decoration_theme
477
+ abt = self.app_bar_theme
478
+ ct = self.card_theme
479
+ dt = self.divider_theme
480
+
481
+ style = ttk.Style(root) if root else ttk.Style()
482
+
483
+ # ── Tema base ────────────────────────────────────────
484
+ # Usa "clam" como base: permite sobrescrever mais configurações
485
+ try:
486
+ style.theme_use("clam")
487
+ except tk.TclError:
488
+ pass
489
+
490
+ # ── Janela principal ─────────────────────────────────
491
+ if root:
492
+ root.configure(bg=cs.background)
493
+
494
+ # ── TFrame ───────────────────────────────────────────
495
+ style.configure(
496
+ "TFrame",
497
+ background=cs.background,
498
+ )
499
+
500
+ style.configure(
501
+ "Surface.TFrame",
502
+ background=cs.surface,
503
+ )
504
+
505
+ style.configure(
506
+ "Card.TFrame",
507
+ background=ct.color or cs.surface,
508
+ relief="raised",
509
+ borderwidth=ct.elevation,
510
+ )
511
+
512
+ # ── TLabel ───────────────────────────────────────────
513
+ body_font = tt.body_small.to_tk_font()
514
+ title_font = tt.title_small.to_tk_font()
515
+ label_font = tt.label_small.to_tk_font()
516
+ display_font = tt.display_small.to_tk_font()
517
+ headline_font = tt.headline_small.to_tk_font()
518
+
519
+ style.configure(
520
+ "TLabel",
521
+ background=cs.background,
522
+ foreground=cs.on_background,
523
+ font=body_font,
524
+ padding=2,
525
+ )
526
+
527
+ # Variantes semânticas de label
528
+ _label_variants = {
529
+ "Display.TLabel": (display_font, cs.on_background),
530
+ "Headline.TLabel": (headline_font, cs.on_background),
531
+ "Title.TLabel": (title_font, cs.on_surface),
532
+ "Body.TLabel": (body_font, cs.on_background),
533
+ "Caption.TLabel": (tt.label_small.to_tk_font(), cs.on_surface_variant),
534
+ "Error.TLabel": (label_font, cs.error),
535
+ "Primary.TLabel": (label_font, cs.primary),
536
+ "OnPrimary.TLabel": (label_font, cs.on_primary),
537
+ }
538
+ for name, (font, fg) in _label_variants.items():
539
+ style.configure(
540
+ name,
541
+ background=cs.background,
542
+ foreground=fg,
543
+ font=font,
544
+ padding=2,
545
+ )
546
+
547
+ # Label sobre superfície colorida
548
+ style.configure(
549
+ "AppBar.TLabel",
550
+ background=abt.background_color or cs.surface,
551
+ foreground=abt.foreground_color or cs.on_surface,
552
+ font=(abt.title_text_style or tt.title_large).to_tk_font(),
553
+ padding=(8, 8),
554
+ )
555
+
556
+ # ── TButton (ElevatedButton) ─────────────────────────
557
+ style.configure(
558
+ "TButton",
559
+ background=cs.primary,
560
+ foreground=cs.on_primary,
561
+ font=tt.label_small.to_tk_font(),
562
+ padding=(bt.padding_x, bt.padding_y),
563
+ relief=bt.elevated_relief,
564
+ borderwidth=0,
565
+ )
566
+ style.map(
567
+ "TButton",
568
+ background=[
569
+ ("active", cs.primary_container),
570
+ ("disabled", cs.surface_variant),
571
+ ],
572
+ foreground=[
573
+ ("active", cs.on_primary_container),
574
+ ("disabled", cs.on_surface_variant),
575
+ ],
576
+ )
577
+
578
+ # ElevatedButton explícito
579
+ style.configure(
580
+ "Elevated.TButton",
581
+ background=cs.primary,
582
+ foreground=cs.on_primary,
583
+ font=tt.label_large.to_tk_font(),
584
+ padding=(bt.padding_x, bt.padding_y),
585
+ relief=bt.elevated_relief,
586
+ )
587
+ style.map(
588
+ "Elevated.TButton",
589
+ background=[
590
+ ("active", cs.primary_container),
591
+ ("disabled", cs.surface_variant),
592
+ ],
593
+ foreground=[
594
+ ("active", cs.on_primary_container),
595
+ ("disabled", cs.on_surface_variant),
596
+ ],
597
+ )
598
+
599
+ # OutlinedButton
600
+ style.configure(
601
+ "Outlined.TButton",
602
+ background=cs.surface,
603
+ foreground=cs.primary,
604
+ font=tt.label_large.to_tk_font(),
605
+ padding=(bt.padding_x, bt.padding_y),
606
+ relief=bt.outlined_relief,
607
+ borderwidth=bt.border_width if hasattr(bt, "border_width") else 1,
608
+ )
609
+ style.map(
610
+ "Outlined.TButton",
611
+ background=[
612
+ ("active", cs.primary_container),
613
+ ("disabled", cs.surface),
614
+ ],
615
+ foreground=[
616
+ ("active", cs.primary),
617
+ ("disabled", cs.on_surface_variant),
618
+ ],
619
+ )
620
+
621
+ # TextButton
622
+ style.configure(
623
+ "Text.TButton",
624
+ background=cs.background,
625
+ foreground=cs.primary,
626
+ font=tt.label_large.to_tk_font(),
627
+ padding=(bt.padding_x, bt.padding_y),
628
+ relief=bt.text_relief,
629
+ borderwidth=0,
630
+ )
631
+ style.map(
632
+ "Text.TButton",
633
+ background=[
634
+ ("active", cs.primary_container),
635
+ ("disabled", cs.background),
636
+ ],
637
+ foreground=[
638
+ ("active", cs.on_primary_container),
639
+ ("disabled", cs.on_surface_variant),
640
+ ],
641
+ )
642
+
643
+ # FloatingActionButton
644
+ style.configure(
645
+ "FAB.TButton",
646
+ background=cs.primary_container,
647
+ foreground=cs.on_primary_container,
648
+ font=tt.label_large.to_tk_font(),
649
+ padding=(16, 16),
650
+ relief="raised",
651
+ borderwidth=0,
652
+ )
653
+ style.map(
654
+ "FAB.TButton",
655
+ background=[("active", cs.primary)],
656
+ foreground=[("active", cs.on_primary)],
657
+ )
658
+
659
+ # Danger / Error button
660
+ style.configure(
661
+ "Danger.TButton",
662
+ background=cs.error,
663
+ foreground=cs.on_error,
664
+ font=tt.label_large.to_tk_font(),
665
+ padding=(bt.padding_x, bt.padding_y),
666
+ relief=bt.elevated_relief,
667
+ )
668
+ style.map(
669
+ "Danger.TButton",
670
+ background=[("active", cs.error_container)],
671
+ foreground=[("active", cs.on_error_container)],
672
+ )
673
+
674
+ # ── TEntry (TextField) ───────────────────────────────
675
+ fill = idt.fill_color or cs.surface_variant
676
+ border = idt.border_color or cs.outline
677
+ focused_border = idt.focused_border_color or cs.primary
678
+
679
+ style.configure(
680
+ "TEntry",
681
+ fieldbackground=fill,
682
+ foreground=cs.on_surface,
683
+ selectbackground=cs.primary,
684
+ selectforeground=cs.on_primary,
685
+ insertcolor=cs.primary,
686
+ font=body_font,
687
+ padding=(idt.padding_x, idt.padding_y),
688
+ relief="flat",
689
+ borderwidth=idt.border_width,
690
+ bordercolor=border,
691
+ )
692
+ style.map(
693
+ "TEntry",
694
+ fieldbackground=[("focus", cs.surface)],
695
+ bordercolor=[("focus", focused_border)],
696
+ )
697
+
698
+ # ── TCombobox (DropdownButton) ───────────────────────
699
+ style.configure(
700
+ "TCombobox",
701
+ fieldbackground=fill,
702
+ foreground=cs.on_surface,
703
+ background=cs.surface,
704
+ selectbackground=cs.primary,
705
+ selectforeground=cs.on_primary,
706
+ font=body_font,
707
+ padding=(8, 6),
708
+ relief="flat",
709
+ )
710
+ style.map(
711
+ "TCombobox",
712
+ fieldbackground=[("readonly", fill)],
713
+ foreground=[("readonly", cs.on_surface)],
714
+ )
715
+
716
+ # ── TCheckbutton ─────────────────────────────────────
717
+ style.configure(
718
+ "TCheckbutton",
719
+ background=cs.background,
720
+ foreground=cs.on_background,
721
+ font=body_font,
722
+ focuscolor=cs.primary,
723
+ indicatorcolor=cs.primary,
724
+ )
725
+ style.map(
726
+ "TCheckbutton",
727
+ background=[("active", cs.background)],
728
+ indicatorcolor=[
729
+ ("selected", cs.primary),
730
+ ("!selected", cs.surface_variant),
731
+ ],
732
+ )
733
+
734
+ # ── TRadiobutton ─────────────────────────────────────
735
+ style.configure(
736
+ "TRadiobutton",
737
+ background=cs.background,
738
+ foreground=cs.on_background,
739
+ font=body_font,
740
+ focuscolor=cs.primary,
741
+ )
742
+ style.map(
743
+ "TRadiobutton",
744
+ background=[("active", cs.background)],
745
+ indicatorcolor=[
746
+ ("selected", cs.primary),
747
+ ("!selected", cs.surface_variant),
748
+ ],
749
+ )
750
+
751
+ # ── TProgressbar ─────────────────────────────────────
752
+ style.configure(
753
+ "TProgressbar",
754
+ troughcolor=cs.surface_variant,
755
+ background=cs.primary,
756
+ bordercolor=cs.outline,
757
+ lightcolor=cs.primary,
758
+ darkcolor=cs.primary_container,
759
+ )
760
+
761
+ # ── Horizontal.TSeparator (Divider) ──────────────────
762
+ style.configure(
763
+ "TSeparator",
764
+ background=dt.color or cs.outline_variant,
765
+ )
766
+
767
+ # ── TScrollbar ───────────────────────────────────────
768
+ style.configure(
769
+ "TScrollbar",
770
+ background=cs.surface_variant,
771
+ troughcolor=cs.background,
772
+ arrowcolor=cs.on_surface_variant,
773
+ bordercolor=cs.background,
774
+ lightcolor=cs.surface_variant,
775
+ darkcolor=cs.surface_variant,
776
+ )
777
+ style.map(
778
+ "TScrollbar",
779
+ background=[("active", cs.primary)],
780
+ )
781
+
782
+ # ── Treeview (TableView) ─────────────────────────────
783
+ style.configure(
784
+ "Treeview",
785
+ background=cs.surface,
786
+ foreground=cs.on_surface,
787
+ fieldbackground=cs.surface,
788
+ font=body_font,
789
+ rowheight=28,
790
+ borderwidth=0,
791
+ )
792
+ style.configure(
793
+ "Treeview.Heading",
794
+ background=cs.surface_variant,
795
+ foreground=cs.on_surface_variant,
796
+ font=tt.label_medium.to_tk_font(),
797
+ relief="flat",
798
+ padding=(8, 6),
799
+ )
800
+ style.map(
801
+ "Treeview",
802
+ background=[("selected", cs.primary_container)],
803
+ foreground=[("selected", cs.on_primary_container)],
804
+ )
805
+
806
+ # ── AppBar Frame ─────────────────────────────────────
807
+ style.configure(
808
+ "AppBar.TFrame",
809
+ background=abt.background_color or cs.surface,
810
+ relief="flat" if abt.elevation == 0 else "raised",
811
+ borderwidth=abt.elevation,
812
+ )
813
+
814
+ # ── Notebook (TabBar) ─────────────────────────────────
815
+ style.configure(
816
+ "TNotebook",
817
+ background=cs.background,
818
+ borderwidth=0,
819
+ )
820
+ style.configure(
821
+ "TNotebook.Tab",
822
+ background=cs.surface_variant,
823
+ foreground=cs.on_surface_variant,
824
+ font=tt.label_medium.to_tk_font(),
825
+ padding=(12, 6),
826
+ )
827
+ style.map(
828
+ "TNotebook.Tab",
829
+ background=[("selected", cs.primary_container)],
830
+ foreground=[("selected", cs.on_primary_container)],
831
+ )
832
+
833
+ # ── Scale (Slider) ────────────────────────────────────
834
+ style.configure(
835
+ "TScale",
836
+ background=cs.background,
837
+ troughcolor=cs.surface_variant,
838
+ sliderrelief="flat",
839
+ )
840
+
841
+
842
+ # ============================================================
843
+ # THEME PROVIDER (ChangeNotifier)
844
+ # ============================================================
845
+
846
+ class ThemeProvider(ChangeNotifier):
847
+ """
848
+ Provider reativo do tema — permite trocar ThemeData em runtime.
849
+
850
+ Padrão de uso:
851
+ provider = ThemeProvider.from_mode(ThemeMode.SYSTEM)
852
+ app.register_provider(provider)
853
+
854
+ # Para alternar:
855
+ provider.set_mode(ThemeMode.DARK)
856
+ # Todos os Consumer(ThemeProvider) serão reconstruídos.
857
+
858
+ Nota: a re-aplicação do tema via `apply()` muda os ttk.Style globais,
859
+ mas widgets já montados precisam ser desmontados/remontados para refletir
860
+ as novas cores — isso depende do mecanismo de rebuild do pyflutter.
861
+ """
862
+
863
+ def __init__(
864
+ self,
865
+ light_theme: Optional[ThemeData] = None,
866
+ dark_theme: Optional[ThemeData] = None,
867
+ mode: ThemeMode = ThemeMode.SYSTEM,
868
+ ) -> None:
869
+ super().__init__()
870
+ self._light = light_theme or ThemeData.light()
871
+ self._dark = dark_theme or ThemeData.dark()
872
+ self._mode = mode
873
+ self._root: Optional[tk.Tk] = None
874
+
875
+ # ── Factory ──────────────────────────────────────────────
876
+
877
+ @classmethod
878
+ def from_mode(
879
+ cls,
880
+ mode: ThemeMode = ThemeMode.SYSTEM,
881
+ light_theme: Optional[ThemeData] = None,
882
+ dark_theme: Optional[ThemeData] = None,
883
+ ) -> "ThemeProvider":
884
+ return cls(light_theme=light_theme, dark_theme=dark_theme, mode=mode)
885
+
886
+ # ── Estado atual ─────────────────────────────────────────
887
+
888
+ @property
889
+ def is_dark(self) -> bool:
890
+ if self._mode == ThemeMode.DARK:
891
+ return True
892
+ if self._mode == ThemeMode.LIGHT:
893
+ return False
894
+ return _detect_system_dark_mode()
895
+
896
+ @property
897
+ def theme(self) -> ThemeData:
898
+ """Retorna o ThemeData ativo (light ou dark)."""
899
+ return self._dark if self.is_dark else self._light
900
+
901
+ # ── Getters de conveniência (espelha context.theme no Flutter) ──
902
+
903
+ @property
904
+ def color_scheme(self) -> ColorScheme:
905
+ return self.theme.color_scheme
906
+
907
+ @property
908
+ def text_theme(self) -> TextTheme:
909
+ return self.theme.text_theme
910
+
911
+ @property
912
+ def primary(self) -> str:
913
+ return self.theme.primary
914
+
915
+ @property
916
+ def background(self) -> str:
917
+ return self.theme.background
918
+
919
+ # ── Mutação ──────────────────────────────────────────────
920
+
921
+ def set_mode(self, mode: ThemeMode) -> None:
922
+ """Alterna entre Light, Dark e System e notifica os listeners."""
923
+ if mode != self._mode:
924
+ self._mode = mode
925
+ if self._root:
926
+ self.theme.apply(self._root)
927
+ self.notify_listeners()
928
+
929
+ def toggle(self) -> None:
930
+ """Alterna entre Dark e Light (ignora System)."""
931
+ self.set_mode(
932
+ ThemeMode.LIGHT if self.is_dark else ThemeMode.DARK
933
+ )
934
+
935
+ def set_light_theme(self, theme: ThemeData) -> None:
936
+ self._light = theme
937
+ self.notify_listeners()
938
+
939
+ def set_dark_theme(self, theme: ThemeData) -> None:
940
+ self._dark = theme
941
+ self.notify_listeners()
942
+
943
+ # ── Aplicação ────────────────────────────────────────────
944
+
945
+ def attach_root(self, root: tk.Tk) -> None:
946
+ """
947
+ Vincula o provider à janela raiz do Tk.
948
+ Deve ser chamado em TkMaterialApp.run() antes do mainloop.
949
+ """
950
+ self._root = root
951
+ self.apply()
952
+
953
+ def apply(self) -> None:
954
+ """Aplica o tema ativo no ttk.Style."""
955
+ self.theme.apply(self._root)
956
+
957
+
958
+ # ============================================================
959
+ # ATALHO GLOBAL (equivalente ao Theme.of(context))
960
+ # ============================================================
961
+
962
+ class Theme:
963
+ """
964
+ Classe utilitária com factory methods — mantém compatibilidade
965
+ com o código existente que usava Theme.PRIMARY, Theme.apply(), etc.
966
+
967
+ Para novos projetos, prefira ThemeProvider + ThemeData.
968
+ """
969
+
970
+ # Constantes legadas (compatibilidade)
971
+ PRIMARY: str = ColorScheme.light().primary
972
+ BACKGROUND: str = ColorScheme.light().background
973
+
974
+ @staticmethod
975
+ def light(font_family: str = "Helvetica") -> ThemeData:
976
+ return ThemeData.light(font_family=font_family)
977
+
978
+ @staticmethod
979
+ def dark(font_family: str = "Helvetica") -> ThemeData:
980
+ return ThemeData.dark(font_family=font_family)
981
+
982
+ @staticmethod
983
+ def apply(root: Optional[tk.Tk] = None) -> None:
984
+ """
985
+ Compatibilidade legada: aplica um tema light padrão.
986
+ Requer que tk.Tk() já tenha sido criado, ou que `root` seja passado.
987
+ """
988
+ ThemeData.light().apply(root)
989
+
990
+ @staticmethod
991
+ def of(context) -> ThemeData:
992
+ """
993
+ Equivalente ao Theme.of(context) do Flutter.
994
+ Recupera o ThemeData ativo a partir do BuildContext.
995
+
996
+ Uso:
997
+ theme = Theme.of(context)
998
+ color = theme.primary
999
+ """
1000
+ try:
1001
+ provider: ThemeProvider = context.read(ThemeProvider)
1002
+ return provider.theme
1003
+ except (KeyError, AttributeError):
1004
+ return ThemeData.light()
1005
+
1006
+
1007
+ # ============================================================
1008
+ # INTEGRAÇÃO COM TkMaterialApp
1009
+ # ============================================================
1010
+
1011
+
1012
+ def make_themed_app(base_app_class: type[BaseMaterialApp]):
1013
+ """
1014
+ Mixin/decorator que estende qualquer subclasse de BaseMaterialApp
1015
+ para aplicar o ThemeProvider automaticamente dentro de run(),
1016
+ após tk.Tk() ser criado — sem risco de janela dupla.
1017
+
1018
+ Uso:
1019
+ from pyflutter.widgets import TkMaterialApp
1020
+ from pyflutter.theme import make_themed_app
1021
+
1022
+ ThemedApp = make_themed_app(TkMaterialApp)
1023
+ app = ThemedApp(home=MyPage(), title="App")
1024
+ app.register_provider(ThemeProvider(...))
1025
+ app.run() # aplica o tema no momento certo
1026
+ """
1027
+
1028
+ class ThemedApp(base_app_class):
1029
+
1030
+ def run(self) -> None:
1031
+
1032
+ self.root = tk.Tk()
1033
+ self.root.title(self.title)
1034
+
1035
+ # Aplica o tema AGORA — root já existe, sem janela dupla.
1036
+ try:
1037
+ provider: ThemeProvider = self.providers.get(ThemeProvider)
1038
+ if provider is not None:
1039
+ provider.attach_root(self.root)
1040
+ except Exception:
1041
+ pass
1042
+
1043
+ self.get_navigator().push(self.home)
1044
+ self.root.mainloop()
1045
+
1046
+ def get_navigator(self) -> Navigator:
1047
+ return super().get_navigator()
1048
+
1049
+ def register_notifier(self, provider: T) -> None:
1050
+ return super().register_notifier(provider)
1051
+
1052
+
1053
+
1054
+
1055
+ ThemedApp.__name__ = f"Themed{base_app_class.__name__}"
1056
+ ThemedApp.__qualname__ = ThemedApp.__name__
1057
+ return ThemedApp
1058
+