위젯은 화면의 일정 영역을 관리하는 역할을 담당하는 구성 요소라고 정의된다. 위젯은 앱이 하는 것과 똑같은 방식으로 이벤트에 응답하고, 하위 위젯을 포함하여 계층 구조를 이룰 수도 있으므로 일종의 미니앱이라고 생각할 수 있다.
DOM 쿼리
Textual에서 위젯의 UI 스타일은 CSS를 통해서 설정하게 된다. 해석한 CSS 내의 특정한 스타일을 위젯에 적용하기 위해서는 CSS 셀렉터로부터 그에 매칭하는 위젯을 선택할 수 있어야 함을 의미한다. 따라서 Textual에는 웹브라우저와 마찬가지로 DOM 쿼리를 통해 요소를 찾는 기능을 기본적으로 제공한다. query_*
로 시작하는 메소드들을 사용하는데, 가장 많이 사용하는 메소드는 query_one
일 것인데, 다음과 같이 사용한다.
query_one(TextArea)
: 해당 위젯 하위의 TextArea 요소를 하나 찾아낸다. 리턴된 객체는 TextArea 타입으로 간주된다.query_one("#editor", TextArea)
:id
속성이 “editor”인 요소를 찾는다. 리턴된 객체는 TextArea 타입으로 간주된다.query_one(".view", TextArea)
: 클래스 속성에.view
를 포함하고 있는 요소를 찾는다. 리턴된 객체는 TextArea 타입으로 간주된다.
textual은 공식 가이드 문서의 예제에서도 타입 어노테이션을 모두 달아놓을만큼 타입 관리에 진심인 편이라, 사용하는 측면에서도 타이핑에 의한 편익을 개발자들이 누릴 수 있도록 배려하고 있다.
query_one()을 제외한, query()
, query_children()
은 복수의 DOM 객체를 탐색하여 반환하는데, 실제로는 리스트와 유사한 DOMQuery
라는 객체를 리턴한다. 만약 query()
를 아무런 셀렉터 없이 호출한다면, 해당 객체의 모든 하위 요소들을 포함한 DOMQuery
객체를 리턴받게 된다.
for widget in self.query():
print(widget)
for buton in self.query("Button"):
print(button)
for button in self.query("Dialog Button.disabled"):
print(button)
for button in self.query(".disabled").results(Button):
print(button)
DOMQuery는 리스트를 서브클래싱한 것마냥, 리스트가 제공하는 인터페이스를 모두 사용할 수 있고, 그 외의 몇 가지 부가 기능을 제공한다. 일견 jQuery와 좀 비슷하다. 포함된 DOM요소들 중에서 특정한 타입만 골라내어 루프를 통해 순회하는 등의 처리를 할 수 있으며, 명시적으로 루프를 돌지 않더라도 포함된 모든 요소들의 속성을 한꺼번에 조작하는 기능도 지원한다.
DOM쿼리 객체는 results()
라는 메소드를 제공하며, 이 결과는 위젯들을 순회할 때 사용할 수 있다. 여기에 타입을 지정해주면, 해당 타입의 결과만 필터링한다. 위 예제에서 마지막 코드가 여기에 해당한다. result()
는 특정한 타입으로 필터링하며, filter()
, exclude()
는 셀렉터 문자열로 포함된 요소들을 필터링한다는 차이점이 있다. 이 때 필터링한 결과도 모두 DOMQuery
타입이다.
results([Type])
first([Type])
,last([Type])
filter([Selector])
exclude([Selector])
DOM쿼리는 jQuery와 비슷하게 일부 속성들을 루프를 돌지않고 한 번에 간편하게 변경하는 기능을 제공하기도 한다.
add_class()
,remove_class()
,set_class()
,toggle_class()
focus()
,blur()
,refresh()
remove()
set(attr, value)
위젯의 확장
기본적으로 fundamental한 UI 요소는 대부분 기본적으로 제공이 되기 때문에, 새로운 위젯이 필요한 경우는 다음의 두 경우가 대부분이다.
- 기본 위젯 몇 가지가 결합되어 하나의 요소처럼 다룰 수 있는 위젯이 필요할 때 -> 위젯을 합성
- 기존 위젯에 추가적인 기능이 필요할 때 -> 기존 위젯을 확장
예를 들어 TODO 앱 같은 걸을 만들 거나 할 때에는 Checkbox
객체와 Input
객체는 항상 하나의 단위로 붙어다니게 될 것이다. 이런 경우, 그 상위의 컨테이너를 확장하고, compose()
를 작성하여 체크박스와 입력필드를 하위 위젯으로 만들고 이들을 하나의 단위처럼 사용할 수 있다. Textual의 공식 튜토리얼에서도 스톱워치 앱을 만드는 과정을 소개하는데, 여기서도 시작∙리셋 버튼과 숫자를 표시하는 레이블요소를 하나의 위젯으로 조합하는 것을 보여준다.
커스텀 위젯
Textual은 사실 왠만한 기본적인 UI 컴포넌트를 기본적으로 제공하기 때문에, 위젯을 합성하는 것으로 필요한 UI요소를 대부분 구현할 수 있다. 그럼에도 특별한 데이터 타입을 시각화하기 위한 새로운 위젯을 바닥부터 새롭게 만들 수도 있다. 다른 위젯에 의존하지 않는 위젯은 render()
라는 메소드를 구현해야 하는데, 이 메소드의 리턴 타입은 RenderResult
타입이며, 다시 이 RenderResult 타입은 Rich 라이브러리의 Renderable과 관련된다.
아래는 간단한 커스텀 위젯을 만들고 사용하는 예제이다.
from textual.app import App, ComposResult, RenderResult
from textual.widget import Widget
class Hello(Widget):
def render(self) -> RenderResult:
return "Hello, [b]world[/b]!"
class CustomApp(App):
def compose(self) -> ComposeResult:
yield Hello()
CustomApp().run()
위젯의 실제 UI는 이렇게 콘텐츠 그 자체 외에도 테두리나 다른 시각 요소를 관리할 필요가 있다. Textual 에서도 대부분의 위젯은 박스 모양으로 생기기는 했지만, Tree와 같이 독특한 구조를 표현할 수 있는 위젯도 있고, CheckBox와 같이 상태에 따라 외관이 변화하는 위젯도 있다. 결국 완전히 새로운 커스텀 위젯을 만들기 위해서는 자유자재로 원하는 UI를 텍스트로 그려낼 수 있는 능력이 요구되며, Textual 뿐만 아니라 Rich나 다른 텍스트 환경 전용의 렌더링 라이브러리에 대한 경험과 지식이 필요로 할 것이다.
바로 위 예제는 render()
를 직접 구현했는데, 사실 대부분의 경우에는 그렇게 할 필요가 없다. 어떤 데이터를 표현하기 위한 가장 기본적인 내용은 Static
위젯이 모두 구현하고 있다. 게다가 Static
위젯에는 .update()
라는 메소드가 있어서, 필요할 때에는 언제든 표시되는 내용을 변경하는 것이 가능하다.
만약 특정한 텍스트를 출력하면서 마우스로 클릭할 때마다 내용이 변경되는 위젯이 필요하다고 하면, Static을 확장하는 것이 가장 효율적인 일일 것이다.
from itertools import cycle
from textual.app import App, ComposeResult
from textual.widgets import Static
hellos = cycle("Hola, Bonjour, Guten tag, Salve, Nin hao".split(", "))
class Hello(Static):
def on_mount(self) -> None:
self.next_word()
def on_click(self) -> None:
self.next_word()
def next_word(self) -> None:
hello = next(hellos)
self.update(f"{hello}, [b]world[/b]!")
class CustomApp(App):
def compose(self) -> ComposeResult:
yield Hello()
CustomApp().run()
디폴트 CSS
각각의 위젯은 CSS 클래스 속성이 아닌 DEFAULT_CSS 속성을 가질 수 있다. 이 속성으로 지정한 스타일은 앱에서 지정한 스타일시트가 없을 때 자동으로 선택되어 적용된다. 커스텀 위젯을 별도의 추가 패키지처럼 사용하고 싶다면, 디폴트 CSS를 잘 활용할 필요가 있을 것이다.
참고로 디폴트CSS에서는 가장 상위에 클래스 자신에 대해서 반드시 명시해야 한다. App의 CSS 속성과 달리, 디폴트 CSS는 앱의 CSS의 내부로 편입되어 CSS의 일부와 같이 해석된다. 따라서 스타일을 적용하는 식별자가 최상위에 와야한다.
한 가지 팁은 앱의 CSS_PATH 속성을 사용해서, 개발하는 위젯의 스타일을 실시간으로 확인한 후, 최종 결과물을 위젯 소스파일로 옮겨주는 것이 편하다. 특히 CSS_PATH가 가리키는 파일이 .css 파일이 아니라 .scss 파일이어도 작동하므로 컴포즈에서의 계층 구조와 동일한 구조의 scss로 정의된 스타일을 그대로 넣어줄 수 있게 된다.