diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/TerminalUI.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/TerminalUI.java index 6dff16b3..9d61c9a7 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/TerminalUI.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/TerminalUI.java @@ -18,6 +18,7 @@ package org.springframework.shell.component.view; import java.io.IOError; import java.util.Collections; import java.util.List; +import java.util.function.BiFunction; import org.jline.keymap.BindingReader; import org.jline.keymap.KeyMap; @@ -148,36 +149,75 @@ public class TerminalUI implements ViewService { } } - private void render(int rows, int columns) { + private void render(Rectangle rect) { if (rootView != null) { - rootView.setRect(0, 0, columns, rows); + rootView.setRect(rect.x(), rect.y(), rect.width(), rect.height()); rootView.draw(virtualDisplay); } if (modalView != null) { modalView.setLayer(1); - modalView.setRect(0, 0, columns, rows); + modalView.setRect(rect.x(), rect.y(), rect.width(), rect.height()); modalView.draw(virtualDisplay); } } + private BiFunction fullScreenViewRect = (terminal, view) -> { + Size s = terminal.getSize(); + return new Rectangle(0, 0, s.getColumns(), s.getRows()); + }; + + private BiFunction nonfullScreenViewRect = (terminal, view) -> { + Size s = terminal.getSize(); + Rectangle rect = view.getRect(); + if (!rect.isEmpty()) { + return rect; + } + return new Rectangle(0, 0, s.getColumns(), 5); + }; + + /** + * Sets a view rect function for full screen mode. Default behaviour uses {@link Rectangle} + * area equal to terminal size. + * + * @param fullScreenViewRect the view rect function + */ + public void setFullScreenViewRect(BiFunction fullScreenViewRect) { + Assert.notNull(fullScreenViewRect, "view rect function must be set"); + this.fullScreenViewRect = fullScreenViewRect; + } + + /** + * Sets a view rect function for full screen mode. Default behaviour uses {@link Rectangle} + * from {@code rootView} if it's not empty, otherwise uses zero based area with + * width matching terminal columns and height matching 5. + * + * @param nonfullScreenViewRect the view rect function + */ + public void setNonfullScreenViewRect(BiFunction nonfullScreenViewRect) { + Assert.notNull(nonfullScreenViewRect, "view rect function must be set"); + this.nonfullScreenViewRect = nonfullScreenViewRect; + } + private void display() { - log.trace("display()"); + log.trace("display() start"); size.copy(terminal.getSize()); if (fullScreen) { display.clear(); - display.resize(size.getRows(), size.getColumns()); display.reset(); - rootView.setRect(0, 0, size.getColumns(), size.getRows()); + display.resize(size.getRows(), size.getColumns()); + Rectangle rect = fullScreenViewRect.apply(terminal, rootView); + rootView.setRect(rect.x(), rect.y(), rect.width(), rect.height()); virtualDisplay.resize(size.getRows(), size.getColumns()); virtualDisplay.setShowCursor(false); - render(size.getRows(), size.getColumns()); + render(rect); } else { + Rectangle rect = nonfullScreenViewRect.apply(terminal, rootView); + display.reset(); display.resize(size.getRows(), size.getColumns()); - Rectangle rect = rootView.getRect(); virtualDisplay.resize(rect.height(), rect.width()); virtualDisplay.setShowCursor(false); - render(rect.height(), rect.width()); + render(rect); } List newLines = virtualDisplay.getScreenLines(); @@ -192,6 +232,7 @@ public class TerminalUI implements ViewService { terminal.puts(Capability.cursor_invisible); } display.update(newLines, targetCursorPos); + log.trace("display() end"); } private void dispatchWinch() { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/geom/Rectangle.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/geom/Rectangle.java index 35f2fcfa..29bf4e0e 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/geom/Rectangle.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/geom/Rectangle.java @@ -36,4 +36,15 @@ public record Rectangle(int x, int y, int width, int height) { h += y; return ((w < x || w > X) && (h < y || h > Y)); } + + /** + * Determines whether the {@code Rectangle} is empty. When the {@code Rectangle} + * is empty, it encloses no area. + * + * @return {@code true} if the {@code Rectangle} is empty; {@code false} + * otherwise. + */ + public boolean isEmpty() { + return width <= 0 || height <= 0; + } } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/view/geom/RectangleTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/view/geom/RectangleTests.java new file mode 100644 index 00000000..96735acc --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/view/geom/RectangleTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.shell.component.view.geom; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class RectangleTests { + + private Rectangle rect; + + @Test + void isEmpty() { + rect = new Rectangle(0, 0, 0, 0); + assertThat(rect.isEmpty()).isTrue(); + rect = new Rectangle(1, 0, 0, 0); + assertThat(rect.isEmpty()).isTrue(); + rect = new Rectangle(0, 1, 0, 0); + assertThat(rect.isEmpty()).isTrue(); + rect = new Rectangle(1, 1, 0, 0); + assertThat(rect.isEmpty()).isTrue(); + rect = new Rectangle(0, 0, 1, 0); + assertThat(rect.isEmpty()).isTrue(); + rect = new Rectangle(0, 0, 0, 1); + assertThat(rect.isEmpty()).isTrue(); + rect = new Rectangle(1, 1, 1, 1); + assertThat(rect.isEmpty()).isFalse(); + } +} diff --git a/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java b/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java index f96007ea..4dfd0491 100644 --- a/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java +++ b/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java @@ -17,12 +17,62 @@ package org.springframework.shell.samples.standard; import org.springframework.shell.command.annotation.Command; import org.springframework.shell.component.ViewComponent; +import org.springframework.shell.component.view.TerminalUI; +import org.springframework.shell.component.view.control.BoxView; import org.springframework.shell.component.view.control.InputView; +import org.springframework.shell.component.view.geom.HorizontalAlign; +import org.springframework.shell.component.view.geom.VerticalAlign; import org.springframework.shell.standard.AbstractShellComponent; @Command public class ComponentUiCommands extends AbstractShellComponent { + @Command(command = "componentui tui1") + public void tui1() { + TerminalUI ui = new TerminalUI(getTerminal()); + BoxView view = new BoxView(); + view.setShowBorder(true); + view.setDrawFunction((screen, rect) -> { + screen.writerBuilder() + .build() + .text("Hello World", rect, HorizontalAlign.CENTER, VerticalAlign.CENTER); + return rect; + }); + ui.setRoot(view, true); + ui.run(); + } + + @Command(command = "componentui tui2") + public void tui2() { + TerminalUI ui = new TerminalUI(getTerminal()); + BoxView view = new BoxView(); + view.setRect(0, 0, 40, 5); + view.setShowBorder(true); + view.setDrawFunction((screen, rect) -> { + screen.writerBuilder() + .build() + .text("Hello World", rect, HorizontalAlign.CENTER, VerticalAlign.CENTER); + return rect; + }); + ui.setRoot(view, false); + ui.run(); + } + + @Command(command = "componentui tui3") + public void tui3() { + TerminalUI ui = new TerminalUI(getTerminal()); + BoxView view = new BoxView(); + view.setShowBorder(true); + view.setDrawFunction((screen, rect) -> { + screen.writerBuilder() + .build() + .text("Hello World", rect, HorizontalAlign.CENTER, VerticalAlign.CENTER); + return rect; + }); + ui.setRoot(view, false); + ui.run(); + } + @Command(command = "componentui string") public String stringInput() { InputView view = new InputView();