extends VBoxContainer
## THE TEXTURE STRIP (hyper-games #94 P1 §9, first slice) — the bottom row of the
## BuildDock: search, pack filter, recent, and a horizontal run of thumbnails.
##
## A click makes a texture the player's CURRENT texture (map.set_current_tex), which
## every new brush face gets. U applies it to the selection and Y picks it from the
## aimed face; both live on the MAP, because the map owns the selection and every
## journal write — this panel never records anything (same rule as the outliner).
##
## Thumbnails are made LAZILY: a button starts blank and gets its 64 px preview
## only once it scrolls into view, a few per frame, so opening the strip does not
## decode 1196 textures. At most MAX_SHOWN buttons exist; a search narrows the rest.
const MAX_SHOWN := 240
const THUMB_PX := 64
const BUTTON_PX := 76
## Previews made per frame. Each is a texture decode + resize; four keeps a fast
## scroll smooth and fills a screenful in well under a second.
const THUMBS_PER_FRAME := 4
const RECENT_MAX := 12
var map: Node = null
var _search: LineEdit
var _pack: OptionButton
var _recent_btn: Button
var _current: Label
var _scroll: ScrollContainer
var _row: HBoxContainer
var _more: Label
var _search_text := ""
var _pack_id := "" # "" = every pack
var _recent: PackedStringArray = []
var _shown: PackedStringArray = []
var _hidden := 0
var _buttons: Dictionary = {} # id -> Button
var _pending: Array[String] = [] # ids whose button still has no preview
func setup(map_node: Node) -> void:
map = map_node
_build_ui()
_rebuild()
refresh_current()
func _build_ui() -> void:
# HFlow, not HBox: on a narrow window the controls wrap to a second line instead of
# forcing the whole bottom band wider than the gap between the side columns.
var top := HFlowContainer.new()
top.add_theme_constant_override("h_separation", 8)
add_child(top)
_search = LineEdit.new()
_search.placeholder_text = "search textures…"
_search.custom_minimum_size = Vector2(180, 0)
_search.text_changed.connect(func(t: String) -> void: set_filter(t, _pack_id))
top.add_child(_search)
_pack = OptionButton.new()
_pack.add_item("All packs")
_pack.set_item_metadata(0, "")
var packs: Array[String] = []
for id in TexturePacks.ids():
var p := id.get_slice("/", 0)
if not packs.has(p):
packs.append(p)
for p in packs:
_pack.add_item(p)
_pack.set_item_metadata(_pack.item_count - 1, p)
_pack.item_selected.connect(func(i: int) -> void: set_filter(_search_text, String(_pack.get_item_metadata(i))))
top.add_child(_pack)
_recent_btn = Button.new()
_recent_btn.text = "Recent"
_recent_btn.toggle_mode = true
_recent_btn.toggled.connect(func(_on: bool) -> void: _rebuild())
top.add_child(_recent_btn)
_current = Label.new()
_current.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_current.clip_text = true
top.add_child(_current)
var hint := Label.new()
hint.text = "click = current · U apply · Y pick"
hint.modulate = Color(1, 1, 1, 0.6)
top.add_child(hint)
_scroll = ScrollContainer.new()
_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
_scroll.custom_minimum_size = Vector2(0, BUTTON_PX + 18)
_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
add_child(_scroll)
_row = HBoxContainer.new()
_row.add_theme_constant_override("separation", 4)
_scroll.add_child(_row)
_more = Label.new()
_more.modulate = Color(1, 1, 1, 0.6)
add_child(_more)
## Narrow the strip. Either may be "" for "no constraint".
func set_filter(search: String, pack: String) -> void:
_search_text = search
_pack_id = pack
if _search != null and _search.text != search:
_search.text = search
_rebuild()
func shown_ids() -> PackedStringArray:
return _shown
## How many matches did not get a button (MAX_SHOWN); the strip says so.
func hidden_count() -> int:
return _hidden
func recent() -> PackedStringArray:
return _recent
## Make `id` the current texture — what a thumbnail click does.
func pick(id: String) -> void:
if map != null and map.has_method("set_current_tex"):
map.set_current_tex(id)
_note_recent(id)
## The map calls this when the current texture changes from elsewhere (Y).
func refresh_current() -> void:
var cur := ""
if map != null and map.has_method("current_face_tex"):
cur = String(map.current_face_tex().get("tex", ""))
if _current != null:
_current.text = "current: %s" % (cur if cur != "" else "—")
for id: String in _buttons:
var b: Button = _buttons[id]
if is_instance_valid(b):
b.button_pressed = id == cur
_note_recent(cur)
func _note_recent(id: String) -> void:
if id == "":
return
var i := _recent.find(id)
if i == 0:
return
if i > 0:
_recent.remove_at(i)
_recent.insert(0, id)
if _recent.size() > RECENT_MAX:
_recent.resize(RECENT_MAX)
func _matches(id: String) -> bool:
if _pack_id != "" and not id.begins_with(_pack_id + "/"):
return false
return _search_text == "" or id.to_lower().contains(_search_text.to_lower())
func _rebuild() -> void:
var source: PackedStringArray = _recent if (_recent_btn != null and _recent_btn.button_pressed) \
else TexturePacks.ids()
_shown = PackedStringArray()
_hidden = 0
for id in source:
if not _matches(id):
continue
if _shown.size() < MAX_SHOWN:
_shown.append(id)
else:
_hidden += 1
if _row == null:
return
for c in _row.get_children():
c.queue_free()
_buttons.clear()
_pending.clear()
var cur := ""
if map != null and map.has_method("current_face_tex"):
cur = String(map.current_face_tex().get("tex", ""))
for id in _shown:
var b := Button.new()
b.custom_minimum_size = Vector2(BUTTON_PX, BUTTON_PX)
b.expand_icon = true
b.toggle_mode = true
b.button_pressed = id == cur
b.tooltip_text = id
b.focus_mode = Control.FOCUS_NONE # never eat Tab OR the UV arrows (BuildDock's rule)
b.text = "" if TexturePacks.thumbnail_cached(id) else id.get_slice("/", 1).left(8)
b.icon = TexturePacks.thumbnail_cached(id)
b.pressed.connect(pick.bind(id))
_row.add_child(b)
_buttons[id] = b
if b.icon == null:
_pending.append(id)
_more.text = ("+%d more — refine the search" % _hidden) if _hidden > 0 else ""
_more.visible = _hidden > 0
func _process(_delta: float) -> void:
if _pending.is_empty() or not is_visible_in_tree():
return
var view := _scroll.get_global_rect()
var made := 0
var i := 0
while i < _pending.size() and made < THUMBS_PER_FRAME:
var id: String = _pending[i]
var b: Button = _buttons.get(id)
if b == null or not is_instance_valid(b):
_pending.remove_at(i)
continue
if not view.intersects(b.get_global_rect()):
i += 1
continue
b.icon = TexturePacks.thumbnail(id, THUMB_PX)
b.text = ""
_pending.remove_at(i)
made += 1