Skip to content
Dinkydau edited this page May 24, 2021 · 5 revisions

What is a scroll

A scroll in itself doesn't do anything. You can add one to a form like any other widget. You would then have to program everything that needs to happen when the scroll is used. That can be done in part by providing an event handler. Let scroll be a scroll<true> (i.e., a vertical scroll; false would be a horizontal scroll). An event handler can be set this way:

scroll.events().value_changed([&, this](const arg_scroll& arg)
{
	//what has to be done when the bar was dragged or otherwise moved
});

This is so general that you could in theory use a scroll as a kind of slider. That would not be intuitive for the user, but the point is that you can do anything you want.

The typical usage, however, is to scroll through something in the form that doesn't fit in the allotted space. For that use case, the functions on scroll have the following meaning.

function call effect meaning
scroll.amount(value) The position of the draggable bar is measured in the range 0 - value. value is the height of the content to scroll through
scroll.range(value) Sets the size of the draggable bar in the scrollbar (less than or equal to the amount) value is the height of the part of the content that is visible
scroll.step(value) how much to scroll when the mouse wheel is used (compared to the amount) how much to move the visible part of the content

Example usage of scroll

This is a class that can contain any panel with a vertical scroll:

using namespace nana;

template <typename ContentPanel>
class ScrollPanel
{
public:
	class VisiblePart : public panel<false>
	{ public:
		place pl{ *this };
		scroll<true> vscrollbar{ *this, rectangle() };
		VisiblePart(window wd) : panel<false>(wd)
		{
			pl.div("<content><scroll weight=15>");
			pl["scroll"] << vscrollbar;
			pl.collocate();
		}
	};
	
	VisiblePart visible;
	ContentPanel content;

	ScrollPanel(window wd)
	: visible(wd)
	, content(visible)
	{
		static_assert(
			is_base_of<panel<true>, ContentPanel>::value
			|| is_base_of<panel<false>, ContentPanel>::value
		, "ContentPanel must be a nana::panel");

		scroll<true>& scroll = visible.vscrollbar;
		scroll.step(40);

		scroll.events().value_changed([&, this](const arg_scroll& arg)
		{
			content.move(0, -1 * scroll.value());
		});

		visible.events().resized([&, this](const arg_resized& arg)
		{
			recalculateScrollbar(arg.height);
		});
	}
	/*
		measures the height of the content by looking at every widget in the panel
	*/
	unsigned contentHeight() {
		unsigned height = 0;
		API::enum_widgets(content, false, [&](widget& wdg)
		{
			height = max({height, wdg.pos().y + wdg.size().height});
		});
		return height;
	}

	unsigned contentWidth() {
		return visible.size().width - visible.vscrollbar.size().width;
	}

	void recalculateScrollbar(int visibleHeight)
	{
		scroll<true>& scroll = visible.vscrollbar;

		unsigned height = contentHeight();

		//When the window is enlarged, this makes sure the new space is used by the panel by adjusting the displacement. Without this, the panel remains scrolled.
		int panel_displacement = scroll.value();
		int emptyPanelSpaceY = visible.size().height - (height - panel_displacement);
		if (emptyPanelSpaceY > 0) {
			panel_displacement = max({0, panel_displacement - emptyPanelSpaceY});
			content.move(0, -1 * panel_displacement);
		}
		
		scroll.range(visibleHeight);
		scroll.amount(height);

		//Hide the scrollbar when the content fits in the visible part.
		if (visibleHeight >= height) {
			visible.pl.modify("scroll", "weight=0");
			visible.pl.collocate();
			//scroll.hide(); //hiding causes 1 frame where the scrollbar turns black before the collocate takes place. I don't know why, and I don't know if it matters if the scrollbar is not hidden while it's 0 pixels wide.
		}
		else {
			//scroll.show();
			visible.pl.modify("scroll", "weight=15");
			visible.pl.collocate();
		}
		
		unsigned width = contentWidth();
		content.size(nana::size(width, height));
	}
};

Usage:

  1. Create a class (for example named MyPanel) for the kind of panel that you want to scroll through.
  2. Instantiate a ScrollPanel<MyPanel> (for example named scrollPanel).
  3. add scrollPanel.visible to the form or panel where you want to see it. Example:
class MyPanel : public panel<true> {
	...
};

class MyForm : public form {
	place pl{ *this };
	ScrollPanel<MyPanel> scrollPanel{ *this };
	MyForm() {	
		pl.div("<x>");
		pl["x"] << scrollPanel.visible; //add the visible part to the form
	}
};

How it works

The parent of "visible" is the window wd. The parent of "content" is "visible" (see the ScrollPanel initialization list). That means the content is not directly displayed on the window. Only the visible part is directly displayed on the window. The visible part contains the content as a widget. Like any other widget that is too large to fit in a panel, only a part of it can be seen.

The scrollbar is programmed to move the content around on the VisiblePart panel, which affects the part that can be seen. The most important work is done by the move function.