화면 전체
화면
잘라낸 조각도 목업도 아닙니다. 하나하나가 이 사이트를 빌드하는 동안 툴킷이 그린 창입니다.
데모 애플리케이션
주문 화면 — 정렬하고 필터링하는 표와 옆에서 미끄러져 열리는 주문 패널 — 이 있고, 옆에는 모든 컴포넌트가 분류별로 놓여 있습니다. 위쪽의 네 선택기가 창 전체의 언어, 방향, 크기, 테마를 바꿉니다.
jbang demo-toolkit/limn-toolkitjbang --java-options=-XstartOnFirstThread demo-toolkit/limn-toolkit

같은 창을, 오른쪽에서 왼쪽으로
ar 번역에 방향 축을 뒤집은 데모 애플리케이션: 메뉴 막대, 탐색, 입력란, 스크롤 막대, 표가 모두 오른쪽에서 읽히고, 그 안의 텍스트는 아랍 문자답게 결합하고 재배열됩니다. 화면을 다시 쓰지 않았습니다. 방향은 컨트롤 크기처럼 설정하는, 상속되는 하나의 축입니다.


폼
라벨, 검증되는 입력란, 선택, 스위치, 동작 줄까지, 폼 가이드의 예제 그대로입니다.


코드
public static Scene scene() {
Row centred = new Row();
centred.mainAlignment(Flex.MainAlignment.CENTER)
.crossAlignment(Flex.CrossAlignment.CENTER);
centred.add(new SizedBox(420, SizedBox.UNSET, form()));
Scene scene = new Scene(new Padding(Insets.all(32), centred));
return scene;
}
public static Widget<?> form() {
TextField name = new TextField();
name.setText("Ada Lovelace");
TextField email = new TextField();
email.setText("ada@example");
Field emailField = emailField(email);
validate(email, emailField);
// setText above was the application's write, and a handler runs for the user alone, so
// the rule in validate did not run: the starting state is set here, explicitly.
emailField.setValidation(Validation.ERROR, "Enter an address like ada@example.com");
PasswordField password = new PasswordField();
password.setText("correct horse battery");
ComboBox<String> plan = new ComboBox<>(List.of("Personal", "Team", "Enterprise"));
plan.setSelectedIndex(1);
Checkbox updates = new Checkbox(Checkbox.Variant.SWITCH, "Email me release notes");
updates.setChecked(true);
Column form = new Column();
form.gap(18).crossAlignment(Flex.CrossAlignment.STRETCH);
form.add(new Label("Create your account").setRole(Label.Role.TITLE));
form.add(counted(new Field("Full name", name).setRequired(true), name, 40));
form.add(emailField);
form.add(new Field("Password", password).setHelperText("At least 12 characters"));
form.add(new Field("Plan", plan));
form.add(updates);
form.add(Separator.horizontal());
form.add(actions());
return form;
}
static Field emailField(TextField email) {
return new Field("Email", email)
.setHelperText("The receipt goes here")
.setRequired(true);
}
static Field counted(Field field, TextField text, int limit) {
field.setHelperText(text.text().length() + " / " + limit);
text.observeChanges((widget, change) -> {
if (change.aspect() == Change.Aspect.TEXT) {
field.setHelperText(text.text().length() + " / " + limit);
}
});
return field;
}
static void validate(TextField email, Field field) {
email.onChange(text -> {
if (text.matches("[^@\\s]+@[^@\\s]+\\.[^@\\s]+")) {
field.setValidation(Validation.SUCCESS);
} else {
field.setValidation(Validation.ERROR, "Enter an address like ada@example.com");
}
});
}
static Widget<?> actions() {
Row actions = new Row();
actions.gap(12).crossAlignment(Flex.CrossAlignment.CENTER);
actions.add(Expanded.spacer(1));
actions.add(new Button("Cancel").setStyle(Button.Style.SECONDARY));
actions.add(new Button("Create account"));
return actions;
}배치한 창
도구 막대, 내용 창 옆의 사이드바, 상태 줄까지, 레이아웃 가이드의 예제 그대로입니다.


코드
public static Scene scene() {
Scene scene = new Scene(shell());
return scene;
}
public static Widget<?> shell() {
ToolBar bar = new ToolBar();
bar.addItem(new Button("Import"));
bar.addSeparator();
bar.addItem(new Button("New collection").setStyle(Button.Style.SECONDARY));
bar.addItem(new Button("Share").setStyle(Button.Style.SECONDARY));
SplitPane panes = SplitPane.horizontal(sidebar(), library());
panes.setRatio(0.24f).setMinimums(150, 320);
Row status = spread(
new Label("18 assets · 3 selected").setMuted(true),
new Label("Synced 2 minutes ago").setMuted(true));
return column(0,
bar,
Expanded.of(panes, 1),
Separator.horizontal(),
new Padding(Insets.symmetric(8, 14), status));
}
private static Widget<?> sidebar() {
return column(0,
Expanded.of(collections(), 1),
Separator.horizontal(),
new Padding(Insets.all(14), details("Textures", "Studio drive")));
}
private static Widget<?> collections() {
ListView<String> list = new ListView<>(
name -> new Padding(Insets.symmetric(9, 14), new Label(name)));
list.setItems(COLLECTIONS);
list.setSelectedIndex(1);
// A list with no caption takes its name directly; a reader says "Collections, list".
list.setAccessibleName("Collections");
return list;
}
private static Widget<?> library() {
SearchField search = new SearchField();
SegmentedControl view = new SegmentedControl(List.of("Grid", "List"));
view.setSelectedIndex(0);
view.setAccessibleName("View");
Row tools = new Row();
tools.gap(10).crossAlignment(Flex.CrossAlignment.CENTER);
tools.add(new SizedBox(190, SizedBox.UNSET, search));
tools.add(view);
List<Widget<?>> tiles = new ArrayList<>();
for (String[] asset : ASSETS) {
tiles.add(tile(asset));
}
return new Padding(Insets.all(20), column(18,
spread(new Label("Textures").setRole(Label.Role.TITLE), tools),
tags(TAGS),
Expanded.of(new ScrollView(tiles(3, tiles)), 1)));
}
private static Widget<?> tile(String[] asset) {
Label badge = new Label(asset[1]);
badge.setColor(Color.WHITE).setStrong(true);
return column(6,
overlay(swatch(asset[3], asset[4]), badge),
new Label(asset[0]).setStrong(true),
new Label(asset[2]).setMuted(true));
}
private static Widget<?> swatch(String from, String to) {
int width = 96;
int height = 64;
Color a = Color.fromHex(from);
Color b = Color.fromHex(to);
byte[] pixels = new byte[width * height * 4];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
float t = (x / (float) (width - 1) + y / (float) (height - 1)) / 2f;
int i = (y * width + x) * 4;
Color mixed = a.lerp(b, t);
pixels[i] = (byte) Color.toByte(mixed.r());
pixels[i + 1] = (byte) Color.toByte(mixed.g());
pixels[i + 2] = (byte) Color.toByte(mixed.b());
pixels[i + 3] = (byte) 0xFF;
}
}
return new ImageView(new Image(width, height, pixels))
.setFit(ImageView.Fit.COVER)
.setPreferredSize(600, 86);
}
private static final List<String> COLLECTIONS =
List.of("All assets", "Textures", "Models", "HDRIs", "Fonts", "Archive");
/** The library's filters, as many and as long as a real collection's would be. */
private static final List<String> TAGS = List.of("Tileable", "PBR", "4K", "Outdoor",
"Brushed metal", "Wood", "Stone", "Organic", "Seamless", "Hand-painted", "Sci-fi panels");
/** name · overlay badge · size on disk · the two ends of the swatch's ramp. */
private static final String[][] ASSETS = {
{"Concrete 04", "2048 × 2048", "4.2 MB", "1E1230", "6D00E0"},
{"Brushed steel", "1024 × 1024", "1.8 MB", "10131E", "5A6E8C"},
{"Oak planks", "2048 × 2048", "3.6 MB", "241206", "9A5A22"},
{"Sand dunes", "4096 × 4096", "18.4 MB", "2A1A08", "C79A4B"},
{"Rust patina", "2048 × 2048", "5.1 MB", "2B0D08", "A8442A"},
{"Marble tile", "2048 × 2048", "3.9 MB", "141626", "8E93C8"},
{"Moss bank", "2048 × 2048", "4.7 MB", "0C1A0E", "3F8A46"},
// Eight, not nine: the last line of the grid is deliberately short, so the
// picture on the guide page shows what a short line does to the tile width.
{"Sea ice", "4096 × 4096", "21.0 MB", "081A22", "6FBFD6"},
};
static Column column(float gap, Widget<?>... children) {
Column column = new Column();
column.gap(gap).crossAlignment(Flex.CrossAlignment.STRETCH);
for (Widget<?> child : children) {
column.add(child);
}
return column;
}
static Row spread(Widget<?> leading, Widget<?> trailing) {
Row row = new Row();
row.gap(12).crossAlignment(Flex.CrossAlignment.CENTER);
row.add(leading);
row.add(Expanded.spacer(1));
row.add(trailing);
return row;
}
static Widget<?> overlay(Widget<?> content, Widget<?> on) {
Stack stack = new Stack();
stack.alignment(Stack.Alignment.CENTER);
stack.add(content);
stack.add(on);
return stack;
}
static Wrap tags(List<String> names) {
Wrap tags = new Wrap();
tags.gap(8).runGap(8);
tags.setControlSize(ControlSize.SMALL);
for (String name : names) {
tags.add(new Button(name).setStyle(Button.Style.SECONDARY));
}
return tags;
}
static Grid details(String name, String location) {
TextField nameField = new TextField();
nameField.setText(name);
TextField locationField = new TextField();
locationField.setText(location);
Checkbox shared = new Checkbox(Checkbox.Variant.BOX, "Shared");
shared.setChecked(true);
Grid form = new Grid(2);
form.gap(12).rowGap(10).expandedColumn(1)
.columnAlignment(1, Flex.CrossAlignment.STRETCH);
form.add(new Label("Name").setMuted(true).setLabelFor(nameField));
form.add(nameField);
form.add(new Label("Location").setMuted(true).setLabelFor(locationField));
form.add(locationField);
form.add(new SizedBox(0, 0)); // an empty cell: the checkbox carries its own caption
form.add(shared);
return form;
}
static Column tiles(int perRow, List<Widget<?>> tiles) {
Column rows = new Column();
rows.gap(14).crossAlignment(Flex.CrossAlignment.STRETCH);
for (int start = 0; start < tiles.size(); start += perRow) {
Row line = new Row();
line.gap(14).crossAlignment(Flex.CrossAlignment.START);
for (int i = start; i < Math.min(start + perRow, tiles.size()); i++) {
line.add(Expanded.of(tiles.get(i), 1));
}
for (int i = tiles.size(); i < start + perRow; i++) {
line.add(Expanded.spacer(1));
}
rows.add(line);
}
return rows;
}모든 크기 단계
같은 컨트롤 다섯 개를 다섯 번, 위의 XSMALL부터 아래의 XLARGE까지. 어느 것에도 너비나 글꼴이나 여백을 주지 않았습니다. 각 줄에 알려 주는 것은 컨트롤 크기 하나뿐이고, 여백과 글자와 모서리 반경과 터치 영역이 함께 움직입니다.


3D 뷰포트
세 개의 광원 아래 물리 기반 재질을 선형 하이 다이내믹 레인지 대상에 그린 뒤 2D 레이어로 합성합니다. 스크롤 뷰는 다른 위젯과 똑같이 이것을 잘라냅니다. 끌어서 돌리고, 스크롤해서 확대하세요.


파일을 쓰는 프로그램으로서의 테마 편집기
팔레트는 데이터이므로 그것을 편집하는 일은 하나의 화면이 됩니다. 그리고 이것은 당신의 애플리케이션이 짊어지는 의존성이 아니라, 실행해서 쓰는 프로그램입니다. 저장되는 것은 테마 파일이고, 애플리케이션은 limn-toolkit만으로 ThemeFormat을 써서 읽어 들입니다. 모서리 슬라이더를 끌면 같은 프레임 안에서 창이 옷을 갈아입습니다. 화면 속 모든 입력란과 버튼과 색 견본이 함께. 옆의 보고서는 각 잉크를 그것이 놓일 수 있는 모든 표면에 대해 재므로, 명도 대비가 부족한 팔레트는 눈에 보이게 떨어집니다.
jbang theme-editor-toolkit/limn-toolkitjbang --java-options=-XstartOnFirstThread theme-editor-toolkit/limn-toolkit


