Files

213 lines
6.9 KiB
GDScript

class_name Button90
extends BaseButton
## Мягкая кнопка: высота 24, скругление 3, 1 px контур, тень только в покое.
## hover — бирюзовый контур и тёмный бирюзовый текст; нажатие — на 1 px внутрь.
## flat_style — пункт меню без рамки; active — мягкая teal-подсветка.
enum Icon { NONE, X, MIN, MAX, PLUS, MINUS, ARROW_UP, ARROW_DOWN, CHECK, LEFT, RIGHT }
@export var caption := "":
set(v):
caption = v
update_minimum_size()
queue_redraw()
@export var caption_size := Theme90.FS_BODY:
set(v):
caption_size = v
update_minimum_size()
queue_redraw()
@export var accent := false:
set(v):
accent = v
queue_redraw()
@export var danger := false:
set(v):
danger = v
queue_redraw()
@export var flat_style := false:
set(v):
flat_style = v
queue_redraw()
@export var icon: Icon = Icon.NONE:
set(v):
icon = v
update_minimum_size()
queue_redraw()
@export var active := false:
set(v):
active = v
queue_redraw()
@export var pad := 9.0
func _ready() -> void:
focus_mode = Control.FOCUS_ALL
mouse_default_cursor_shape = Control.CURSOR_ARROW
mouse_entered.connect(queue_redraw)
mouse_exited.connect(queue_redraw)
button_down.connect(queue_redraw)
button_up.connect(queue_redraw)
resized.connect(queue_redraw)
if not pressed.is_connected(_play_click):
pressed.connect(_play_click)
## Клик-звук UI. Audio — autoload; в отрыве от дерева (превью) молча пропускаем.
func _play_click() -> void:
var audio := get_node_or_null("/root/Audio")
if audio != null and audio.has_method("play_sfx"):
audio.play_sfx("click")
## Справочный размер под текст — раскладка задаёт размеры явно.
func preferred_size() -> Vector2:
if icon != Icon.NONE and caption.is_empty():
return Vector2(16, 16)
var f := Theme90.ui()
var w := ceilf(f.get_string_size(caption, HORIZONTAL_ALIGNMENT_LEFT, -1, caption_size).x) + pad * 2.0
return Vector2(maxf(w, 16.0), Theme90.H)
func set_caption(v: String) -> void:
caption = v
func _accent_border() -> Color:
return Color(Theme90.ACCENT.r, Theme90.ACCENT.g, Theme90.ACCENT.b, 0.55)
func _draw() -> void:
var r := Rect2(Vector2.ZERO, size)
var mode := get_draw_mode()
var dis := mode == BaseButton.DRAW_DISABLED or disabled
var pressed := mode == BaseButton.DRAW_PRESSED or mode == BaseButton.DRAW_HOVER_PRESSED
var hover := mode == BaseButton.DRAW_HOVER or mode == BaseButton.DRAW_HOVER_PRESSED
if toggle_mode and button_pressed:
pressed = true
var is_active := active or (toggle_mode and button_pressed)
var f := Theme90.ui()
if flat_style:
var bg := Color(0, 0, 0, 0)
if is_active:
bg = Theme90.ACCENT_PALE
elif hover and not dis:
bg = Color(1, 1, 1, 0.5)
if bg.a > 0.0:
draw_style_box(Theme90.box(bg, Color(0, 0, 0, 0), 3, 0), r)
var fg := Theme90.INK
if dis:
fg = Theme90.SHADE
elif is_active or hover:
fg = Theme90.NAVY
if is_active:
draw_rect(Rect2(r.position.x + 5, r.end.y - 2, maxf(0.0, r.size.x - 10), 2),
Theme90.ACCENT)
_draw_content(r, f, fg, pressed, dis)
return
var face := Theme90.PAPER_HI
var border := Theme90.BORDER
var fg := Theme90.INK
var shadow := Theme90.SHADOW
if dis:
face = Theme90.PAPER
border = Theme90.BORDER_SOFT
fg = Theme90.SHADE
shadow = Color(0, 0, 0, 0)
elif accent:
face = Theme90.ACCENT.darkened(0.06) if pressed else Theme90.ACCENT
border = Theme90.ACCENT.darkened(0.18)
fg = Theme90.WHITE
if hover:
face = Theme90.ACCENT.lightened(0.04)
elif danger:
face = Theme90.PAPER_HI
border = Color(Theme90.ERROR.r, Theme90.ERROR.g, Theme90.ERROR.b, 0.42)
fg = Theme90.ERROR
if hover:
face = Color(1, 1, 1, 1)
border = Theme90.ERROR
elif is_active:
face = Theme90.ACCENT_PALE
border = Color(Theme90.ACCENT.r, Theme90.ACCENT.g, Theme90.ACCENT.b, 0.65)
fg = Theme90.NAVY
if hover:
face = Theme90.ACCENT_PALE.lerp(Theme90.WHITE, 0.35)
else:
if hover:
face = Theme90.WHITE
border = _accent_border()
fg = Theme90.NAVY
if pressed and not dis:
face = face.darkened(0.05) if face.get_luminance() > 0.4 else face.darkened(0.1)
shadow = Color(0, 0, 0, 0)
if not (pressed and not dis):
draw_style_box(Theme90.box(face, border, 3, 1, shadow), r)
else:
draw_style_box(Theme90.box(face, border, 3, 1), r)
draw_line(Vector2(3, 1), Vector2(r.size.x - 3, 1), Color(0, 0, 0, 0.10), 1.0)
# мягкий верхний блик
if not pressed and not dis and face.get_luminance() > 0.55 and r.size.y >= 16.0:
draw_line(Vector2(4, 1), Vector2(r.size.x - 4, 1), Color(1, 1, 1, 0.55), 1.0)
# аккуратный фокус — тонкое кольцо внутри
if has_focus() and not dis:
draw_style_box(Theme90.box(Color(0, 0, 0, 0),
Color(Theme90.ACCENT.r, Theme90.ACCENT.g, Theme90.ACCENT.b, 0.75), 3, 1),
r.grow(-2.0))
_draw_content(r, f, fg, pressed, dis)
func _draw_content(r: Rect2, f: Font, fg: Color, pressed: bool, dis: bool) -> void:
var shift := Vector2(0, 1) if pressed and not dis else Vector2.ZERO
var icon_w := 0.0
if icon != Icon.NONE:
icon_w = 12.0
var total := 0.0
if not caption.is_empty():
total = f.get_string_size(caption, HORIZONTAL_ALIGNMENT_LEFT, -1, caption_size).x
total += icon_w + (4.0 if icon_w > 0.0 else 0.0)
var left := r.position.x + roundf((r.size.x - total) * 0.5) + shift.x
var x := left
var cy := roundf(r.get_center().y) + shift.y
if icon != Icon.NONE:
var ir := Rect2(x, cy - 6.0, 12.0, 12.0)
_draw_icon(ir, fg)
x += icon_w + 4.0
if not caption.is_empty():
var baseline := roundf(cy + f.get_ascent(caption_size) * 0.5 - 1.0)
draw_string(f, Vector2(x, baseline), caption, HORIZONTAL_ALIGNMENT_LEFT, -1, caption_size, fg)
func _draw_icon(ir: Rect2, fg: Color) -> void:
match icon:
Icon.X:
Theme90.draw_x(self, ir, fg, 1.2)
Icon.MIN:
draw_rect(Rect2(ir.position.x + 3, ir.get_center().y, 6, 1), fg)
Icon.MAX:
draw_style_box(Theme90.box(Color(0, 0, 0, 0), fg, 2, 1),
Rect2(ir.position.x + 3, ir.position.y + 3, 7, 7))
draw_rect(Rect2(ir.position.x + 3, ir.position.y + 3, 7, 2), fg)
Icon.PLUS:
draw_rect(Rect2(ir.get_center().x - 3, ir.get_center().y - 0.5, 7, 1.4), fg)
draw_rect(Rect2(ir.get_center().x - 0.5, ir.get_center().y - 3, 1.4, 7), fg)
Icon.MINUS:
draw_rect(Rect2(ir.get_center().x - 3, ir.get_center().y - 0.5, 7, 1.4), fg)
Icon.ARROW_UP:
Theme90.draw_chevron(self, ir.get_center(), 0, fg, 3.2, 1.4)
Icon.ARROW_DOWN:
Theme90.draw_chevron(self, ir.get_center(), 1, fg, 3.2, 1.4)
Icon.LEFT:
Theme90.draw_chevron(self, ir.get_center(), 3, fg, 3.2, 1.4)
Icon.RIGHT:
Theme90.draw_chevron(self, ir.get_center(), 2, fg, 3.2, 1.4)
Icon.CHECK:
draw_line(Vector2(ir.position.x + 2, ir.position.y + 6), Vector2(ir.position.x + 5, ir.position.y + 9), fg, 1.8)
draw_line(Vector2(ir.position.x + 5, ir.position.y + 9), Vector2(ir.position.x + 10, ir.position.y + 3), fg, 1.8)