This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this manual in English
Ta instrukcja wyjaśnia, jak tworzyć interaktywne elementy UI w edytorze przy użyciu skryptów edytora napisanych w Lua. Aby zacząć pracę ze skryptami edytora, zobacz instrukcję skryptów edytora. Pełne API edytora znajdziesz tutaj. Obecnie można tworzyć tylko interaktywne okna dialogowe, chociaż w przyszłości chcemy rozszerzyć obsługę skryptowego UI na resztę edytora.
Cała funkcjonalność związana z UI znajduje się w module editor.ui. Oto najprostszy przykład skryptu edytora z własnym UI na start:
local M = {}
function M.get_commands()
return {
{
label = "Do with confirmation",
locations = {"View"},
run = function()
local result = editor.ui.show_dialog(editor.ui.dialog({
title = "Perform action?",
buttons = {
editor.ui.dialog_button({
text = "Cancel",
cancel = true,
result = false
}),
editor.ui.dialog_button({
text = "Perform",
default = true,
result = true
})
}
}))
print('Perform action:', result)
end
}
}
end
return M
Ten fragment kodu definiuje polecenie View → Do with confirmation. Gdy je uruchomisz, zobaczysz następujące okno dialogowe:

Na końcu, po naciśnięciu Enter (albo kliknięciu przycisku Perform), w konsoli edytora zobaczysz następujący wiersz:
Perform action: true
Edytor udostępnia różne komponenty UI, które można składać, aby uzyskać pożądany interfejs. Zgodnie z konwencją wszystkie komponenty są konfigurowane pojedynczą tabelą o nazwie props. Same komponenty nie są tabelami, lecz niezmiennymi userdata używanymi przez edytor do tworzenia UI.
Props to tabele definiujące wejścia komponentów. Należy traktować je jako niezmienne: modyfikowanie tabeli props in-place nie spowoduje ponownego renderowania komponentu, ale użycie innej tabeli już tak. UI jest aktualizowane wtedy, gdy instancja komponentu otrzyma tabelę props, która w płytkim porównaniu nie jest równa poprzedniej.
Gdy komponent otrzyma jakiś obszar w UI, zajmie całą dostępną przestrzeń, ale nie oznacza to, że widoczna część komponentu się rozciągnie. Zamiast tego widoczna część zajmie tyle miejsca, ile potrzebuje, a następnie zostanie wyrównana w obrębie przydzielonego obszaru. Dlatego większość wbudowanych komponentów definiuje pole alignment.
Na przykład rozważ ten komponent etykiety:
editor.ui.label({
text = "Hello",
alignment = editor.ui.ALIGNMENT.RIGHT
})
Widoczna część to tekst Hello, a w obrębie przydzielonego obszaru komponentu jest on wyrównany tak:

Edytor definiuje różne wbudowane komponenty, których można używać razem do budowania UI. Komponenty można z grubsza podzielić na 3 kategorie: układ, prezentacja danych i wejście.
Komponenty układu służą do umieszczania innych komponentów obok siebie. Główne komponenty układu to horizontal, vertical i grid. Komponenty te definiują też pola takie jak padding i spacing, gdzie padding oznacza pustą przestrzeń od krawędzi przydzielonego obszaru do zawartości, a spacing pustą przestrzeń między elementami potomnymi:

Edytor definiuje small, medium i large stałe dla paddingu i spacingu. W przypadku spacingu wartość small jest przeznaczona do odstępów między różnymi podelementami pojedynczego elementu UI, medium do odstępów między poszczególnymi elementami UI, a large do odstępów między grupami elementów. Domyślny spacing to medium. Dla paddingu wartość large oznacza odstęp od krawędzi okna do zawartości, medium odstęp od krawędzi istotnego elementu UI, a small odstęp od krawędzi małych elementów UI, takich jak menu kontekstowe i podpowiedzi, które nie są jeszcze zaimplementowane.
Kontener horizontal umieszcza swoje elementy potomne jeden po drugim w poziomie, zawsze rozciągając wysokość każdego elementu potomnego tak, aby wypełniała dostępną przestrzeń. Domyślnie szerokość każdego elementu potomnego jest utrzymywana na minimalnym poziomie, ale można sprawić, by zajmował tyle miejsca, ile się da, ustawiając w nim pole grow na true.
Kontener vertical jest podobny do horizontal, ale z zamienionymi osiami.
Na koniec, grid to komponent kontenera, który układa elementy potomne w dwuwymiarowej siatce, podobnie jak tabela. Ustawienie grow w siatce dotyczy wierszy albo kolumn, dlatego ustawia się je nie na elemencie potomnym, ale w tabeli konfiguracji kolumny. Dodatkowo elementy potomne w siatce można skonfigurować tak, aby zajmowały wiele wierszy lub kolumn za pomocą pól row_span i column_span. Siatki są przydatne przy tworzeniu formularzy z wieloma polami wejściowymi:
editor.ui.grid({
padding = editor.ui.PADDING.LARGE, -- dodaj padding wokół krawędzi dialogu
columns = {{}, {grow = true}}, -- spraw, by druga kolumna się rozciągała
children = {
{
editor.ui.label({
text = "Level Name",
alignment = editor.ui.ALIGNMENT.RIGHT
}),
editor.ui.string_field({})
},
{
editor.ui.label({
text = "Author",
alignment = editor.ui.ALIGNMENT.RIGHT
}),
editor.ui.string_field({})
}
}
})
Powyższy kod utworzy następujący formularz w oknie dialogowym:

Edytor definiuje 4 komponenty prezentacji danych:
label — etykieta tekstowa przeznaczona do używania z polami formularzy.icon — ikona; obecnie można jej używać tylko do prezentowania niewielkiego zestawu predefiniowanych ikon, ale w przyszłości chcemy dopuścić więcej ikon.heading — element tekstowy przeznaczony do wyświetlania wiersza nagłówka, na przykład w formularzu lub oknie dialogowym. Enum editor.ui.HEADING_STYLE definiuje różne style nagłówków, w tym nagłówki H1-H6 z HTML-a, a także specyficzne dla edytora DIALOG i FORM.paragraph — element tekstowy przeznaczony do wyświetlania akapitu tekstu. Główna różnica względem label polega na tym, że paragraph obsługuje zawijanie wierszy: jeśli przydzielony obszar jest zbyt wąski, tekst zostanie zawinięty, a jeśli nadal nie zmieści się w widoku, zostanie ewentualnie skrócony do "...".Komponenty wejściowe służą do interakcji użytkownika z UI. Wszystkie komponenty wejściowe obsługują pole enabled, które kontroluje, czy interakcja jest włączona, oraz definiują różne callbacki powiadamiające skrypt edytora o interakcji.
Jeśli tworzysz statyczne UI, wystarczy zdefiniować callbacki, które po prostu modyfikują zmienne lokalne. W przypadku dynamicznych interfejsów i bardziej zaawansowanych interakcji zobacz sekcję reaktywność.
Na przykład można tak utworzyć proste, statyczne okno dialogowe tworzenia nowego pliku:
-- początkowa nazwa pliku, zostanie zastąpiona przez dialog
local file_name = ""
local create_file = editor.ui.show_dialog(editor.ui.dialog({
title = "Utwórz nowy plik",
content = editor.ui.horizontal({
padding = editor.ui.PADDING.LARGE,
spacing = editor.ui.SPACING.MEDIUM,
children = {
editor.ui.label({
text = "Nazwa nowego pliku",
alignment = editor.ui.ALIGNMENT.CENTER
}),
editor.ui.string_field({
grow = true,
text = file_name,
-- Callback wywoływany podczas wpisywania:
on_value_changed = function(new_text)
file_name = new_text
end
})
}
}),
buttons = {
editor.ui.dialog_button({ text = "Anuluj", cancel = true, result = false }),
editor.ui.dialog_button({ text = "Utwórz plik", default = true, result = true })
}
}))
if create_file then
print("create", file_name)
end
Oto lista wbudowanych komponentów wejściowych:
string_field, integer_field i number_field to warianty jednoliniowego pola tekstowego, które pozwalają edytować odpowiednio łańcuchy znaków, liczby całkowite i liczby.select_box służy do wybierania opcji z predefiniowanej tablicy za pomocą listy rozwijanej.check_box to logiczne pole wejściowe z callbackiem on_value_changed.button z callbackiem on_press, który jest wywoływany po naciśnięciu przycisku.external_file_field to komponent przeznaczony do wybierania ścieżki do pliku na komputerze. Składa się z pola tekstowego i przycisku otwierającego okno wyboru pliku.resource_field to komponent przeznaczony do wybierania zasobu w projekcie.Wszystkie komponenty poza przyciskami pozwalają ustawić pole issue, które wyświetla problem powiązany z komponentem, czyli editor.ui.ISSUE_SEVERITY.ERROR albo editor.ui.ISSUE_SEVERITY.WARNING, na przykład:
issue = {severity = editor.ui.ISSUE_SEVERITY.WARNING, message = "Ta wartość jest przestarzała"}
Gdy issue jest określone, zmienia wygląd komponentu wejściowego i dodaje podpowiedź z komunikatem problemu.
Oto demonstracja wszystkich pól wejściowych wraz z ich wariantami issue:

Aby wyświetlić okno dialogowe, musisz użyć funkcji editor.ui.show_dialog. Oczekuje ona komponentu dialog, który definiuje główną strukturę dialogów Defold: title, header, content i buttons. Komponent dialog jest trochę wyjątkowy: nie można użyć go jako elementu potomnego innego komponentu, ponieważ reprezentuje okno, a nie element UI. header i content są jednak zwykłymi komponentami.
Przyciski dialogowe też są szczególne: tworzy się je za pomocą komponentu dialog_button. W odróżnieniu od zwykłych przycisków przyciski dialogowe nie mają callbacku on_pressed. Zamiast tego definiują pole result z wartością, którą funkcja editor.ui.show_dialog zwróci po zamknięciu dialogu. Przyciski dialogowe definiują też logiczne pola cancel i default: przycisk z polem cancel jest uruchamiany, gdy użytkownik naciśnie Escape albo zamknie dialog przyciskiem zamykania systemu operacyjnego, a przycisk default jest uruchamiany, gdy użytkownik naciśnie Enter. Przycisk dialogowy może mieć jednocześnie ustawione cancel i default na true.
Dodatkowo edytor definiuje kilka komponentów pomocniczych:
separator to cienka linia używana do oddzielania bloków zawartościscroll to komponent opakowujący, który pokazuje paski przewijania, gdy opakowany komponent nie mieści się w przydzielonej przestrzeniPonieważ komponenty są niezmiennymi userdata, po ich utworzeniu nie da się ich zmieniać. Jak więc sprawić, żeby UI zmieniało się w czasie? Odpowiedź: komponenty reaktywne.
UI skryptów edytora czerpie inspirację z biblioteki React, więc wiedza o reaktywnym UI i hookach Reacta będzie pomocna.
Najprościej mówiąc, komponent reaktywny to komponent z funkcją Lua, która otrzymuje dane (props) i zwraca widok, czyli inny komponent. Funkcja komponentu reaktywnego może używać hooków: specjalnych funkcji w module editor.ui, które dodają komponentom cechy reaktywne. Zgodnie z konwencją wszystkie hooki mają nazwy zaczynające się od use_.
Aby utworzyć komponent reaktywny, użyj funkcji editor.ui.component().
Spójrzmy na przykład: okno dialogowe tworzenia nowego pliku, które pozwala utworzyć plik tylko wtedy, gdy wpisana nazwa pliku nie jest pusta:
-- 1. dialog jest komponentem reaktywnym
local dialog = editor.ui.component(function(props)
-- 2. komponent definiuje lokalny stan, czyli nazwę pliku, która domyślnie jest pustym ciągiem
local name, set_name = editor.ui.use_state("")
return editor.ui.dialog({
title = props.title,
content = editor.ui.vertical({
padding = editor.ui.PADDING.LARGE,
children = {
editor.ui.string_field({
value = name,
-- 3. wpisywanie i Enter aktualizują lokalny stan
on_value_changed = set_name
})
}
}),
buttons = {
editor.ui.dialog_button({
text = "Anuluj",
cancel = true
}),
editor.ui.dialog_button({
text = "Utwórz plik",
-- 4. tworzenie jest włączone, gdy nazwa nie jest pusta
enabled = name ~= "",
default = true,
-- 5. wynikiem jest nazwa
result = name
})
}
})
end)
-- 6. show_dialog zwróci albo niepustą nazwę pliku, albo nil po anulowaniu
local file_name = editor.ui.show_dialog(dialog({ title = "Nazwa nowego pliku" }))
if file_name then
print("create " .. file_name)
else
print("cancelled")
end
Gdy uruchomisz polecenie menu wykonujące ten kod, edytor pokaże na początku dialog z wyłączonym przyciskiem <kbd>Create File</kbd>, ale gdy wpiszesz nazwę i naciśniesz Enter, przycisk stanie się aktywny:

Jak to działa? Przy pierwszym renderowaniu hook use_state tworzy lokalny stan powiązany z komponentem i zwraca go wraz z setterem tego stanu. Gdy funkcja settera zostanie wywołana, planuje ponowne renderowanie komponentu. Podczas kolejnych renderowań funkcja komponentu jest wywoływana ponownie, a use_state zwraca zaktualizowany stan. Nowy komponent widoku zwrócony przez funkcję komponentu jest następnie porównywany z poprzednim, a UI jest aktualizowane tam, gdzie wykryto zmiany.
Takie reaktywne podejście bardzo upraszcza budowanie interaktywnych interfejsów i utrzymywanie ich w synchronizacji: zamiast jawnie aktualizować wszystkie dotknięte komponenty UI po danych wejściowych użytkownika, definiujesz widok jako czystą funkcję danych wejściowych (props i stanu lokalnego), a edytor sam obsługuje wszystkie aktualizacje.
Edytor oczekuje, że reaktywne komponenty funkcyjne będą zachowywać się poprawnie, żeby to działało:
Jeśli znasz React, zauważysz, że hooki w edytorze mają nieco inną semantykę, jeśli chodzi o zależności hooków.
Edytor definiuje 2 hooki: use_memo i use_state.
use_state
Lokalny stan można utworzyć na 2 sposoby: z wartością domyślną albo z funkcją inicjalizującą:
-- wartość domyślna
local enabled, set_enabled = editor.ui.use_state(true)
-- funkcja inicjalizująca + argumenty
local id, set_id = editor.ui.use_state(string.lower, props.name)
Podobnie setter można wywołać z nową wartością albo funkcją aktualizującą:
-- funkcja aktualizująca
local function increment_by(n, by)
return n + by
end
local counter = editor.ui.component(function(props)
local count, set_count = editor.ui.use_state(0)
return editor.ui.horizontal({
spacing = editor.ui.SPACING.SMALL,
children = {
editor.ui.label({
text = tostring(count),
alignment = editor.ui.ALIGNMENT.LEFT,
grow = true
}),
editor.ui.text_button({
text = "+1",
on_pressed = function() set_count(increment_by, 1) end
}),
editor.ui.text_button({
text = "+5",
on_pressed = function() set_count(increment_by, 5) end
})
}
})
end)
Na koniec stan może zostać zresetowany. Dochodzi do tego, gdy zmieni się którykolwiek z argumentów przekazywanych do editor.ui.use_state(), sprawdzanych przez ==. Z tego powodu nie wolno używać literałów tabel ani literałowych funkcji inicjalizujących jako argumentów haka use_state, bo spowoduje to reset stanu przy każdym ponownym renderowaniu. Dla zobrazowania:
-- ❌ ŹLE: literał tabeli w inicjalizatorze powoduje reset stanu przy każdym ponownym renderowaniu
local user, set_user = editor.ui.use_state({ first_name = props.first_name, last_name = props.last_name})
-- ✅ DOBRZE: użyj funkcji inicjalizującej poza funkcją komponentu, aby utworzyć stan tabeli
local function create_user(first_name, last_name)
return { first_name = first_name, last_name = last_name}
end
-- ...później, wewnątrz funkcji komponentu:
local user, set_user = editor.ui.use_state(create_user, props.first_name, props.last_name)
-- ❌ ŹLE: literał funkcji inicjalizującej powoduje reset stanu przy każdym ponownym renderowaniu
local id, set_id = editor.ui.use_state(function() return string.lower(props.name) end)
-- ✅ DOBRZE: użyj referencji do funkcji inicjalizującej, aby utworzyć stan
local id, set_id = editor.ui.use_state(string.lower, props.name)
use_memo
Możesz użyć haka use_memo, aby poprawić wydajność. W funkcjach renderujących często wykonuje się pewne obliczenia, na przykład sprawdzanie poprawności danych wejściowych użytkownika. Haka use_memo można użyć wtedy, gdy sprawdzenie, czy argumenty funkcji obliczeniowej się zmieniły, jest tańsze niż samo wywołanie tej funkcji. Hak wywoła funkcję obliczeniową przy pierwszym renderowaniu i ponownie wykorzysta obliczoną wartość podczas kolejnych renderowań, jeśli wszystkie argumenty use_memo pozostaną bez zmian:
-- funkcja walidująca poza funkcją komponentu
local function validate_password(password)
if #password < 8 then
return false, "Hasło musi mieć co najmniej 8 znaków."
elseif not password:match("%l") then
return false, "Hasło musi zawierać co najmniej jedną małą literę."
elseif not password:match("%u") then
return false, "Hasło musi zawierać co najmniej jedną wielką literę."
elseif not password:match("%d") then
return false, "Hasło musi zawierać co najmniej jedną cyfrę."
else
return true, "Hasło jest poprawne."
end
end
-- ...później, wewnątrz funkcji komponentu
local username, set_username = editor.ui.use_state('')
local password, set_password = editor.ui.use_state('')
local valid, message = editor.ui.use_memo(validate_password, password)
W tym przykładzie walidacja hasła wykona się przy każdej zmianie hasła, na przykład podczas wpisywania w polu hasła, ale nie wtedy, gdy zmieni się nazwa użytkownika.
Innym zastosowaniem haka use_memo jest tworzenie callbacków, które są potem używane w komponentach wejściowych, albo sytuacje, gdy lokalnie utworzona funkcja jest używana jako wartość props innego komponentu. To zapobiega niepotrzebnym ponownym renderowaniom.