m-you-tk 0.1.0__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.
- m_you/__init__.py +41 -0
- m_you/assets/material.ttf +0 -0
- m_you/button.py +63 -0
- m_you/checkbox.py +96 -0
- m_you/dropdown.py +189 -0
- m_you/input.py +110 -0
- m_you/radiobutton.py +85 -0
- m_you/slider.py +97 -0
- m_you/switch.py +81 -0
- m_you/toast.py +72 -0
- m_you_tk-0.1.0.dist-info/METADATA +10 -0
- m_you_tk-0.1.0.dist-info/RECORD +15 -0
- m_you_tk-0.1.0.dist-info/WHEEL +5 -0
- m_you_tk-0.1.0.dist-info/licenses/LICENSE +15 -0
- m_you_tk-0.1.0.dist-info/top_level.txt +1 -0
m_you/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# --- 1. ЛОГИКА ЗАГРУЗКИ ШРИФТА ---
|
|
6
|
+
# Используем dirname(__file__), чтобы путь всегда вел внутрь установленного пакета
|
|
7
|
+
font_path = os.path.join(os.path.dirname(__file__), "assets", "material.ttf")
|
|
8
|
+
|
|
9
|
+
if os.path.exists(font_path):
|
|
10
|
+
if sys.platform == "win32":
|
|
11
|
+
# Загрузка для Windows (без установки в систему)
|
|
12
|
+
ctypes.windll.gdi32.AddFontResourceExW(font_path, 0x10, 0)
|
|
13
|
+
# На Linux (Arch) шрифты обычно подхватываются из папки ~/.fonts или через fontconfig,
|
|
14
|
+
# но для кроссплатформенности на Win этого кода достаточно.
|
|
15
|
+
else:
|
|
16
|
+
# Не принтим ошибку в продакшне, чтобы не спамить пользователю в консоль,
|
|
17
|
+
# либо используем logging
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
# --- 2. ЭКСПОРТ КОМПОНЕНТОВ ---
|
|
21
|
+
# Это позволяет писать: from m_you import MaterialButton
|
|
22
|
+
from .button import MaterialButton
|
|
23
|
+
from .input import MaterialInput
|
|
24
|
+
from .dropdown import MaterialDropdown
|
|
25
|
+
from .checkbox import MaterialCheckbox
|
|
26
|
+
from .switch import MaterialSwitch
|
|
27
|
+
from .slider import MaterialSlider
|
|
28
|
+
from .radiobutton import MaterialRadioButton
|
|
29
|
+
from .toast import MaterialToast
|
|
30
|
+
|
|
31
|
+
# Список того, что будет доступно при "from m_you import *"
|
|
32
|
+
__all__ = [
|
|
33
|
+
"MaterialButton",
|
|
34
|
+
"MaterialInput",
|
|
35
|
+
"MaterialDropdown",
|
|
36
|
+
"MaterialCheckbox",
|
|
37
|
+
"MaterialSwitch",
|
|
38
|
+
"MaterialSlider",
|
|
39
|
+
"MaterialRadioButton",
|
|
40
|
+
"MaterialToast"
|
|
41
|
+
]
|
|
Binary file
|
m_you/button.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from .dropdown import is_globally_locked
|
|
3
|
+
|
|
4
|
+
ICON_MAP = {
|
|
5
|
+
"edit": "\ue3c9", "settings": "\ue8b8", "home": "\ue88a",
|
|
6
|
+
"search": "\ue8b6", "favorite": "\ue87d", "delete": "\ue872",
|
|
7
|
+
"add": "\ue145", "check": "\ue5ca", "close": "\ue5cd",
|
|
8
|
+
"menu": "\ue5d2", "person": "\ue7fd", "share": "\ue80d",
|
|
9
|
+
"sound": "\ue050", "wifi": "\ue63e", "battery": "\ue1a4",
|
|
10
|
+
"camera": "\ue412", "folder": "\ue2c7", "email": "\ue0be", "clock": "\ue192"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class MaterialButton(tk.Canvas):
|
|
14
|
+
def __init__(self, master, text="Default", icon="home", command=None, **kwargs):
|
|
15
|
+
self.idle_color = "#543996"
|
|
16
|
+
self.active_color = "#6121ff"
|
|
17
|
+
self.text_color = "#EADDFF"
|
|
18
|
+
|
|
19
|
+
self.icon_char = ICON_MAP.get(icon.lower(), ICON_MAP["home"])
|
|
20
|
+
|
|
21
|
+
kwargs.setdefault('height', 60)
|
|
22
|
+
kwargs.setdefault('width', 200)
|
|
23
|
+
kwargs.setdefault('highlightthickness', 0)
|
|
24
|
+
kwargs.setdefault('bg', master.cget('bg'))
|
|
25
|
+
super().__init__(master, **kwargs)
|
|
26
|
+
|
|
27
|
+
self.command = command
|
|
28
|
+
self._draw(text)
|
|
29
|
+
|
|
30
|
+
self.bind("<ButtonPress-1>", self._on_press)
|
|
31
|
+
self.bind("<ButtonRelease-1>", self._on_release)
|
|
32
|
+
|
|
33
|
+
def _draw(self, text):
|
|
34
|
+
self.delete("all")
|
|
35
|
+
w, h = int(self.cget('width')), int(self.cget('height'))
|
|
36
|
+
pad = 6
|
|
37
|
+
r = h - (pad * 2)
|
|
38
|
+
|
|
39
|
+
# Форма кнопки
|
|
40
|
+
self.create_oval(pad, pad, r+pad, h-pad, fill=self.idle_color, outline="", tags="shape")
|
|
41
|
+
self.create_oval(w-r-pad, pad, w-pad, h-pad, fill=self.idle_color, outline="", tags="shape")
|
|
42
|
+
self.create_rectangle(pad+r/2, pad, w-pad-r/2, h-pad + 1, fill=self.idle_color, outline="", tags="shape")
|
|
43
|
+
|
|
44
|
+
# Контент
|
|
45
|
+
self.create_text(pad + r/2 + 5, h/2, text=self.icon_char, fill=self.text_color,
|
|
46
|
+
font=("Material Icons", 18), tags="content")
|
|
47
|
+
self.create_text(w/2 + 10, h/2, text=text, fill=self.text_color,
|
|
48
|
+
font=("Segoe UI", 11, "bold"), tags="content")
|
|
49
|
+
|
|
50
|
+
def _on_press(self, event):
|
|
51
|
+
self.itemconfig("shape", fill=self.active_color)
|
|
52
|
+
self.move("all", 1, 1)
|
|
53
|
+
|
|
54
|
+
def _on_release(self, event):
|
|
55
|
+
self.itemconfig("shape", fill=self.idle_color)
|
|
56
|
+
self.move("all", -1, -1)
|
|
57
|
+
|
|
58
|
+
# ПРОВЕРКА: Если только что закрыли меню — игнорируем клик
|
|
59
|
+
if is_globally_locked():
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if self.command:
|
|
63
|
+
self.command()
|
m_you/checkbox.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
|
|
3
|
+
class MaterialCheckbox(tk.Canvas):
|
|
4
|
+
def __init__(self, master, text="Option", width=250, **kwargs):
|
|
5
|
+
self.bg_idle = "#49454F"
|
|
6
|
+
self.bg_active = "#6121ff"
|
|
7
|
+
self.tick_color = "#FFFFFF"
|
|
8
|
+
self.text_color = "#E6E1E5"
|
|
9
|
+
|
|
10
|
+
self.is_checked = False
|
|
11
|
+
self.anim_p = 0.0
|
|
12
|
+
self.after_id = None
|
|
13
|
+
|
|
14
|
+
kwargs.setdefault('height', 40)
|
|
15
|
+
kwargs.setdefault('width', width)
|
|
16
|
+
kwargs.setdefault('highlightthickness', 0)
|
|
17
|
+
kwargs.setdefault('bg', master.cget('bg'))
|
|
18
|
+
super().__init__(master, **kwargs)
|
|
19
|
+
|
|
20
|
+
# 1. НЕВИДИМАЯ ПОДЛОЖКА (Для клика по всей области)
|
|
21
|
+
# Она ловит клики даже там, где нет текста или линий
|
|
22
|
+
self.create_rectangle(0, 0, width, 40, fill="", outline="", tags="target")
|
|
23
|
+
|
|
24
|
+
# 2. Рамка чекбокса
|
|
25
|
+
self.box_id = self.create_rectangle(10, 10, 30, 30, outline=self.bg_idle, width=2, tags="target")
|
|
26
|
+
|
|
27
|
+
# 3. Внутренняя заливка
|
|
28
|
+
self.fill_id = self.create_rectangle(20, 20, 20, 20, fill="", outline="", tags="target")
|
|
29
|
+
|
|
30
|
+
# 4. Галочка
|
|
31
|
+
self.tick_line1 = self.create_line(14, 20, 14, 20, fill="", width=2, capstyle="round", tags="target")
|
|
32
|
+
self.tick_line2 = self.create_line(19, 25, 19, 25, fill="", width=2, capstyle="round", tags="target")
|
|
33
|
+
|
|
34
|
+
# 5. Текст
|
|
35
|
+
self.create_text(40, 20, text=text, fill=self.text_color,
|
|
36
|
+
font=("Segoe UI", 10), anchor="w", tags="target")
|
|
37
|
+
|
|
38
|
+
# ТЕПЕРЬ ВСЁ С ТЕГОМ "target" БУДЕТ КЛИКАБЕЛЬНЫМ
|
|
39
|
+
self.tag_bind("target", "<Button-1>", lambda e: self.toggle())
|
|
40
|
+
|
|
41
|
+
# Добавим эффект наведения для всей зоны
|
|
42
|
+
self.tag_bind("target", "<Enter>", lambda e: self.config(cursor="hand2"))
|
|
43
|
+
self.tag_bind("target", "<Leave>", lambda e: self.config(cursor=""))
|
|
44
|
+
|
|
45
|
+
def toggle(self):
|
|
46
|
+
self.is_checked = not self.is_checked
|
|
47
|
+
target = 1.0 if self.is_checked else 0.0
|
|
48
|
+
self._animate(target)
|
|
49
|
+
|
|
50
|
+
def _animate(self, target):
|
|
51
|
+
if self.after_id: self.after_cancel(self.after_id)
|
|
52
|
+
step = 0.25 # Чуть ускорим для отзывчивости
|
|
53
|
+
diff = target - self.anim_p
|
|
54
|
+
if abs(diff) > 0.01:
|
|
55
|
+
self.anim_p += diff * step
|
|
56
|
+
self._update_ui()
|
|
57
|
+
self.after_id = self.after(10, lambda: self._animate(target))
|
|
58
|
+
else:
|
|
59
|
+
self.anim_p = target
|
|
60
|
+
self._update_ui()
|
|
61
|
+
|
|
62
|
+
def _update_ui(self):
|
|
63
|
+
p = self.anim_p
|
|
64
|
+
|
|
65
|
+
# Цвет и заливка
|
|
66
|
+
if p > 0.05:
|
|
67
|
+
size = 10 * p
|
|
68
|
+
self.coords(self.fill_id, 20-size, 20-size, 20+size, 20+size)
|
|
69
|
+
self.itemconfig(self.fill_id, fill=self.bg_active)
|
|
70
|
+
self.itemconfig(self.box_id, outline=self.bg_active)
|
|
71
|
+
else:
|
|
72
|
+
self.itemconfig(self.fill_id, fill="")
|
|
73
|
+
self.itemconfig(self.box_id, outline=self.bg_idle)
|
|
74
|
+
|
|
75
|
+
# Галочка
|
|
76
|
+
p1 = min(1.0, p * 2)
|
|
77
|
+
p2 = max(0.0, (p-0.5)*2)
|
|
78
|
+
|
|
79
|
+
if p > 0.3:
|
|
80
|
+
color = self.tick_color
|
|
81
|
+
x2, y2 = 14 + (5 * p1), 20 + (5 * p1)
|
|
82
|
+
self.coords(self.tick_line1, 14, 20, x2, y2)
|
|
83
|
+
self.itemconfig(self.tick_line1, fill=color)
|
|
84
|
+
|
|
85
|
+
if p > 0.5:
|
|
86
|
+
x3, y3 = 19 + (7 * p2), 25 - (10 * p2)
|
|
87
|
+
self.coords(self.tick_line2, 19, 25, x3, y3)
|
|
88
|
+
self.itemconfig(self.tick_line2, fill=color)
|
|
89
|
+
else:
|
|
90
|
+
self.itemconfig(self.tick_line2, fill="")
|
|
91
|
+
else:
|
|
92
|
+
self.itemconfig(self.tick_line1, fill="")
|
|
93
|
+
self.itemconfig(self.tick_line2, fill="")
|
|
94
|
+
|
|
95
|
+
def get(self):
|
|
96
|
+
return self.is_checked
|
m_you/dropdown.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from PIL import Image, ImageTk, ImageDraw
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
# Глобальный таймер для предотвращения "сквозного клика"
|
|
6
|
+
_last_close_time = 0
|
|
7
|
+
|
|
8
|
+
def is_globally_locked():
|
|
9
|
+
"""Проверяет, прошло ли более 200мс с момента закрытия любого меню"""
|
|
10
|
+
return (time.time() - _last_close_time) < 0.2
|
|
11
|
+
|
|
12
|
+
class MaterialDropdown(tk.Canvas):
|
|
13
|
+
def __init__(self, master, label="Select Option", options=None, width=300, **kwargs):
|
|
14
|
+
self.bg_color = "#2B2930"
|
|
15
|
+
self.line_idle = "#49454F"
|
|
16
|
+
self.line_active = "#6121ff"
|
|
17
|
+
self.label_color = "#CAC4D0"
|
|
18
|
+
self.text_color = "#E6E1E5"
|
|
19
|
+
|
|
20
|
+
self.label_text = label
|
|
21
|
+
self.options = options or []
|
|
22
|
+
self.width_val = width
|
|
23
|
+
self.is_open = False
|
|
24
|
+
self.is_animating = False
|
|
25
|
+
self.anim_progress = 0.0
|
|
26
|
+
self.after_id = None
|
|
27
|
+
self.dropdown_after_id = None
|
|
28
|
+
|
|
29
|
+
kwargs.setdefault('height', 56)
|
|
30
|
+
kwargs.setdefault('highlightthickness', 0)
|
|
31
|
+
kwargs.setdefault('bg', master.cget('bg'))
|
|
32
|
+
kwargs.setdefault('width', width) # Добавляем эту строку
|
|
33
|
+
super().__init__(master, **kwargs)
|
|
34
|
+
|
|
35
|
+
# 1. Фон
|
|
36
|
+
self.rect = self.create_rectangle(0, 0, width, 56, fill=self.bg_color, outline="", tags="target")
|
|
37
|
+
|
|
38
|
+
# 2. Текст
|
|
39
|
+
self.label_id = self.create_text(16, 28, text=label, fill=self.label_color,
|
|
40
|
+
font=("Segoe UI", 11), anchor="w", tags="target")
|
|
41
|
+
|
|
42
|
+
# 3. Стрелка (подвинута на 32 от края)
|
|
43
|
+
self._create_arrow_image()
|
|
44
|
+
self.arrow_id = self.create_image(width - 32, 28, image=self.arrow_tk, tags="target")
|
|
45
|
+
|
|
46
|
+
# 4. Линии
|
|
47
|
+
self.create_line(0, 54, width, 54, fill=self.line_idle, width=1, tags="line_layer")
|
|
48
|
+
self.active_line = self.create_line(width/2, 54, width/2, 54, fill=self.line_active, width=3, tags="line_layer")
|
|
49
|
+
self.tag_raise("line_layer")
|
|
50
|
+
|
|
51
|
+
self.list_window = None
|
|
52
|
+
|
|
53
|
+
# Бинды
|
|
54
|
+
self.tag_bind("target", "<Button-1>", self.toggle_menu)
|
|
55
|
+
|
|
56
|
+
def _create_arrow_image(self):
|
|
57
|
+
size = 24
|
|
58
|
+
self.base_arrow = Image.new("RGBA", (size, size), (0,0,0,0))
|
|
59
|
+
draw = ImageDraw.Draw(self.base_arrow)
|
|
60
|
+
padding = 7
|
|
61
|
+
coords = [(padding, padding+2), (size-padding, padding+2), (size/2, size-padding+2)]
|
|
62
|
+
draw.polygon(coords, fill=self.label_color)
|
|
63
|
+
self.arrow_tk = ImageTk.PhotoImage(self.base_arrow)
|
|
64
|
+
|
|
65
|
+
def toggle_menu(self, event=None):
|
|
66
|
+
if self.is_animating: return
|
|
67
|
+
if self.is_open:
|
|
68
|
+
self.close_menu()
|
|
69
|
+
else:
|
|
70
|
+
self.open_menu()
|
|
71
|
+
|
|
72
|
+
def open_menu(self):
|
|
73
|
+
if self.is_open and not self.is_animating: return
|
|
74
|
+
self.is_open = True
|
|
75
|
+
self.is_animating = True
|
|
76
|
+
self._start_animation(True)
|
|
77
|
+
|
|
78
|
+
# Создаем Toplevel
|
|
79
|
+
self.list_window = tk.Toplevel(self)
|
|
80
|
+
self.list_window.overrideredirect(True)
|
|
81
|
+
self.list_window.configure(bg="#2B2930")
|
|
82
|
+
self.list_window.attributes("-topmost", True)
|
|
83
|
+
|
|
84
|
+
container = tk.Frame(self.list_window, bg="#2B2930", padx=2, pady=2)
|
|
85
|
+
container.pack(fill="both", expand=True)
|
|
86
|
+
|
|
87
|
+
for opt in self.options:
|
|
88
|
+
btn = tk.Label(container, text=opt, bg="#2B2930", fg=self.text_color,
|
|
89
|
+
font=("Segoe UI", 11), anchor="w", padx=14, pady=10, cursor="hand2")
|
|
90
|
+
btn.pack(fill="x")
|
|
91
|
+
btn.bind("<Enter>", lambda e, b=btn: b.configure(bg="#36343B"))
|
|
92
|
+
btn.bind("<Leave>", lambda e, b=btn: b.configure(bg="#2B2930"))
|
|
93
|
+
btn.bind("<Button-1>", lambda e, v=opt: self.select_option(v))
|
|
94
|
+
|
|
95
|
+
# --- ИСПРАВЛЕНИЕ: используем self.width_val вместо winfo_width() ---
|
|
96
|
+
# 1. Используем заданную ширину (width=300 или width=400)
|
|
97
|
+
actual_w = self.width_val
|
|
98
|
+
# 2. Получаем координаты относительно экрана
|
|
99
|
+
x = self.winfo_rootx()
|
|
100
|
+
y = self.winfo_rooty() + self.winfo_height()
|
|
101
|
+
|
|
102
|
+
target_h = len(self.options) * 42 + 4
|
|
103
|
+
# 3. Устанавливаем ширину окна в точности как у Canvas
|
|
104
|
+
self.list_window.geometry(f"{actual_w}x0+{x}+{y}")
|
|
105
|
+
# ------------------------------------
|
|
106
|
+
|
|
107
|
+
self._animate_dropdown(0, target_h, actual_w)
|
|
108
|
+
self.list_window.focus_set()
|
|
109
|
+
self.list_window.bind("<FocusOut>", lambda e: self.close_menu())
|
|
110
|
+
|
|
111
|
+
def _animate_dropdown(self, current_h, target_h, width):
|
|
112
|
+
if not self.list_window: return
|
|
113
|
+
step = (target_h - current_h) * 0.3 + 2
|
|
114
|
+
if current_h < target_h - 1:
|
|
115
|
+
new_h = int(current_h + step)
|
|
116
|
+
self.list_window.geometry(f"{width}x{new_h}")
|
|
117
|
+
self.dropdown_after_id = self.after(10, lambda: self._animate_dropdown(new_h, target_h, width))
|
|
118
|
+
else:
|
|
119
|
+
self.list_window.geometry(f"{width}x{target_h}")
|
|
120
|
+
self.is_animating = False
|
|
121
|
+
|
|
122
|
+
def select_option(self, value):
|
|
123
|
+
self.itemconfig(self.label_id, text=value, fill=self.text_color)
|
|
124
|
+
self.close_menu()
|
|
125
|
+
|
|
126
|
+
def close_menu(self, event=None):
|
|
127
|
+
global _last_close_time
|
|
128
|
+
if not self.is_open: return
|
|
129
|
+
self.is_open = False
|
|
130
|
+
self.is_animating = True
|
|
131
|
+
self._start_animation(False)
|
|
132
|
+
|
|
133
|
+
if self.list_window:
|
|
134
|
+
self.list_window.destroy()
|
|
135
|
+
self.list_window = None
|
|
136
|
+
|
|
137
|
+
# Ставим метку времени закрытия
|
|
138
|
+
_last_close_time = time.time()
|
|
139
|
+
self.after(150, lambda: setattr(self, 'is_animating', False))
|
|
140
|
+
|
|
141
|
+
def _start_animation(self, focus):
|
|
142
|
+
if self.after_id: self.after_cancel(self.after_id)
|
|
143
|
+
target = 1.0 if focus else 0.0
|
|
144
|
+
self._animate_step(target)
|
|
145
|
+
|
|
146
|
+
def _animate_step(self, target):
|
|
147
|
+
step = 0.2
|
|
148
|
+
diff = target - self.anim_progress
|
|
149
|
+
if abs(diff) > 0.01:
|
|
150
|
+
self.anim_progress += diff * step
|
|
151
|
+
self._update_ui()
|
|
152
|
+
self.after_id = self.after(10, lambda: self._animate_step(target))
|
|
153
|
+
else:
|
|
154
|
+
self.anim_progress = target
|
|
155
|
+
self._update_ui()
|
|
156
|
+
self.after_id = None
|
|
157
|
+
|
|
158
|
+
def _lerp_color(self, color_start, color_end, fraction):
|
|
159
|
+
def hex_to_rgb(hex_c):
|
|
160
|
+
hex_c = hex_c.lstrip('#')
|
|
161
|
+
return tuple(int(hex_c[i:i+2], 16) for i in (0, 2, 4))
|
|
162
|
+
c1, c2 = hex_to_rgb(color_start), hex_to_rgb(color_end)
|
|
163
|
+
curr_rgb = tuple(int(c1[i] + (c2[i] - c1[i]) * fraction) for i in range(3))
|
|
164
|
+
return "#%02x%02x%02x" % curr_rgb
|
|
165
|
+
|
|
166
|
+
def _update_ui(self):
|
|
167
|
+
p = self.anim_progress
|
|
168
|
+
curr_color = self._lerp_color(self.label_color, self.line_active, p)
|
|
169
|
+
self.itemconfig(self.label_id, fill=curr_color)
|
|
170
|
+
|
|
171
|
+
# Анимация стрелки
|
|
172
|
+
angle = p * -180
|
|
173
|
+
rotated = self.base_arrow.rotate(angle, resample=Image.BICUBIC)
|
|
174
|
+
r_t, g_t, b_t = tuple(int(curr_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
|
|
175
|
+
data = rotated.getdata()
|
|
176
|
+
new_data = [ (r_t, g_t, b_t, item[3]) if item[3] > 0 else item for item in data ]
|
|
177
|
+
rotated.putdata(new_data)
|
|
178
|
+
self.arrow_tk = ImageTk.PhotoImage(rotated)
|
|
179
|
+
self.itemconfig(self.arrow_id, image=self.arrow_tk)
|
|
180
|
+
|
|
181
|
+
# Линия
|
|
182
|
+
x_start = (self.width_val / 2) * (1 - p)
|
|
183
|
+
x_end = (self.width_val / 2) * (1 + p)
|
|
184
|
+
self.coords(self.active_line, x_start, 54, x_end, 54)
|
|
185
|
+
self.tag_raise("line_layer")
|
|
186
|
+
|
|
187
|
+
def get(self):
|
|
188
|
+
val = self.itemcget(self.label_id, "text")
|
|
189
|
+
return val if val != self.label_text else ""
|
m_you/input.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
|
|
3
|
+
class MaterialInput(tk.Canvas):
|
|
4
|
+
def __init__(self, master, label="Username", icon=None, width=300, **kwargs):
|
|
5
|
+
self.bg_color = "#2B2930"
|
|
6
|
+
self.line_idle = "#49454F"
|
|
7
|
+
self.line_active = "#6121ff"
|
|
8
|
+
self.label_color = "#CAC4D0"
|
|
9
|
+
self.text_color = "#E6E1E5"
|
|
10
|
+
|
|
11
|
+
self.width_val = width
|
|
12
|
+
kwargs.setdefault('height', 60) # Немного увеличим высоту, чтобы ничего не резало
|
|
13
|
+
kwargs.setdefault('highlightthickness', 0)
|
|
14
|
+
kwargs.setdefault('bg', master.cget('bg'))
|
|
15
|
+
super().__init__(master, **kwargs)
|
|
16
|
+
|
|
17
|
+
self.anim_progress = 0.0
|
|
18
|
+
self.after_id = None
|
|
19
|
+
|
|
20
|
+
# Фон
|
|
21
|
+
self.rect = self.create_rectangle(0, 0, self.width_val, 56, fill=self.bg_color, outline="")
|
|
22
|
+
|
|
23
|
+
# Смещение текста
|
|
24
|
+
self.text_offset = 16
|
|
25
|
+
if icon:
|
|
26
|
+
self.text_offset = 48
|
|
27
|
+
try:
|
|
28
|
+
from .button import ICON_MAP
|
|
29
|
+
icon_char = ICON_MAP.get(icon.lower(), "")
|
|
30
|
+
except: icon_char = "•"
|
|
31
|
+
|
|
32
|
+
self.icon_id = self.create_text(24, 28, text=icon_char, fill=self.label_color,
|
|
33
|
+
font=("Material Icons", 18))
|
|
34
|
+
|
|
35
|
+
# Поле ввода (Entry)
|
|
36
|
+
# highlightthickness=0 и bd=0 убирают белые рамки и обрезку
|
|
37
|
+
self.entry = tk.Entry(self, bg=self.bg_color, fg=self.text_color,
|
|
38
|
+
insertbackground=self.line_active, borderwidth=0,
|
|
39
|
+
highlightthickness=0, font=("Segoe UI", 12))
|
|
40
|
+
|
|
41
|
+
# Размещаем Entry чуть выше линии
|
|
42
|
+
self.entry_window = self.create_window(self.text_offset, 30, window=self.entry,
|
|
43
|
+
width=self.width_val - self.text_offset - 12,
|
|
44
|
+
anchor="nw")
|
|
45
|
+
|
|
46
|
+
# Заголовок (Label)
|
|
47
|
+
self.label_id = self.create_text(self.text_offset, 22, text=label,
|
|
48
|
+
fill=self.label_color, font=("Segoe UI", 11), anchor="w")
|
|
49
|
+
|
|
50
|
+
# Линии
|
|
51
|
+
# Рисуем линии на 1 пиксель короче ширины, чтобы края не "вылезали"
|
|
52
|
+
self.create_line(2, 54, self.width_val-2, 54, fill=self.line_idle, width=1, tags="line")
|
|
53
|
+
self.active_line = self.create_line(self.width_val/2, 54, self.width_val/2, 54,
|
|
54
|
+
fill=self.line_active, width=2, tags="line")
|
|
55
|
+
|
|
56
|
+
# Бинды
|
|
57
|
+
self.tag_bind(self.rect, "<Button-1>", lambda e: self.entry.focus_set())
|
|
58
|
+
self.entry.bind("<FocusIn>", lambda e: self._start_animation(True))
|
|
59
|
+
self.entry.bind("<FocusOut>", lambda e: self._start_animation(False))
|
|
60
|
+
|
|
61
|
+
def _start_animation(self, focus):
|
|
62
|
+
if self.after_id: self.after_cancel(self.after_id)
|
|
63
|
+
# Позиция: наверху если фокус или есть текст
|
|
64
|
+
target = 1.0 if (focus or self.entry.get()) else 0.0
|
|
65
|
+
self._animate_step(target)
|
|
66
|
+
|
|
67
|
+
def _animate_step(self, target):
|
|
68
|
+
step = 0.2
|
|
69
|
+
diff = target - self.anim_progress
|
|
70
|
+
if abs(diff) > 0.01:
|
|
71
|
+
self.anim_progress += diff * step
|
|
72
|
+
self._update_ui()
|
|
73
|
+
self.after_id = self.after(10, lambda: self._animate_step(target))
|
|
74
|
+
else:
|
|
75
|
+
self.anim_progress = target
|
|
76
|
+
self._update_ui()
|
|
77
|
+
self.after_id = None
|
|
78
|
+
|
|
79
|
+
def _update_ui(self):
|
|
80
|
+
p = self.anim_progress
|
|
81
|
+
has_focus = (self.focus_get() == self.entry)
|
|
82
|
+
|
|
83
|
+
# Цвет: строго фиолетовый только при фокусе
|
|
84
|
+
curr_color = self.line_active if has_focus else self.label_color
|
|
85
|
+
|
|
86
|
+
# Позиция и размер текста
|
|
87
|
+
curr_y = 22 - (18 * p)
|
|
88
|
+
curr_size = int(11 - (2 * p))
|
|
89
|
+
|
|
90
|
+
self.coords(self.label_id, self.text_offset, curr_y)
|
|
91
|
+
self.itemconfig(self.label_id,
|
|
92
|
+
font=("Segoe UI", curr_size, "bold" if has_focus else "normal"),
|
|
93
|
+
fill=curr_color)
|
|
94
|
+
|
|
95
|
+
# Линия
|
|
96
|
+
line_visual_color = self.line_active if has_focus else self.line_idle
|
|
97
|
+
self.itemconfig(self.active_line, fill=line_visual_color)
|
|
98
|
+
|
|
99
|
+
x_start = (self.width_val / 2) * (1 - p) + 2
|
|
100
|
+
x_end = (self.width_val / 2) * (1 + p) - 2
|
|
101
|
+
self.coords(self.active_line, x_start, 54, x_end, 54)
|
|
102
|
+
|
|
103
|
+
if hasattr(self, 'icon_id'):
|
|
104
|
+
self.itemconfig(self.icon_id, fill=curr_color)
|
|
105
|
+
|
|
106
|
+
# Чтобы линии всегда были поверх фона
|
|
107
|
+
self.tag_raise("line")
|
|
108
|
+
|
|
109
|
+
def get(self):
|
|
110
|
+
return self.entry.get()
|
m_you/radiobutton.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
|
|
3
|
+
class MaterialRadioButton(tk.Canvas):
|
|
4
|
+
def __init__(self, master, text="Option", value=None, variable=None, command=None, **kwargs):
|
|
5
|
+
self.idle_color = "#543996"
|
|
6
|
+
self.active_color = "#6121ff"
|
|
7
|
+
self.text_color = "#E6E1E5"
|
|
8
|
+
|
|
9
|
+
self.text = text # Сохраняем текст в self, чтобы он был доступен везде
|
|
10
|
+
self.value = value
|
|
11
|
+
self.variable = variable
|
|
12
|
+
self.command = command
|
|
13
|
+
|
|
14
|
+
# Параметры анимации точки
|
|
15
|
+
self.dot_size = 0.0 # 0.0 - нет точки, 1.0 - полная точка
|
|
16
|
+
|
|
17
|
+
kwargs.setdefault('height', 28)
|
|
18
|
+
kwargs.setdefault('width', 180)
|
|
19
|
+
kwargs.setdefault('highlightthickness', 0)
|
|
20
|
+
kwargs.setdefault('bg', master.cget('bg'))
|
|
21
|
+
super().__init__(master, **kwargs)
|
|
22
|
+
|
|
23
|
+
self._draw()
|
|
24
|
+
self.bind("<Button-1>", self._on_click)
|
|
25
|
+
|
|
26
|
+
if self.variable:
|
|
27
|
+
self.variable.trace_add("write", lambda *args: self._animate_toggle())
|
|
28
|
+
|
|
29
|
+
def _draw(self):
|
|
30
|
+
self.delete("all")
|
|
31
|
+
h = int(self.cget('height'))
|
|
32
|
+
self.r = 10
|
|
33
|
+
self.center_y = h / 2
|
|
34
|
+
self.center_x = 15
|
|
35
|
+
|
|
36
|
+
# Внешнее кольцо
|
|
37
|
+
self.outer = self.create_oval(
|
|
38
|
+
self.center_x - self.r, self.center_y - self.r,
|
|
39
|
+
self.center_x + self.r, self.center_y + self.r,
|
|
40
|
+
outline=self.idle_color, width=2
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Внутренняя точка (теперь она будет менять размер)
|
|
44
|
+
self.inner = self.create_oval(0, 0, 0, 0, fill=self.active_color, outline="")
|
|
45
|
+
|
|
46
|
+
# Текст (теперь берем из self.text)
|
|
47
|
+
self.create_text(35, self.center_y, text=self.text, fill=self.text_color,
|
|
48
|
+
font=("Segoe UI", 10), anchor="w")
|
|
49
|
+
|
|
50
|
+
self._update_ui()
|
|
51
|
+
|
|
52
|
+
def _animate_toggle(self):
|
|
53
|
+
# Логика LERP для размера точки
|
|
54
|
+
is_selected = self.variable.get() == self.value
|
|
55
|
+
target = 1.0 if is_selected else 0.0
|
|
56
|
+
|
|
57
|
+
diff = target - self.dot_size
|
|
58
|
+
if abs(diff) > 0.05:
|
|
59
|
+
self.dot_size += diff * 0.2
|
|
60
|
+
self._update_ui()
|
|
61
|
+
self.after(16, self._animate_toggle)
|
|
62
|
+
else:
|
|
63
|
+
self.dot_size = target
|
|
64
|
+
self._update_ui()
|
|
65
|
+
|
|
66
|
+
def _update_ui(self):
|
|
67
|
+
# Меняем цвет кольца
|
|
68
|
+
color = self.active_color if self.variable.get() == self.value else self.idle_color
|
|
69
|
+
self.itemconfig(self.outer, outline=color)
|
|
70
|
+
|
|
71
|
+
# Меняем размер точки
|
|
72
|
+
current_r = (self.r - 4) * self.dot_size
|
|
73
|
+
if current_r > 0:
|
|
74
|
+
self.coords(self.inner,
|
|
75
|
+
self.center_x - current_r, self.center_y - current_r,
|
|
76
|
+
self.center_x + current_r, self.center_y + current_r)
|
|
77
|
+
self.itemconfig(self.inner, state="normal")
|
|
78
|
+
else:
|
|
79
|
+
self.itemconfig(self.inner, state="hidden")
|
|
80
|
+
|
|
81
|
+
def _on_click(self, event):
|
|
82
|
+
if self.variable:
|
|
83
|
+
self.variable.set(self.value)
|
|
84
|
+
if self.command:
|
|
85
|
+
self.command()
|
m_you/slider.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
|
|
3
|
+
class MaterialSlider(tk.Canvas):
|
|
4
|
+
def __init__(self, master, from_=0, to=100, step=1, value=None, **kwargs):
|
|
5
|
+
# 1. Определяем ширину: берем из kwargs или ставим 300 по дефолту
|
|
6
|
+
self.width_val = kwargs.get('width', 300)
|
|
7
|
+
|
|
8
|
+
# Настройки Canvas
|
|
9
|
+
kwargs.setdefault('height', 70)
|
|
10
|
+
kwargs.setdefault('highlightthickness', 0)
|
|
11
|
+
kwargs.setdefault('bg', '#1C1B1F')
|
|
12
|
+
super().__init__(master, **kwargs)
|
|
13
|
+
|
|
14
|
+
# Параметры
|
|
15
|
+
self.from_ = from_
|
|
16
|
+
self.to = to
|
|
17
|
+
self.step = step
|
|
18
|
+
self.idle_color = "#543996" # Темный в покое
|
|
19
|
+
self.active_color = "#6121ff" # Твой яркий цвет при движении
|
|
20
|
+
self.track_color = "#38393E" # Убрал лишнюю запятую тут!
|
|
21
|
+
|
|
22
|
+
self.margin = 20
|
|
23
|
+
self.current_x = self.margin
|
|
24
|
+
self.target_x = self.margin
|
|
25
|
+
self.value = from_
|
|
26
|
+
|
|
27
|
+
# Рисуем трек
|
|
28
|
+
self.track = self.create_line(self.margin, 30, self.width_val - self.margin, 30,
|
|
29
|
+
width=16, capstyle='round', fill=self.track_color)
|
|
30
|
+
|
|
31
|
+
# Активная линия
|
|
32
|
+
self.active_line = self.create_line(self.margin, 30, self.margin, 30,
|
|
33
|
+
width=16, capstyle='round', fill=self.idle_color)
|
|
34
|
+
|
|
35
|
+
# Текст значения
|
|
36
|
+
self.label = self.create_text(self.margin, 55, text="",
|
|
37
|
+
fill="#E6E1E5", font=("Arial", 10, "bold"), anchor="w")
|
|
38
|
+
|
|
39
|
+
self.bind("<B1-Motion>", self._on_drag)
|
|
40
|
+
self.bind("<Button-1>", self._on_drag)
|
|
41
|
+
self.bind("<ButtonRelease-1>", lambda e: self.itemconfig(self.active_line, fill=self.idle_color))
|
|
42
|
+
|
|
43
|
+
self._animate()
|
|
44
|
+
|
|
45
|
+
# Установка начального значения
|
|
46
|
+
initial_val = value if value is not None else from_
|
|
47
|
+
self.set(initial_val)
|
|
48
|
+
|
|
49
|
+
def _on_drag(self, event):
|
|
50
|
+
self.itemconfig(self.active_line, fill=self.active_color)
|
|
51
|
+
self.target_x = max(self.margin, min(event.x, self.width_val - self.margin))
|
|
52
|
+
|
|
53
|
+
def _animate(self):
|
|
54
|
+
lerp_factor = 0.15
|
|
55
|
+
diff = self.target_x - self.current_x
|
|
56
|
+
|
|
57
|
+
if abs(diff) > 0.1:
|
|
58
|
+
self.current_x += diff * lerp_factor
|
|
59
|
+
self._update_ui(self.current_x)
|
|
60
|
+
|
|
61
|
+
self.after(16, self._animate)
|
|
62
|
+
|
|
63
|
+
def _update_ui(self, x):
|
|
64
|
+
self.coords(self.active_line, self.margin, 30, x, 30)
|
|
65
|
+
|
|
66
|
+
range_px = self.width_val - (2 * self.margin)
|
|
67
|
+
raw_pos = (x - self.margin) / range_px
|
|
68
|
+
|
|
69
|
+
# Защита от выхода за границы
|
|
70
|
+
raw_pos = max(0.0, min(raw_pos, 1.0))
|
|
71
|
+
|
|
72
|
+
val = self.from_ + raw_pos * (self.to - self.from_)
|
|
73
|
+
|
|
74
|
+
if self.step:
|
|
75
|
+
self.value = round(val / self.step) * self.step
|
|
76
|
+
else:
|
|
77
|
+
self.value = val
|
|
78
|
+
|
|
79
|
+
display_text = int(self.value) if self.value == int(self.value) else round(self.value, 1)
|
|
80
|
+
self.itemconfig(self.label, text=str(display_text))
|
|
81
|
+
|
|
82
|
+
def get(self):
|
|
83
|
+
return self.value
|
|
84
|
+
|
|
85
|
+
def set(self, val):
|
|
86
|
+
# Ограничиваем значение
|
|
87
|
+
val = max(self.from_, min(val, self.to))
|
|
88
|
+
self.value = val
|
|
89
|
+
|
|
90
|
+
# Рассчитываем целевой X
|
|
91
|
+
range_px = self.width_val - (2 * self.margin)
|
|
92
|
+
percent = (val - self.from_) / (self.to - self.from_)
|
|
93
|
+
self.target_x = self.margin + (percent * range_px)
|
|
94
|
+
|
|
95
|
+
# Обновляем текст сразу
|
|
96
|
+
display_text = int(self.value) if self.value == int(self.value) else round(self.value, 1)
|
|
97
|
+
self.itemconfig(self.label, text=str(display_text))
|
m_you/switch.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
|
|
3
|
+
class MaterialSwitch(tk.Canvas):
|
|
4
|
+
def __init__(self, master, text="", width=250, **kwargs):
|
|
5
|
+
# Цвета Material You
|
|
6
|
+
self.track_off = "#49454F"
|
|
7
|
+
self.track_on = "#381E72"
|
|
8
|
+
self.thumb_off = "#938F99"
|
|
9
|
+
self.thumb_on = "#D0BCFF"
|
|
10
|
+
self.text_color = "#E6E1E5"
|
|
11
|
+
|
|
12
|
+
self.is_on = False
|
|
13
|
+
self.anim_p = 0.0
|
|
14
|
+
self.after_id = None
|
|
15
|
+
|
|
16
|
+
kwargs.setdefault('height', 48)
|
|
17
|
+
kwargs.setdefault('width', width)
|
|
18
|
+
kwargs.setdefault('highlightthickness', 0)
|
|
19
|
+
kwargs.setdefault('bg', master.cget('bg'))
|
|
20
|
+
super().__init__(master, **kwargs)
|
|
21
|
+
|
|
22
|
+
# 1. Подложка (Track) — капсула
|
|
23
|
+
# Координаты: x=10, y=12 до x=62, y=36 (стандартный размер свитча)
|
|
24
|
+
self.track_id = self.create_oval(10, 12, 34, 36, fill=self.track_off, outline="", tags="target")
|
|
25
|
+
self.track_rect = self.create_rectangle(22, 12, 50, 38, fill=self.track_off, outline="", tags="target")
|
|
26
|
+
self.track_end = self.create_oval(38, 12, 62, 36, fill=self.track_off, outline="", tags="target")
|
|
27
|
+
|
|
28
|
+
# 2. Ползунок (Thumb)
|
|
29
|
+
# В выключенном состоянии он меньше и серый
|
|
30
|
+
self.thumb_id = self.create_oval(16, 18, 30, 32, fill=self.thumb_off, outline="", tags="target")
|
|
31
|
+
|
|
32
|
+
# 3. Текст
|
|
33
|
+
self.create_text(74, 24, text=text, fill=self.text_color,
|
|
34
|
+
font=("Segoe UI", 11), anchor="w", tags="target")
|
|
35
|
+
|
|
36
|
+
# Бинды
|
|
37
|
+
self.tag_bind("target", "<Button-1>", lambda e: self.toggle())
|
|
38
|
+
|
|
39
|
+
def toggle(self):
|
|
40
|
+
self.is_on = not self.is_on
|
|
41
|
+
target = 1.0 if self.is_on else 0.0
|
|
42
|
+
self._animate(target)
|
|
43
|
+
|
|
44
|
+
def _animate(self, target):
|
|
45
|
+
if self.after_id: self.after_cancel(self.after_id)
|
|
46
|
+
step = 0.2
|
|
47
|
+
diff = target - self.anim_p
|
|
48
|
+
if abs(diff) > 0.01:
|
|
49
|
+
self.anim_p += diff * step
|
|
50
|
+
self._update_ui()
|
|
51
|
+
self.after_id = self.after(10, lambda: self._animate(target))
|
|
52
|
+
else:
|
|
53
|
+
self.anim_p = target
|
|
54
|
+
self._update_ui()
|
|
55
|
+
|
|
56
|
+
def _lerp_color(self, c1, c2, p):
|
|
57
|
+
def to_rgb(h): return tuple(int(h.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
|
|
58
|
+
rgb1, rgb2 = to_rgb(c1), to_rgb(c2)
|
|
59
|
+
res = tuple(int(rgb1[i] + (rgb2[i] - rgb1[i]) * p) for i in range(3))
|
|
60
|
+
return "#%02x%02x%02x" % res
|
|
61
|
+
|
|
62
|
+
def _update_ui(self):
|
|
63
|
+
p = self.anim_p
|
|
64
|
+
|
|
65
|
+
# Цвета
|
|
66
|
+
t_color = self._lerp_color(self.track_off, self.track_on, p)
|
|
67
|
+
thumb_color = self._lerp_color(self.thumb_off, self.thumb_on, p)
|
|
68
|
+
|
|
69
|
+
for tid in [self.track_id, self.track_rect, self.track_end]:
|
|
70
|
+
self.itemconfig(tid, fill=t_color)
|
|
71
|
+
self.itemconfig(self.thumb_id, fill=thumb_color)
|
|
72
|
+
|
|
73
|
+
# Движение и размер ручки
|
|
74
|
+
# Выкл: x=16, size=14. Вкл: x=40, size=20
|
|
75
|
+
size = 14 + (6 * p)
|
|
76
|
+
x_base = 16 + (24 * p)
|
|
77
|
+
y_center = 24
|
|
78
|
+
self.coords(self.thumb_id, x_base, y_center - size/2, x_base + size, y_center + size/2)
|
|
79
|
+
|
|
80
|
+
def get(self):
|
|
81
|
+
return self.is_on
|
m_you/toast.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
|
|
3
|
+
class MaterialToast(tk.Canvas):
|
|
4
|
+
def __init__(self, master, message="Action completed", duration=2500, **kwargs):
|
|
5
|
+
# Цвета из Button
|
|
6
|
+
self.bg_color = "#543996"
|
|
7
|
+
self.text_color = "#EADDFF"
|
|
8
|
+
|
|
9
|
+
self.master = master
|
|
10
|
+
self.duration = duration
|
|
11
|
+
|
|
12
|
+
# 1. Адаптивный расчет ширины
|
|
13
|
+
# Примерно 10 пикселей на символ + отступы по бокам
|
|
14
|
+
self.width_val = len(message) * 9 + 60
|
|
15
|
+
# Ограничиваем, чтобы не был слишком узким или широким
|
|
16
|
+
self.width_val = max(200, min(self.width_val, master.winfo_width() - 40))
|
|
17
|
+
|
|
18
|
+
kwargs.setdefault('height', 48)
|
|
19
|
+
kwargs.setdefault('highlightthickness', 0)
|
|
20
|
+
kwargs.setdefault('bg', master.cget('bg'))
|
|
21
|
+
kwargs.setdefault('width', self.width_val)
|
|
22
|
+
super().__init__(master, **kwargs)
|
|
23
|
+
|
|
24
|
+
# Рисуем скругленную капсулу
|
|
25
|
+
r = 24 # Полное скругление для высоты 48
|
|
26
|
+
self.create_oval(0, 0, r*2, 48, fill=self.bg_color, outline="")
|
|
27
|
+
self.create_oval(self.width_val-r*2, 0, self.width_val, 48, fill=self.bg_color, outline="")
|
|
28
|
+
self.create_rectangle(r, 0, self.width_val-r, 48, fill=self.bg_color, outline="")
|
|
29
|
+
|
|
30
|
+
# Текст сообщения по центру
|
|
31
|
+
self.create_text(self.width_val/2, 24, text=message, fill=self.text_color,
|
|
32
|
+
font=("Segoe UI", 10, "bold"), anchor="center")
|
|
33
|
+
|
|
34
|
+
self.is_showing = False
|
|
35
|
+
|
|
36
|
+
def show(self):
|
|
37
|
+
if self.is_showing: return
|
|
38
|
+
self.is_showing = True
|
|
39
|
+
|
|
40
|
+
# 2. Адаптивное позиционирование (всегда центр снизу)
|
|
41
|
+
# Обновляем координаты прямо перед показом на случай, если окно меняли
|
|
42
|
+
x = (self.master.winfo_width() - self.width_val) // 2
|
|
43
|
+
y_start = self.master.winfo_height() + 10
|
|
44
|
+
y_target = self.master.winfo_height() - 70 # Чуть ниже, чем раньше
|
|
45
|
+
|
|
46
|
+
self.place(x=x, y=y_start)
|
|
47
|
+
self._animate(y_start, y_target, "up")
|
|
48
|
+
|
|
49
|
+
# Автоматическое скрытие
|
|
50
|
+
self.master.after(self.duration, self.hide)
|
|
51
|
+
|
|
52
|
+
def _animate(self, curr_y, target_y, direction):
|
|
53
|
+
if not self.winfo_exists(): return
|
|
54
|
+
|
|
55
|
+
diff = target_y - curr_y
|
|
56
|
+
step = diff * 0.2 # Мягкий Easing
|
|
57
|
+
|
|
58
|
+
if abs(diff) > 0.5:
|
|
59
|
+
new_y = curr_y + step
|
|
60
|
+
self.place(y=new_y)
|
|
61
|
+
self.master.after(10, lambda: self._animate(new_y, target_y, direction))
|
|
62
|
+
else:
|
|
63
|
+
self.place(y=target_y)
|
|
64
|
+
if direction == "down":
|
|
65
|
+
self.place_forget()
|
|
66
|
+
self.destroy()
|
|
67
|
+
|
|
68
|
+
def hide(self):
|
|
69
|
+
if not self.winfo_exists(): return
|
|
70
|
+
y_curr = self.winfo_y()
|
|
71
|
+
y_end = self.master.winfo_height() + 50
|
|
72
|
+
self._animate(y_curr, y_end, "down")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: m-you-tk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Material You components for Tkinter
|
|
5
|
+
Author-email: logic-break <abibasqabiba@email.com>
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: Pillow>=9.0.0
|
|
10
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
m_you/__init__.py,sha256=3MBLtFXRAcNugU6qkIQRFEk8REzfN2eNomusNPYwiR8,1712
|
|
2
|
+
m_you/button.py,sha256=yGpWarwP9kzV6y5SatThmLuzcnZCozbW6tZQFo5hUus,2582
|
|
3
|
+
m_you/checkbox.py,sha256=EQ5JTlSy8Av0aLx6HahZaHc98PW7bwkIBjZHT8RdLtw,3979
|
|
4
|
+
m_you/dropdown.py,sha256=TtGu4dJI4rTF1kkLGw6936hFu6aa7qn9flPAYPAS2Es,7949
|
|
5
|
+
m_you/input.py,sha256=U-QPJXPzL_IJS6cJMYsQ3bo_RExvGbuo2lQcVdJyEWE,4952
|
|
6
|
+
m_you/radiobutton.py,sha256=Bp0RScGKmcYcsrT0KMk5rTGNT3xm6qa9DGFZgd9D4sQ,3311
|
|
7
|
+
m_you/slider.py,sha256=2C5vBlYDew3mTNZ-_MPwLk17BRTS7jo8ub2aAS7l92Y,3930
|
|
8
|
+
m_you/switch.py,sha256=SzkQ18WCjeFCM4_8LDmFdKb60VmOv1dgse3qbfv9kFU,3266
|
|
9
|
+
m_you/toast.py,sha256=b7PgQ-jD2yu-bxt-LIcugVCO-ZJ5fYOc9jXAvlhkRVM,3152
|
|
10
|
+
m_you/assets/material.ttf,sha256=7xSfCL3S_wmk4shXNHa3sPP7sVtiOVSt5ZiZ5xdb7do,356840
|
|
11
|
+
m_you_tk-0.1.0.dist-info/licenses/LICENSE,sha256=TDKOBOHBEBuzqtr9pyXbQFHpn25BV_RXV8jtZn_6k9w,809
|
|
12
|
+
m_you_tk-0.1.0.dist-info/METADATA,sha256=cXfJrPVuT19FbZ1lWMD8HRCu8XkvI8MSP0yt5VySByI,294
|
|
13
|
+
m_you_tk-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
14
|
+
m_you_tk-0.1.0.dist-info/top_level.txt,sha256=4nLhyeoOyjYFohyBd3kKqECvqrsYZ-qkzxE2RzZb2Qg,6
|
|
15
|
+
m_you_tk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Copyright (c) 2026 logic-break
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
m_you
|