Commit | Line | Data |
---|---|---|
a3b510ab NR |
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 | } |