/*
 * This file is part of lanterna (https://github.com/mabe02/lanterna).
 *
 * lanterna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2010-2020 Martin Berglund
 */
package com.googlecode.lanterna.gui2;

import com.googlecode.lanterna.Symbols;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.graphics.ThemeDefinition;

Classic scrollbar that can be used to display where inside a larger component a view is showing. This implementation is not interactable and needs to be driven externally, meaning you can't focus on the scrollbar itself, you have to update its state as part of another component being modified. ScrollBars are either horizontal or vertical, which affects the way they appear and how they are drawn.

This class works on two concepts, the min-position-max values and the view size. The minimum value is always 0 and cannot be changed. The maximum value is 100 and can be adjusted programmatically. Position value is whever along the axis of 0 to max the scrollbar's tracker currently is placed. The view size is an important concept, it determines how big the tracker should be and limits the position so that it can only reach maximum value - view size.

The regular way to use the ScrollBar class is to tie it to the model-view of another component and set the scrollbar's maximum to the total height (or width, if the scrollbar is horizontal) of the model-view. View size should then be assigned based on the current size of the view, meaning as the terminal and/or the GUI changes and the components visible space changes, the scrollbar's view size is updated along with it. Finally the position of the scrollbar should be equal to the scroll offset in the component.

Author:Martin
/** * Classic scrollbar that can be used to display where inside a larger component a view is showing. This implementation * is not interactable and needs to be driven externally, meaning you can't focus on the scrollbar itself, you have to * update its state as part of another component being modified. {@code ScrollBar}s are either horizontal or vertical, * which affects the way they appear and how they are drawn. * <p> * This class works on two concepts, the min-position-max values and the view size. The minimum value is always 0 and * cannot be changed. The maximum value is 100 and can be adjusted programmatically. Position value is whever along the * axis of 0 to max the scrollbar's tracker currently is placed. The view size is an important concept, it determines * how big the tracker should be and limits the position so that it can only reach {@code maximum value - view size}. * <p> * The regular way to use the {@code ScrollBar} class is to tie it to the model-view of another component and set the * scrollbar's maximum to the total height (or width, if the scrollbar is horizontal) of the model-view. View size * should then be assigned based on the current size of the view, meaning as the terminal and/or the GUI changes and the * components visible space changes, the scrollbar's view size is updated along with it. Finally the position of the * scrollbar should be equal to the scroll offset in the component. * * @author Martin */
public class ScrollBar extends AbstractComponent<ScrollBar> { private final Direction direction; private int maximum; private int position; private int viewSize;
Creates a new ScrollBar with a specified direction
Params:
  • direction – Direction of the scrollbar
/** * Creates a new {@code ScrollBar} with a specified direction * @param direction Direction of the scrollbar */
public ScrollBar(Direction direction) { this.direction = direction; this.maximum = 100; this.position = 0; this.viewSize = 0; }
Returns the direction of this ScrollBar
Returns:Direction of this ScrollBar
/** * Returns the direction of this {@code ScrollBar} * @return Direction of this {@code ScrollBar} */
public Direction getDirection() { return direction; }
Sets the maximum value the scrollbar's position (minus the view size) can have
Params:
  • maximum – Maximum value
Returns:Itself
/** * Sets the maximum value the scrollbar's position (minus the view size) can have * @param maximum Maximum value * @return Itself */
public ScrollBar setScrollMaximum(int maximum) { if(maximum < 0) { throw new IllegalArgumentException("Cannot set ScrollBar maximum to " + maximum); } this.maximum = maximum; invalidate(); return this; }
Returns the maximum scroll value
Returns:Maximum scroll value
/** * Returns the maximum scroll value * @return Maximum scroll value */
public int getScrollMaximum() { return maximum; }
Sets the scrollbar's position, should be a value between 0 and maximum - view size
Params:
  • position – Scrollbar's tracker's position
Returns:Itself
/** * Sets the scrollbar's position, should be a value between 0 and {@code maximum - view size} * @param position Scrollbar's tracker's position * @return Itself */
public ScrollBar setScrollPosition(int position) { this.position = Math.min(position, this.maximum); invalidate(); return this; }
Returns the position of the ScrollBar's tracker
Returns:Position of the ScrollBar's tracker
/** * Returns the position of the {@code ScrollBar}'s tracker * @return Position of the {@code ScrollBar}'s tracker */
public int getScrollPosition() { return position; }
Sets the view size of the scrollbar, determining how big the scrollbar's tracker should be and also affecting the maximum value of tracker's position
Params:
  • viewSize – View size of the scrollbar
Returns:Itself
/** * Sets the view size of the scrollbar, determining how big the scrollbar's tracker should be and also affecting the * maximum value of tracker's position * @param viewSize View size of the scrollbar * @return Itself */
public ScrollBar setViewSize(int viewSize) { this.viewSize = viewSize; return this; }
Returns the view size of the scrollbar
Returns:View size of the scrollbar
/** * Returns the view size of the scrollbar * @return View size of the scrollbar */
public int getViewSize() { if(viewSize > 0) { return viewSize; } if(direction == Direction.HORIZONTAL) { return getSize().getColumns(); } else { return getSize().getRows(); } } @Override protected ComponentRenderer<ScrollBar> createDefaultRenderer() { return new DefaultScrollBarRenderer(); }
Helper class for making new ScrollBar renderers a little bit cleaner
/** * Helper class for making new {@code ScrollBar} renderers a little bit cleaner */
public static abstract class ScrollBarRenderer implements ComponentRenderer<ScrollBar> { @Override public TerminalSize getPreferredSize(ScrollBar component) { return TerminalSize.ONE; } }
Default renderer for ScrollBar which will be used unless overridden. This will draw a scrollbar using arrows at each extreme end, a background color for spaces between those arrows and the tracker and then the tracker itself in three different styles depending on the size of the tracker. All characters and colors are customizable through whatever theme is currently in use.
/** * Default renderer for {@code ScrollBar} which will be used unless overridden. This will draw a scrollbar using * arrows at each extreme end, a background color for spaces between those arrows and the tracker and then the * tracker itself in three different styles depending on the size of the tracker. All characters and colors are * customizable through whatever theme is currently in use. */
public static class DefaultScrollBarRenderer extends ScrollBarRenderer { private boolean growScrollTracker;
Default constructor
/** * Default constructor */
public DefaultScrollBarRenderer() { this.growScrollTracker = true; }
Should tracker automatically grow in size along with the ScrollBar (default: true)
Params:
  • growScrollTracker – Automatically grow tracker
/** * Should tracker automatically grow in size along with the {@code ScrollBar} (default: {@code true}) * @param growScrollTracker Automatically grow tracker */
public void setGrowScrollTracker(boolean growScrollTracker) { this.growScrollTracker = growScrollTracker; } @Override public void drawComponent(TextGUIGraphics graphics, ScrollBar component) { TerminalSize size = graphics.getSize(); Direction direction = component.getDirection(); int position = component.getScrollPosition(); int maximum = component.getScrollMaximum(); int viewSize = component.getViewSize(); if(size.getRows() == 0 || size.getColumns() == 0) { return; } //Adjust position if necessary if(position + viewSize >= maximum) { position = Math.max(0, maximum - viewSize); component.setScrollPosition(position); } ThemeDefinition themeDefinition = component.getThemeDefinition(); graphics.applyThemeStyle(themeDefinition.getNormal()); if(direction == Direction.VERTICAL) { if(size.getRows() == 1) { graphics.setCharacter(0, 0, themeDefinition.getCharacter("VERTICAL_BACKGROUND", Symbols.BLOCK_MIDDLE)); } else if(size.getRows() == 2) { graphics.setCharacter(0, 0, themeDefinition.getCharacter("UP_ARROW", Symbols.TRIANGLE_UP_POINTING_BLACK)); graphics.setCharacter(0, 1, themeDefinition.getCharacter("DOWN_ARROW", Symbols.TRIANGLE_DOWN_POINTING_BLACK)); } else { int scrollableArea = size.getRows() - 2; int scrollTrackerSize = 1; if(growScrollTracker) { float ratio = clampRatio((float) viewSize / (float) maximum); scrollTrackerSize = Math.max(1, (int) (ratio * (float) scrollableArea)); } float ratio = clampRatio((float)position / (float)(maximum - viewSize)); int scrollTrackerPosition = (int)(ratio * (float)(scrollableArea - scrollTrackerSize)) + 1; graphics.setCharacter(0, 0, themeDefinition.getCharacter("UP_ARROW", Symbols.TRIANGLE_UP_POINTING_BLACK)); graphics.drawLine(0, 1, 0, size.getRows() - 2, themeDefinition.getCharacter("VERTICAL_BACKGROUND", Symbols.BLOCK_MIDDLE)); graphics.setCharacter(0, size.getRows() - 1, themeDefinition.getCharacter("DOWN_ARROW", Symbols.TRIANGLE_DOWN_POINTING_BLACK)); if(scrollTrackerSize == 1) { graphics.setCharacter(0, scrollTrackerPosition, themeDefinition.getCharacter("VERTICAL_SMALL_TRACKER", Symbols.BLOCK_SOLID)); } else if(scrollTrackerSize == 2) { graphics.setCharacter(0, scrollTrackerPosition, themeDefinition.getCharacter("VERTICAL_TRACKER_TOP", Symbols.BLOCK_SOLID)); graphics.setCharacter(0, scrollTrackerPosition + 1, themeDefinition.getCharacter("VERTICAL_TRACKER_BOTTOM", Symbols.BLOCK_SOLID)); } else { graphics.setCharacter(0, scrollTrackerPosition, themeDefinition.getCharacter("VERTICAL_TRACKER_TOP", Symbols.BLOCK_SOLID)); graphics.drawLine(0, scrollTrackerPosition + 1, 0, scrollTrackerPosition + scrollTrackerSize - 2, themeDefinition.getCharacter("VERTICAL_TRACKER_BACKGROUND", Symbols.BLOCK_SOLID)); graphics.setCharacter(0, scrollTrackerPosition + (scrollTrackerSize / 2), themeDefinition.getCharacter("VERTICAL_SMALL_TRACKER", Symbols.BLOCK_SOLID)); graphics.setCharacter(0, scrollTrackerPosition + scrollTrackerSize - 1, themeDefinition.getCharacter("VERTICAL_TRACKER_BOTTOM", Symbols.BLOCK_SOLID)); } } } else { if(size.getColumns() == 1) { graphics.setCharacter(0, 0, themeDefinition.getCharacter("HORIZONTAL_BACKGROUND", Symbols.BLOCK_MIDDLE)); } else if(size.getColumns() == 2) { graphics.setCharacter(0, 0, Symbols.TRIANGLE_LEFT_POINTING_BLACK); graphics.setCharacter(1, 0, Symbols.TRIANGLE_RIGHT_POINTING_BLACK); } else { int scrollableArea = size.getColumns() - 2; int scrollTrackerSize = 1; if(growScrollTracker) { float ratio = clampRatio((float) viewSize / (float) maximum); scrollTrackerSize = Math.max(1, (int) (ratio * (float) scrollableArea)); } float ratio = clampRatio((float)position / (float)(maximum - viewSize)); int scrollTrackerPosition = (int)(ratio * (float)(scrollableArea - scrollTrackerSize)) + 1; graphics.setCharacter(0, 0, themeDefinition.getCharacter("LEFT_ARROW", Symbols.TRIANGLE_LEFT_POINTING_BLACK)); graphics.drawLine(1, 0, size.getColumns() - 2, 0, themeDefinition.getCharacter("HORIZONTAL_BACKGROUND", Symbols.BLOCK_MIDDLE)); graphics.setCharacter(size.getColumns() - 1, 0, themeDefinition.getCharacter("RIGHT_ARROW", Symbols.TRIANGLE_RIGHT_POINTING_BLACK)); if(scrollTrackerSize == 1) { graphics.setCharacter(scrollTrackerPosition, 0, themeDefinition.getCharacter("HORIZONTAL_SMALL_TRACKER", Symbols.BLOCK_SOLID)); } else if(scrollTrackerSize == 2) { graphics.setCharacter(scrollTrackerPosition, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_LEFT", Symbols.BLOCK_SOLID)); graphics.setCharacter(scrollTrackerPosition + 1, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_RIGHT", Symbols.BLOCK_SOLID)); } else { graphics.setCharacter(scrollTrackerPosition, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_LEFT", Symbols.BLOCK_SOLID)); graphics.drawLine(scrollTrackerPosition + 1, 0, scrollTrackerPosition + scrollTrackerSize - 2, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_BACKGROUND", Symbols.BLOCK_SOLID)); graphics.setCharacter(scrollTrackerPosition + (scrollTrackerSize / 2), 0, themeDefinition.getCharacter("HORIZONTAL_SMALL_TRACKER", Symbols.BLOCK_SOLID)); graphics.setCharacter(scrollTrackerPosition + scrollTrackerSize - 1, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_RIGHT", Symbols.BLOCK_SOLID)); } } } } private float clampRatio(float value) { if(value < 0.0f) { return 0.0f; } else if(value > 1.0f) { return 1.0f; } else { return value; } } } }