1 /*
2 	nanogui.widget -- Base class of all widgets
3 
4 	NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch> and
5 	ported to D by Alexandr Druzhinin <drug2004@bk.ru>
6 	The widget drawing code is based on the NanoVG demo application
7 	by Mikko Mononen.
8 
9 	All rights reserved. Use of this source code is governed by a
10 	BSD-style license that can be found in the LICENSE.txt file.
11 */
12 ///
13 module nanogui.widget;
14 
15 import std.container.array;
16 
17 import nanogui.theme;
18 import nanogui.layout;
19 import nanogui.common : Cursor, Vector2i, Vector2f, MouseButton, KeyAction;
20 import nanogui.screen : Screen;
21 public import nanogui.common;
22 
23 /**
24  * Base class of all widgets.
25  *
26  * Widget is the base class of all widgets in nanogui. It can
27  * also be used as an panel to arrange an arbitrary number of child
28  * widgets using a layout generator (see `nanogui.layout.Layout`).
29  */
30 class Widget
31 {
32 public:
33 	/// Construct a new widget with the given parent widget
34 	this(Widget parent)
35 	{
36 		mVisible = true;
37 		mEnabled = true;
38 		mTooltip = ""; 
39 		mFontSize = -1;
40 		mIconExtraScale = 1.0f; 
41 		mCursor = Cursor.Arrow;
42 		if (parent)
43 			parent.addChild(this);
44 	}
45 
46 	/// Return the parent widget
47 	final Widget parent() { return mParent; }
48 	/// Return the parent widget
49 	auto parent() const { return mParent; }
50 	/// Set the parent widget
51 	final void parent(Widget parent) { mParent = parent; }
52 
53 	/// Return the used `nanogui.layout.Layout` generator
54 	final Layout layout() { return mLayout; }
55 	/// Return the used `nanogui.layout.Layout` generator
56 	auto layout() const { return mLayout; }
57 	/// Set the used `nanogui.layout.Layout` generator
58 	final void layout(Layout layout) { mLayout = layout; }
59 
60 	/// Return the `nanogui.theme.Theme` used to draw this widget
61 	final const(Theme) theme() const { return mTheme; }
62 	/// Set the `nanogui.theme.Theme` used to draw this widget
63 	void theme(Theme theme)
64 	{
65 		if (mTheme is theme)
66 			return;
67 		mTheme = theme;
68 		foreach(child; mChildren)
69 			child.theme = theme;
70 	}
71 
72 	/// Return the position relative to the parent widget
73 	final Vector2i position() const { return mPos; }
74 	/// Set the position relative to the parent widget
75 	final void position(Vector2i pos) { mPos = pos; }
76 
77 	/// Return the absolute position on screen
78 	final Vector2i absolutePosition() const
79 	{
80 		return mParent ?
81 			(parent.absolutePosition + mPos) : mPos;
82 	}
83 
84 	/// Return the size of the widget
85 	final Vector2i size() const { return mSize; }
86 	/// set the size of the widget
87 	final void size(Vector2i size) { mSize = size; }
88 
89 	/// Return the width of the widget
90 	final int width() const { return mSize.x; }
91 	/// Set the width of the widget
92 	final void width(int width) { mSize.x = width; }
93 
94 	/// Return the height of the widget
95 	final int height() const { return mSize.y; }
96 	/// Set the height of the widget
97 	final void height(int height) { mSize.y = height; }
98 
99 	/**
100 	 * Set the fixed size of this widget
101 	 *
102 	 * If nonzero, components of the fixed size attribute override any values
103 	 * computed by a layout generator associated with this widget. Note that
104 	 * just setting the fixed size alone is not enough to actually change its
105 	 * size; this is done with a call to `nanogui.widget.Widget.size` or a call to `nanogui.widget.Widget.performLayout`
106 	 * in the parent widget.
107 	 */
108 	final void fixedSize(Vector2i fixedSize) { mFixedSize = fixedSize; }
109 
110 	/// Return the fixed size
111 	final Vector2i fixedSize() const { return mFixedSize; }
112 
113 	/// Return the fixed width (see `fixedSize`)
114 	final int fixedWidth() const { return mFixedSize.x; }
115 	/// Return the fixed height (see `fixedSize`)
116 	final int fixedHeight() const { return mFixedSize.y; }
117 	/// Set the fixed width (see `fixedSize`)
118 	final void fixedWidth(int width) { mFixedSize.x = width; }
119 	/// Set the fixed height (see `fixedSize`)
120 	final void fixedHeight(int height) { mFixedSize.y = height; }
121 
122 	/// Return whether or not the widget is currently visible (assuming all parents are visible)
123 	final bool visible() const { return mVisible; }
124 	/// Set whether or not the widget is currently visible (assuming all parents are visible)
125 	final void visible(bool visible) { mVisible = visible; }
126 
127 	/// Check if this widget is currently visible, taking parent widgets into account
128 	final bool visibleRecursive() const {
129 		import std.typecons : Rebindable;
130 		bool visible = true;
131 		Rebindable!(const Widget) widget = this;
132 		while (widget) {
133 			visible &= widget.visible;
134 			widget = widget.parent;
135 		}
136 		return visible;
137 	}
138 
139 	/// Return the number of child widgets
140 	final int childCount() const
141 	{
142 		import std.conv : castFrom;
143 		return castFrom!size_t.to!int(mChildren.length);
144 	}
145 
146 	/// Return the list of child widgets of the current widget
147 	auto children() { return mChildren; }
148 	/// ditto
149 	auto children() const { return mChildren; }
150 
151 	/**
152 	* Add a child widget to the current widget at
153 	* the specified index.
154 	*
155 	* This function almost never needs to be called by hand,
156 	* since the constructor of `Widget` automatically
157 	* adds the current widget to its parent
158 	*/
159 	void addChild(int index, Widget widget)
160 	{
161 		assert(index <= childCount);
162 		mChildren.insertBefore(mChildren[index..$], widget);
163 		widget.parent = this;
164 		widget.theme = mTheme;
165 	}
166 
167 	/// Convenience function which appends a widget at the end
168 	void addChild(Widget widget)
169 	{
170 		addChild(childCount(), widget);
171 	}
172 
173 	/// Remove a child widget by index
174 	void removeChild(int index)
175 	{
176 		import std.range : takeOne;
177 		mChildren.linearRemove(mChildren[index..$].takeOne);
178 	}
179 
180 	/// Remove a child widget by value
181 	void removeChild(Widget widget)
182 	{
183 		import std.algorithm : find;
184 		import std.range : takeOne;
185 		mChildren.linearRemove(mChildren[].find(widget).takeOne);
186 	}
187 
188 	/// Retrieves the child at the specific position
189 	auto childAt(int index) const { return mChildren[index]; }
190 
191 	/// Retrieves the child at the specific position
192 	auto childAt(int index) { return mChildren[index]; }
193 
194 	/// Returns the index of a specific child or -1 if not found
195 	int childIndex(Widget widget) const
196 	{
197 		import std.algorithm : countUntil;
198 		return cast(int) countUntil!(a=>a is widget)(mChildren[]);
199 	}
200 
201 	/// Variadic shorthand notation to construct and add a child widget
202 	auto add(W, Args...)(Args args)
203 	{
204 		return new WidgetClass(this, args);
205 	}
206 
207 	/// Walk up the hierarchy and return the parent window
208 	final Window window()
209 	{
210 		Widget widget = this;
211 		while (true) {
212 			if (!widget)
213 				throw new Exception(
214 					"Widget:internal error (could not find parent window)");
215 			Window window = cast(Window)(widget);
216 			if (window)
217 				return window;
218 			widget = widget.parent;
219 		}
220 	}
221 
222 	/// Walk up the hierarchy and return the parent screen
223 	final Screen screen()
224 	{
225 		auto widget = this;
226 		while (true) {
227 			if (!widget)
228 				throw new Exception(
229 					"Widget:internal error (could not find parent screen)");
230 			auto screen = cast(Screen) widget;
231 			if (screen)
232 				return screen;
233 			widget = widget.parent;
234 		}
235 	}
236 
237 	/// Associate this widget with an ID value (optional)
238 	void setId(string id) { mId = id; }
239 	/// Return the ID value associated with this widget, if any
240 	auto id() const { return mId; }
241 
242 	/// Return whether or not this widget is currently enabled
243 	final bool enabled() const { return mEnabled; }
244 	/// Set whether or not this widget is currently enabled
245 	final void enabled(bool enabled) { mEnabled = enabled; }
246 
247 	/// Return whether or not this widget is currently focused
248 	bool focused() const { return mFocused; }
249 	/// Set whether or not this widget is currently focused
250 	void focused(bool focused) { mFocused = focused; }
251 	/// Request the focus to be moved to this widget
252 	void requestFocus()
253 	{
254 		import nanogui.screen : Screen;
255 		Widget widget = this;
256 		while (widget.parent())
257 			widget = widget.parent();
258 		(cast(Screen) widget).updateFocus(this);
259 	}
260 
261 	string tooltip() const { return mTooltip; }
262 	void tooltip(string tooltip) { mTooltip = tooltip; }
263 
264 	/// Return current font size. If not set the default of the current theme will be returned
265 	final int fontSize() const
266 	{
267 		return (mFontSize < 0 && mTheme) ? mTheme.mStandardFontSize : mFontSize;
268 	}
269 	/// Set the font size of this widget
270 	final void fontSize(int fontSize) { mFontSize = fontSize; }
271 	/// Return whether the font size is explicitly specified for this widget
272 	final bool hasFontSize() const { return mFontSize > 0; }
273 
274 	/**
275 	* The amount of extra scaling applied to *icon* fonts.
276 	* See `nanogui.Widget.mIconExtraScale`.
277 	*/
278 	float iconExtraScale() const { return mIconExtraScale; }
279 
280 	/**
281 	* Sets the amount of extra scaling applied to *icon* fonts.
282 	* See `nanogui.Widget.mIconExtraScale`.
283 	*/
284 	void iconExtraScale(float scale) { mIconExtraScale = scale; }
285 
286 	/// Return a pointer to the cursor of the widget
287 	Cursor cursor() const { return mCursor; }
288 	/// Set the cursor of the widget
289 	void cursor(Cursor value) { mCursor = value; }
290 
291 	/// Check if the widget contains a certain position
292 	final bool contains(Vector2i p) const {
293 		import std.algorithm : all;
294 		// the widget contains a position if it more than
295 		// the widget position and less than widget position
296 		// + widget size
297 		auto d = (p-mPos);
298 		return d[].all!"a>=0" && (d-mSize)[].all!"a<=0";
299 	}
300 
301 	/// Determine the widget located at the given position value (recursive)
302 	Widget findWidget(Vector2i p)
303 	{
304 		foreach_reverse(child; mChildren)
305 		{
306 			if (child.visible() && child.contains(p - mPos))
307 				return child.findWidget(p - mPos);
308 		}
309 		return contains(p) ? this : null;
310 	}
311 
312 	/// Handle a mouse button event (default implementation: propagate to children)
313 	bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers)
314 	{
315 		foreach_reverse(ch; mChildren)
316 		{
317 			Widget child = ch;
318 			if (child.visible && child.contains(p - mPos) &&
319 				child.mouseButtonEvent(p - mPos, button, down, modifiers))
320 				return true;
321 		}
322 		if (button == MouseButton.Left && down && !mFocused)
323 			requestFocus();
324 		return false;
325 	}
326 
327 	/// Handle a mouse motion event (default implementation: propagate to children)
328 	bool mouseMotionEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers)
329 	{
330 		foreach_reverse(it; mChildren)
331 		{
332 			Widget child = it;
333 			if (!child.visible)
334 				continue;
335 			const contained = child.contains(p - mPos);
336 			const prevContained = child.contains(p - mPos - rel);
337 			if (contained != prevContained)
338 				child.mouseEnterEvent(p, contained);
339 			if ((contained || prevContained) &&
340 				child.mouseMotionEvent(p - mPos, rel, button, modifiers))
341 				return true;
342 		}
343 		return false;
344 	}
345 
346 	/// Handle a mouse drag event (default implementation: do nothing)
347 	bool mouseDragEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers)
348 	{
349 		return false;
350 	}
351 
352 	/// Handle a mouse enter/leave event (default implementation: record this fact, but do nothing)
353 	bool mouseEnterEvent(Vector2i p, bool enter)
354 	{
355 		mMouseFocus = enter;
356 		return false;
357 	}
358 
359 	/// Handle a mouse scroll event (default implementation: propagate to children)
360 	bool scrollEvent(Vector2i p, Vector2f rel)
361 	{
362 		foreach_reverse (child; mChildren)
363 		{
364 			if (!child.visible)
365 				continue;
366 			if (child.contains(p - mPos) && child.scrollEvent(p - mPos, rel))
367 				return true;
368 		}
369 		return false;
370 	}
371 
372 	/// Handle a focus change event (default implementation: record the focus status, but do nothing)
373 	bool focusEvent(bool focused)
374 	{
375 		mFocused = focused;
376 		return false;
377 	}
378 
379 	/// Handle a keyboard event (default implementation: do nothing)
380 	bool keyboardEvent(int key, int scancode, KeyAction action, int modifiers)
381 	{
382 		return false;
383 	}
384 
385 	/// Handle text input (UTF-32 format) (default implementation: do nothing)
386 	bool keyboardCharacterEvent(dchar codepoint)
387 	{
388 		return false;
389 	}
390 
391 	/// Compute the preferred size of the widget
392 	Vector2i preferredSize(NanoContext ctx) const
393 	{
394 		if (mLayout)
395 			return mLayout.preferredSize(ctx, this);
396 		else
397 			return mSize;
398 	}
399 
400 	/// Compute the preferred size of the widget considering its child except
401 	/// skipped one (for example button panel of window)
402 	final Vector2i preferredSize(NanoContext ctx, const Widget skipped) const
403 	{
404 		if (mLayout)
405 			return mLayout.preferredSize(ctx, this, skipped);
406 		else
407 			return mSize;
408 	}
409 
410 	/// Invoke the associated layout generator to properly place child widgets, if any
411 	void performLayout(NanoContext ctx)
412 	{
413 		if (mLayout) {
414 			mLayout.performLayout(ctx, this);
415 		} else {
416 			foreach(c; mChildren) {
417 				Vector2i pref = c.preferredSize(ctx), fix = c.fixedSize();
418 				c.size(Vector2i(
419 					fix[0] ? fix[0] : pref[0],
420 					fix[1] ? fix[1] : pref[1]
421 				));
422 				c.performLayout(ctx);
423 			}
424 		}
425 	}
426 
427 	/// Draw the widget (and all child widgets)
428 	void draw(ref NanoContext ctx)
429 	{
430 		version(NANOGUI_SHOW_WIDGET_BOUNDS)
431 		{
432 			ctx.strokeWidth(1.0f);
433 			ctx.beginPath;
434 			ctx.rect(mPos.x + 1.0f, mPos.y + 0.0f, mSize.x - 1, mSize.y - 1);
435 			ctx.strokeColor(Color(255, 0, 0, 255));
436 			ctx.stroke;
437 		}
438 
439 		if (mChildren.length == 0)
440 			return;
441 
442 		ctx.save;
443 		ctx.translate(mPos.x, mPos.y);
444 		foreach(child; mChildren)
445 		{
446 			if (child.visible)
447 			{
448 				ctx.save;
449 				scope(exit) ctx.restore;
450 				ctx.intersectScissor(child.mPos.x, child.mPos.y, child.mSize.x, child.mSize.y);
451 				child.draw(ctx);
452 			}
453 		}
454 		ctx.restore;
455 	}
456 
457 // // Save the state of the widget into the given \ref Serializer instance
458 //virtual void save(Serializer &s) const;
459 
460 // // Restore the state of the widget from the given \ref Serializer instance
461 //virtual bool load(Serializer &s);
462 
463 protected:
464 	/// Free all resources used by the widget and any children
465 	~this()
466 	{
467 //foreach(child; mChildren) {
468 //    if (child)
469 //        child.decRef();
470 //}
471 	}
472 
473 	/**
474 	 * Convenience definition for subclasses to get the full icon scale for this
475 	 * class of Widget.  It simple returns the value
476 	 * `mTheme.mIconScale * this.mIconExtraScale`.
477 	 *
478 	 * See_also:
479 	 *     `Theme.mIconScale` and `Widget.mIconExtraScale`.  This tiered scaling
480 	 *     strategy may not be appropriate with fonts other than `entypo.ttf`.
481 	 */
482 	pragma(inline, true)
483 	float icon_scale() const { return mTheme.mIconScale * mIconExtraScale; }
484 
485 	Widget mParent;
486 	Theme mTheme;
487 	Layout mLayout;
488 	string mId;
489 	Vector2i mPos, mSize, mFixedSize;
490 	Array!Widget mChildren;
491 
492 	/**
493 	 * Whether or not this Widget is currently visible.  When a Widget is not
494 	 * currently visible, no time is wasted executing its drawing method.
495 	 */
496 	bool mVisible;
497 
498 	/**
499 	 * Whether or not this Widget is currently enabled.  Various different kinds
500 	 * of derived types use this to determine whether or not user input will be
501 	 * accepted.  For example, when ``mEnabled == false``, the state of a
502 	 * CheckBox cannot be changed, or a TextBox will not allow new input.
503 	 */
504 	bool mEnabled;
505 	bool mFocused, mMouseFocus;
506 	string mTooltip;
507 	int mFontSize;
508 
509 	/**
510 	 * The amount of extra icon scaling used in addition the the theme's
511 	 * default icon font scale.  Default value is ``1.0``, which implies
512 	 * that `icon_scale` simply returns the value of `nanogui.Theme.mIconScale`.
513 	 *
514 	 * Most widgets do not need extra scaling, but some (e.g., `CheckBox`, `TextBox`)
515 	 * need to adjust the Theme's default icon scaling
516 	 * `nanogui.Theme.mIconScale` to properly display icons within their
517 	 * bounds (upscale, or downscale).
518 	 *
519 	 * Summary:
520 	 *
521 	 *    When using `nvgFontSize` for icons in subclasses, make sure to call
522 	 *    the `icon_scale` function.  Expected usage when *drawing* icon fonts
523 	 *    is something like:
524 	 *
525 	 * ---
526 	 *
527 	 *       void draw(NanoContext ctx)
528 	 *       {
529 	 *           // fontSize depends on the kind of `Widget`.  Search for `FontSize`
530 	 *           // in the `Theme` class (e.g., standard vs button)
531 	 *           float ih = fontSize;
532 	 *           // assuming your Widget has a declared `mIcon`
533 	 *           if (isFontIcon(mIcon)) {
534 	 *               ih *= icon_scale();
535 	 *               ctx.fontFace("icons");
536 	 *               ctx.fontSize(ih);
537 	 *               /// remaining drawing code (see button.d for more)
538 	 *           }
539 	 *       }
540 	 * ---
541 	 */
542 	float mIconExtraScale;
543 	Cursor mCursor;
544 }