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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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