| 1 | package com.googlecode.lanterna.gui2; |
| 2 | |
| 3 | import com.googlecode.lanterna.Symbols; |
| 4 | import com.googlecode.lanterna.TerminalSize; |
| 5 | import com.googlecode.lanterna.graphics.ThemeDefinition; |
| 6 | |
| 7 | /** |
| 8 | * Classic scrollbar that can be used to display where inside a larger component a view is showing. This implementation |
| 9 | * is not interactable and needs to be driven externally, meaning you can't focus on the scrollbar itself, you have to |
| 10 | * update its state as part of another component being modified. {@code ScrollBar}s are either horizontal or vertical, |
| 11 | * which affects the way they appear and how they are drawn. |
| 12 | * <p> |
| 13 | * This class works on two concepts, the min-position-max values and the view size. The minimum value is always 0 and |
| 14 | * cannot be changed. The maximum value is 100 and can be adjusted programmatically. Position value is whever along the |
| 15 | * axis of 0 to max the scrollbar's tracker currently is placed. The view size is an important concept, it determines |
| 16 | * how big the tracker should be and limits the position so that it can only reach {@code maximum value - view size}. |
| 17 | * <p> |
| 18 | * The regular way to use the {@code ScrollBar} class is to tie it to the model-view of another component and set the |
| 19 | * scrollbar's maximum to the total height (or width, if the scrollbar is horizontal) of the model-view. View size |
| 20 | * should then be assigned based on the current size of the view, meaning as the terminal and/or the GUI changes and the |
| 21 | * components visible space changes, the scrollbar's view size is updated along with it. Finally the position of the |
| 22 | * scrollbar should be equal to the scroll offset in the component. |
| 23 | * |
| 24 | * @author Martin |
| 25 | */ |
| 26 | public class ScrollBar extends AbstractComponent<ScrollBar> { |
| 27 | |
| 28 | private final Direction direction; |
| 29 | private int maximum; |
| 30 | private int position; |
| 31 | private int viewSize; |
| 32 | |
| 33 | /** |
| 34 | * Creates a new {@code ScrollBar} with a specified direction |
| 35 | * @param direction Direction of the scrollbar |
| 36 | */ |
| 37 | public ScrollBar(Direction direction) { |
| 38 | this.direction = direction; |
| 39 | this.maximum = 100; |
| 40 | this.position = 0; |
| 41 | this.viewSize = 0; |
| 42 | } |
| 43 | |
| 44 | /** |
| 45 | * Returns the direction of this {@code ScrollBar} |
| 46 | * @return Direction of this {@code ScrollBar} |
| 47 | */ |
| 48 | public Direction getDirection() { |
| 49 | return direction; |
| 50 | } |
| 51 | |
| 52 | /** |
| 53 | * Sets the maximum value the scrollbar's position (minus the view size) can have |
| 54 | * @param maximum Maximum value |
| 55 | * @return Itself |
| 56 | */ |
| 57 | public ScrollBar setScrollMaximum(int maximum) { |
| 58 | if(maximum < 0) { |
| 59 | throw new IllegalArgumentException("Cannot set ScrollBar maximum to " + maximum); |
| 60 | } |
| 61 | this.maximum = maximum; |
| 62 | invalidate(); |
| 63 | return this; |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * Returns the maximum scroll value |
| 68 | * @return Maximum scroll value |
| 69 | */ |
| 70 | public int getScrollMaximum() { |
| 71 | return maximum; |
| 72 | } |
| 73 | |
| 74 | |
| 75 | /** |
| 76 | * Sets the scrollbar's position, should be a value between 0 and {@code maximum - view size} |
| 77 | * @param position Scrollbar's tracker's position |
| 78 | * @return Itself |
| 79 | */ |
| 80 | public ScrollBar setScrollPosition(int position) { |
| 81 | this.position = Math.min(position, this.maximum); |
| 82 | invalidate(); |
| 83 | return this; |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Returns the position of the {@code ScrollBar}'s tracker |
| 88 | * @return Position of the {@code ScrollBar}'s tracker |
| 89 | */ |
| 90 | public int getScrollPosition() { |
| 91 | return position; |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * Sets the view size of the scrollbar, determining how big the scrollbar's tracker should be and also affecting the |
| 96 | * maximum value of tracker's position |
| 97 | * @param viewSize View size of the scrollbar |
| 98 | * @return Itself |
| 99 | */ |
| 100 | public ScrollBar setViewSize(int viewSize) { |
| 101 | this.viewSize = viewSize; |
| 102 | return this; |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Returns the view size of the scrollbar |
| 107 | * @return View size of the scrollbar |
| 108 | */ |
| 109 | public int getViewSize() { |
| 110 | if(viewSize > 0) { |
| 111 | return viewSize; |
| 112 | } |
| 113 | if(direction == Direction.HORIZONTAL) { |
| 114 | return getSize().getColumns(); |
| 115 | } |
| 116 | else { |
| 117 | return getSize().getRows(); |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | @Override |
| 122 | protected ComponentRenderer<ScrollBar> createDefaultRenderer() { |
| 123 | return new DefaultScrollBarRenderer(); |
| 124 | } |
| 125 | |
| 126 | /** |
| 127 | * Helper class for making new {@code ScrollBar} renderers a little bit cleaner |
| 128 | */ |
| 129 | public static abstract class ScrollBarRenderer implements ComponentRenderer<ScrollBar> { |
| 130 | @Override |
| 131 | public TerminalSize getPreferredSize(ScrollBar component) { |
| 132 | return TerminalSize.ONE; |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | /** |
| 137 | * Default renderer for {@code ScrollBar} which will be used unless overridden. This will draw a scrollbar using |
| 138 | * arrows at each extreme end, a background color for spaces between those arrows and the tracker and then the |
| 139 | * tracker itself in three different styles depending on the size of the tracker. All characters and colors are |
| 140 | * customizable through whatever theme is currently in use. |
| 141 | */ |
| 142 | public static class DefaultScrollBarRenderer extends ScrollBarRenderer { |
| 143 | |
| 144 | private boolean growScrollTracker; |
| 145 | |
| 146 | /** |
| 147 | * Default constructor |
| 148 | */ |
| 149 | public DefaultScrollBarRenderer() { |
| 150 | this.growScrollTracker = true; |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | * Should tracker automatically grow in size along with the {@code ScrollBar} (default: {@code true}) |
| 155 | * @param growScrollTracker Automatically grow tracker |
| 156 | */ |
| 157 | public void setGrowScrollTracker(boolean growScrollTracker) { |
| 158 | this.growScrollTracker = growScrollTracker; |
| 159 | } |
| 160 | |
| 161 | @Override |
| 162 | public void drawComponent(TextGUIGraphics graphics, ScrollBar component) { |
| 163 | TerminalSize size = graphics.getSize(); |
| 164 | Direction direction = component.getDirection(); |
| 165 | int position = component.getScrollPosition(); |
| 166 | int maximum = component.getScrollMaximum(); |
| 167 | int viewSize = component.getViewSize(); |
| 168 | |
| 169 | if(size.getRows() == 0 || size.getColumns() == 0) { |
| 170 | return; |
| 171 | } |
| 172 | |
| 173 | //Adjust position if necessary |
| 174 | if(position + viewSize >= maximum) { |
| 175 | position = Math.max(0, maximum - viewSize); |
| 176 | component.setScrollPosition(position); |
| 177 | } |
| 178 | |
| 179 | ThemeDefinition themeDefinition = graphics.getThemeDefinition(ScrollBar.class); |
| 180 | graphics.applyThemeStyle(themeDefinition.getNormal()); |
| 181 | |
| 182 | if(direction == Direction.VERTICAL) { |
| 183 | if(size.getRows() == 1) { |
| 184 | graphics.setCharacter(0, 0, themeDefinition.getCharacter("VERTICAL_BACKGROUND", Symbols.BLOCK_MIDDLE)); |
| 185 | } |
| 186 | else if(size.getRows() == 2) { |
| 187 | graphics.setCharacter(0, 0, themeDefinition.getCharacter("UP_ARROW", Symbols.ARROW_UP)); |
| 188 | graphics.setCharacter(0, 1, themeDefinition.getCharacter("DOWN_ARROW", Symbols.ARROW_DOWN)); |
| 189 | } |
| 190 | else { |
| 191 | int scrollableArea = size.getRows() - 2; |
| 192 | int scrollTrackerSize = 1; |
| 193 | if(growScrollTracker) { |
| 194 | float ratio = clampRatio((float) viewSize / (float) maximum); |
| 195 | scrollTrackerSize = Math.max(1, (int) (ratio * (float) scrollableArea)); |
| 196 | } |
| 197 | |
| 198 | float ratio = clampRatio((float)position / (float)(maximum - viewSize)); |
| 199 | int scrollTrackerPosition = (int)(ratio * (float)(scrollableArea - scrollTrackerSize)) + 1; |
| 200 | |
| 201 | graphics.setCharacter(0, 0, themeDefinition.getCharacter("UP_ARROW", Symbols.ARROW_UP)); |
| 202 | graphics.drawLine(0, 1, 0, size.getRows() - 2, themeDefinition.getCharacter("VERTICAL_BACKGROUND", Symbols.BLOCK_MIDDLE)); |
| 203 | graphics.setCharacter(0, size.getRows() - 1, themeDefinition.getCharacter("DOWN_ARROW", Symbols.ARROW_DOWN)); |
| 204 | if(scrollTrackerSize == 1) { |
| 205 | graphics.setCharacter(0, scrollTrackerPosition, themeDefinition.getCharacter("VERTICAL_SMALL_TRACKER", Symbols.SOLID_SQUARE_SMALL)); |
| 206 | } |
| 207 | else if(scrollTrackerSize == 2) { |
| 208 | graphics.setCharacter(0, scrollTrackerPosition, themeDefinition.getCharacter("VERTICAL_TRACKER_TOP", (char)0x28c)); |
| 209 | graphics.setCharacter(0, scrollTrackerPosition + 1, themeDefinition.getCharacter("VERTICAL_TRACKER_BOTTOM", 'v')); |
| 210 | } |
| 211 | else { |
| 212 | graphics.setCharacter(0, scrollTrackerPosition, themeDefinition.getCharacter("VERTICAL_TRACKER_TOP", (char)0x28c)); |
| 213 | graphics.drawLine(0, scrollTrackerPosition + 1, 0, scrollTrackerPosition + scrollTrackerSize - 2, themeDefinition.getCharacter("VERTICAL_TRACKER_BACKGROUND", ' ')); |
| 214 | graphics.setCharacter(0, scrollTrackerPosition + (scrollTrackerSize / 2), themeDefinition.getCharacter("VERTICAL_SMALL_TRACKER", Symbols.SOLID_SQUARE_SMALL)); |
| 215 | graphics.setCharacter(0, scrollTrackerPosition + scrollTrackerSize - 1, themeDefinition.getCharacter("VERTICAL_TRACKER_BOTTOM", 'v')); |
| 216 | } |
| 217 | } |
| 218 | } |
| 219 | else { |
| 220 | if(size.getColumns() == 1) { |
| 221 | graphics.setCharacter(0, 0, themeDefinition.getCharacter("HORIZONTAL_BACKGROUND", Symbols.BLOCK_MIDDLE)); |
| 222 | } |
| 223 | else if(size.getColumns() == 2) { |
| 224 | graphics.setCharacter(0, 0, Symbols.ARROW_LEFT); |
| 225 | graphics.setCharacter(1, 0, Symbols.ARROW_RIGHT); |
| 226 | } |
| 227 | else { |
| 228 | int scrollableArea = size.getColumns() - 2; |
| 229 | int scrollTrackerSize = 1; |
| 230 | if(growScrollTracker) { |
| 231 | float ratio = clampRatio((float) viewSize / (float) maximum); |
| 232 | scrollTrackerSize = Math.max(1, (int) (ratio * (float) scrollableArea)); |
| 233 | } |
| 234 | |
| 235 | float ratio = clampRatio((float)position / (float)(maximum - viewSize)); |
| 236 | int scrollTrackerPosition = (int)(ratio * (float)(scrollableArea - scrollTrackerSize)) + 1; |
| 237 | |
| 238 | graphics.setCharacter(0, 0, themeDefinition.getCharacter("LEFT_ARROW", Symbols.ARROW_LEFT)); |
| 239 | graphics.drawLine(1, 0, size.getColumns() - 2, 0, themeDefinition.getCharacter("HORIZONTAL_BACKGROUND", Symbols.BLOCK_MIDDLE)); |
| 240 | graphics.setCharacter(size.getColumns() - 1, 0, themeDefinition.getCharacter("RIGHT_ARROW", Symbols.ARROW_RIGHT)); |
| 241 | if(scrollTrackerSize == 1) { |
| 242 | graphics.setCharacter(scrollTrackerPosition, 0, themeDefinition.getCharacter("HORIZONTAL_SMALL_TRACKER", Symbols.SOLID_SQUARE_SMALL)); |
| 243 | } |
| 244 | else if(scrollTrackerSize == 2) { |
| 245 | graphics.setCharacter(scrollTrackerPosition, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_LEFT", '<')); |
| 246 | graphics.setCharacter(scrollTrackerPosition + 1, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_RIGHT", '>')); |
| 247 | } |
| 248 | else { |
| 249 | graphics.setCharacter(scrollTrackerPosition, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_LEFT", '<')); |
| 250 | graphics.drawLine(scrollTrackerPosition + 1, 0, scrollTrackerPosition + scrollTrackerSize - 2, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_BACKGROUND", ' ')); |
| 251 | graphics.setCharacter(scrollTrackerPosition + (scrollTrackerSize / 2), 0, themeDefinition.getCharacter("HORIZONTAL_SMALL_TRACKER", Symbols.SOLID_SQUARE_SMALL)); |
| 252 | graphics.setCharacter(scrollTrackerPosition + scrollTrackerSize - 1, 0, themeDefinition.getCharacter("HORIZONTAL_TRACKER_RIGHT", '>')); |
| 253 | } |
| 254 | } |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | private float clampRatio(float value) { |
| 259 | if(value < 0.0f) { |
| 260 | return 0.0f; |
| 261 | } |
| 262 | else if(value > 1.0f) { |
| 263 | return 1.0f; |
| 264 | } |
| 265 | else { |
| 266 | return value; |
| 267 | } |
| 268 | } |
| 269 | } |
| 270 | } |