From 95aaa865c587dcd6d3562e50d0ecbdfde94802ac Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sun, 4 Feb 2024 09:43:56 +0000 Subject: [PATCH] ProgressView spinner calculation in item function - Move spinner frame calculation to item itself so that - Remove spinnerFrame from state and add start/update times - view don't have hard dependency to it, as we'd need its interval - Change spinner to run only when view is running - Relates #995 --- .../component/view/control/ProgressView.java | 81 +++++++++++++------ .../samples/standard/ComponentUiCommands.java | 53 ++++++------ 2 files changed, 78 insertions(+), 56 deletions(-) diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ProgressView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ProgressView.java index f4a8f914..bc7ed6ac 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ProgressView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ProgressView.java @@ -15,10 +15,14 @@ */ package org.springframework.shell.component.view.control; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.springframework.shell.component.message.ShellMessageBuilder; import org.springframework.shell.component.view.control.cell.TextCell; import org.springframework.shell.component.view.screen.Screen; @@ -36,15 +40,18 @@ import org.springframework.util.Assert; */ public class ProgressView extends BoxView { + private final static Logger log = LoggerFactory.getLogger(ProgressView.class); private final int tickStart; private final int tickEnd; private int tickValue; private boolean running = false; private String description; - private Spinner spinner = Spinner.of(Spinner.LINE1, 130); - private int spinnerFrame; + private Spinner spinner; private List items; private GridView grid; + private long startTime; + private long updateTime; + private List> cells = new ArrayList<>(); private final static Function> DEFAULT_DESCRIPTION_FACTORY = (item) -> TextCell.of(item, ctx -> { @@ -66,9 +73,27 @@ public class ProgressView extends BoxView { private final static Function> DEFAULT_SPINNER_FACTORY = item -> { TextCell cell = TextCell.of(item, ctx -> { - Spinner spin = ctx.spinner(); - int spinState = ctx.getState().sprinnerFrame(); - return String.format("%s", spin.getFrames()[spinState]); + int frame = 0; + + Spinner spin = ctx.getSpinner(); + if (spin == null) { + spin = Spinner.of(Spinner.LINE1, 130); + } + if (ctx.getState().running()) { + // we know start time and current update time, + // calculate elapsed time "frame" to pick rolling + // spinner frame. + int interval = spin.getInterval(); + long startTime = ctx.getState().startTime(); + long updateTime = ctx.getState().updateTime(); + long elapsedTime = updateTime - startTime; + long elapsedFrame = elapsedTime / interval; + frame = (int) elapsedFrame % spin.getFrames().length; + log.debug("Calculate frame {} {} {}", interval, elapsedTime, elapsedFrame); + } + log.debug("Drawing frame {}", frame); + + return String.format("%s", spin.getFrames()[frame]); }); return cell; }; @@ -173,11 +198,21 @@ public class ProgressView extends BoxView { this.description = description; } + /** + * Sets an explicit spinner to use. + * + * @param spinner the spinner to use + */ + public void setSpinner(Spinner spinner) { + this.spinner = spinner; + } + public void start() { if (running) { return; } running = true; + startTime = System.currentTimeMillis(); ProgressState state = getState(); dispatch(ShellMessageBuilder.ofView(this, ProgressViewStartEvent.of(this, state))); } @@ -212,6 +247,7 @@ public class ProgressView extends BoxView { for (ProgressViewItem item : items) { columnSizes[index] = item.size; TextCell cell = item.factory.apply(buildContext()); + cells.add(cell); cell.setHorizontalAlign(item.align); grid.addItem(new BoxWrapper(cell), 0, index, 1, 1, 0, 0); index++; @@ -221,27 +257,17 @@ public class ProgressView extends BoxView { } - boolean needLastMillisInit = true; - private long lastMillis; - @Override protected void drawInternal(Screen screen) { long current = System.currentTimeMillis(); - if (needLastMillisInit) { - needLastMillisInit = false; - lastMillis = current; - } - long elapsedFromLast = current - lastMillis; - if (elapsedFromLast > spinner.getInterval()) { - spinnerFrame = (spinnerFrame + 1) % spinner.getFrames().length; - lastMillis = current; + updateTime = current; + Context context = buildContext(); + for (TextCell cell : cells) { + cell.setItem(context); } Rectangle rect = getRect(); - int width = rect.width(); - width = width / 3; - grid.setRect(rect.x(), rect.y(), rect.width(), rect.height()); grid.draw(screen); @@ -291,7 +317,7 @@ public class ProgressView extends BoxView { * @return a view progress state */ public ProgressState getState() { - return ProgressState.of(tickStart, tickEnd, tickValue, running, spinnerFrame); + return ProgressState.of(tickStart, tickEnd, tickValue, running, startTime, updateTime); } private Context buildContext() { @@ -318,7 +344,7 @@ public class ProgressView extends BoxView { } @Override - public Spinner spinner() { + public Spinner getSpinner() { return ProgressView.this.spinner; } }; @@ -355,7 +381,7 @@ public class ProgressView extends BoxView { * * @return spinner frames */ - Spinner spinner(); + Spinner getSpinner(); /** * Resolve style using existing {@link ThemeResolver} and {@code theme name}. @@ -376,12 +402,15 @@ public class ProgressView extends BoxView { * @param tickEnd the tick end value, positive and more than tick start * @param tickValue the current tick value, within inclusive bounds of tick start/end * @param running the running state - * @param spinnerFrame the current spinner frame index + * @param startTime the running start time + * @param updateTime the last running update time */ - public record ProgressState(int tickStart, int tickEnd, int tickValue, boolean running, int sprinnerFrame) { + public record ProgressState(int tickStart, int tickEnd, int tickValue, boolean running, long startTime, + long updateTime) { - public static ProgressState of(int tickStart, int tickEnd, int tickValue, boolean running, int spinnerFrame) { - return new ProgressState(tickStart, tickEnd, tickValue, running, spinnerFrame); + public static ProgressState of(int tickStart, int tickEnd, int tickValue, boolean running, long startTime, + long updateTime) { + return new ProgressState(tickStart, tickEnd, tickValue, running, startTime, updateTime); } } 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 0f92ec65..457b6095 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 @@ -31,6 +31,7 @@ import org.springframework.shell.component.view.control.BoxView; import org.springframework.shell.component.view.control.InputView; import org.springframework.shell.component.view.control.ProgressView; import org.springframework.shell.component.view.control.ProgressView.ProgressViewItem; +import org.springframework.shell.component.view.control.Spinner; import org.springframework.shell.component.view.event.EventLoop; import org.springframework.shell.geom.HorizontalAlign; import org.springframework.shell.geom.VerticalAlign; @@ -95,13 +96,7 @@ public class ComponentUiCommands extends AbstractShellComponent { return String.format("Input was '%s'", input); } - @Command(command = "componentui progress1") - public void progress1() { - ProgressView view = new ProgressView(); - view.setDescription("name"); - view.setRect(0, 0, 20, 1); - - + private void runProgress(ProgressView view) { ViewComponent component = new ViewComponent(getTerminal(), view); EventLoop eventLoop = component.getEventLoop(); @@ -123,10 +118,19 @@ public class ComponentUiCommands extends AbstractShellComponent { } })); - component.run(); } + @Command(command = "componentui progress1") + public void progress1() { + ProgressView view = new ProgressView(); + view.setDescription("name"); + view.setRect(0, 0, 20, 1); + view.start(); + + runProgress(view); + } + @Command(command = "componentui progress2") public void progress2() { ProgressView view = new ProgressView(0, 100, ProgressViewItem.ofText(10, HorizontalAlign.LEFT), @@ -134,31 +138,20 @@ public class ComponentUiCommands extends AbstractShellComponent { ProgressViewItem.ofPercent(0, HorizontalAlign.RIGHT)); view.setDescription("name"); view.setRect(0, 0, 20, 1); + view.start(); + runProgress(view); + } - ViewComponent component = new ViewComponent(getTerminal(), view); - EventLoop eventLoop = component.getEventLoop(); + @Command(command = "componentui progress3") + public void progress3() { + ProgressView view = new ProgressView(); + view.setDescription("name"); + view.setRect(0, 0, 20, 1); + view.setSpinner(Spinner.of(Spinner.DOTS1, 80)); + view.start(); - Flux> ticks = Flux.interval(Duration.ofMillis(100)).map(l -> { - Message message = MessageBuilder - .withPayload(l) - .setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.USER) - .build(); - return message; - }); - eventLoop.dispatch(ticks); - - eventLoop.onDestroy(eventLoop.events() - .filter(m -> EventLoop.Type.USER.equals(StaticShellMessageHeaderAccessor.getEventType(m))) - .subscribe(m -> { - if (m.getPayload() instanceof Long) { - view.tickAdvance(5); - eventLoop.dispatch(ShellMessageBuilder.ofRedraw()); - } - })); - - - component.run(); + runProgress(view); } }