⛏️ index : haiku.git

/*
 * Copyright 2001-2022 Haiku, Inc. All Rights Reserved.
 * Distributed under the terms of the MIT license.
 *
 * Authors:
 *		Stephan Aßmus, superstippi@gmx.de
 *		DarkWyrm, bpmagic@columbus.rr.com
 *		Axel Dörfler, axeld@pinc-software.de
 *		Marc Flerackers, mflerackers@androme.be
 *		John Scipione, jscipione@gmail.com
 */


#include <Box.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <ControlLook.h>
#include <Layout.h>
#include <LayoutUtils.h>
#include <Message.h>
#include <Region.h>

#include <binary_compatibility/Interface.h>


struct BBox::LayoutData {
	LayoutData()
		: valid(false)
	{
	}

	BRect	label_box;		// label box (label string or label view); in case
							// of a label string not including descent
	BRect	insets;			// insets induced by border and label
	BSize	min;
	BSize	max;
	BSize	preferred;
	BAlignment alignment;
	bool	valid;			// validity the other fields
};


BBox::BBox(BRect frame, const char* name, uint32 resizingMode, uint32 flags,
		border_style border)
	:
	BView(frame, name, resizingMode, flags  | B_WILL_DRAW | B_FRAME_EVENTS),
	  fStyle(border)
{
	_InitObject();
}


BBox::BBox(const char* name, uint32 flags, border_style border, BView* child)
	:
	BView(name, flags | B_WILL_DRAW | B_FRAME_EVENTS),
	fStyle(border)
{
	_InitObject();

	if (child)
		AddChild(child);
}


BBox::BBox(border_style border, BView* child)
	:
	BView(NULL, B_WILL_DRAW | B_FRAME_EVENTS | B_NAVIGABLE_JUMP),
	fStyle(border)
{
	_InitObject();

	if (child)
		AddChild(child);
}


BBox::BBox(BMessage* archive)
	:
	BView(archive),
	fStyle(B_FANCY_BORDER)
{
	_InitObject(archive);
}


BBox::~BBox()
{
	_ClearLabel();

	delete fLayoutData;
}


BArchivable*
BBox::Instantiate(BMessage* archive)
{
	if (validate_instantiation(archive, "BBox"))
		return new BBox(archive);

	return NULL;
}


status_t
BBox::Archive(BMessage* archive, bool deep) const
{
	status_t ret = BView::Archive(archive, deep);

	if (fLabel && ret == B_OK)
		ret = archive->AddString("_label", fLabel);

	if (fLabelView && ret == B_OK)
		ret = archive->AddBool("_lblview", true);

	if (fStyle != B_FANCY_BORDER && ret == B_OK)
		ret = archive->AddInt32("_style", fStyle);

	return ret;
}


void
BBox::SetBorder(border_style border)
{
	if (border == fStyle)
		return;

	fStyle = border;

	InvalidateLayout();

	if (Window() != NULL && LockLooper()) {
		Invalidate();
		UnlockLooper();
	}
}


border_style
BBox::Border() const
{
	return fStyle;
}


//! This function is not part of the R5 API and is not yet finalized yet
float
BBox::TopBorderOffset()
{
	_ValidateLayoutData();

	if (fLabel != NULL || fLabelView != NULL)
		return fLayoutData->label_box.Height() / 2;

	return 0;
}


//! This function is not part of the R5 API and is not yet finalized yet
BRect
BBox::InnerFrame()
{
	_ValidateLayoutData();

	BRect frame(Bounds());
	frame.left += fLayoutData->insets.left;
	frame.top += fLayoutData->insets.top;
	frame.right -= fLayoutData->insets.right;
	frame.bottom -= fLayoutData->insets.bottom;

	return frame;
}


void
BBox::SetLabel(const char* string)
{
	_ClearLabel();

	if (string)
		fLabel = strdup(string);

	InvalidateLayout();

	if (Window())
		Invalidate();
}


status_t
BBox::SetLabel(BView* viewLabel)
{
	_ClearLabel();

	if (viewLabel) {
		fLabelView = viewLabel;
		fLabelView->MoveTo(10.0f, 0.0f);
		AddChild(fLabelView, ChildAt(0));
	}

	InvalidateLayout();

	if (Window())
		Invalidate();

	return B_OK;
}


const char*
BBox::Label() const
{
	return fLabel;
}


BView*
BBox::LabelView() const
{
	return fLabelView;
}


void
BBox::Draw(BRect updateRect)
{
	_ValidateLayoutData();

	PushState();

	BRect labelBox = BRect(0, 0, 0, 0);
	if (fLabel != NULL) {
		labelBox = fLayoutData->label_box;
		BRegion update(updateRect);
		update.Exclude(labelBox);

		ConstrainClippingRegion(&update);
	} else if (fLabelView != NULL)
		labelBox = fLabelView->Bounds();

	switch (fStyle) {
		case B_FANCY_BORDER:
			_DrawFancy(labelBox);
			break;

		case B_PLAIN_BORDER:
			_DrawPlain(labelBox);
			break;

		default:
			break;
	}

	if (fLabel != NULL) {
		ConstrainClippingRegion(NULL);

		font_height fontHeight;
		GetFontHeight(&fontHeight);

		// offset label up by 1/6 the font height
		float lineHeight = fontHeight.ascent + fontHeight.descent;
		float yOffset = roundf(lineHeight / 6.0f);

		SetHighColor(ui_color(B_PANEL_TEXT_COLOR));
		DrawString(fLabel, BPoint(10.0f, fontHeight.ascent - yOffset));
	}

	PopState();
}


void
BBox::AttachedToWindow()
{
	AdoptParentColors();

	// Force low color to match view color for proper label drawing.
	float viewTint = B_NO_TINT;
	float lowTint = B_NO_TINT;

	if (LowUIColor(&lowTint) != ViewUIColor(&viewTint) || viewTint != lowTint)
		SetLowUIColor(ViewUIColor(), viewTint);
	else if (LowColor() != ViewColor())
		SetLowColor(ViewColor());

	if (ViewColor() == B_TRANSPARENT_COLOR)
		AdoptSystemColors();

	// The box could have been resized in the mean time
	fBounds = Bounds().OffsetToCopy(0, 0);
}


void
BBox::DetachedFromWindow()
{
	BView::DetachedFromWindow();
}


void
BBox::AllAttached()
{
	BView::AllAttached();
}


void
BBox::AllDetached()
{
	BView::AllDetached();
}


void
BBox::FrameResized(float width, float height)
{
	BRect bounds(Bounds());

	// invalidate the regions that the app_server did not
	// (for removing the previous or drawing the new border)
	if (fStyle != B_NO_BORDER) {
		// TODO: this must be made part of the be_control_look stuff!
		int32 borderSize = fStyle == B_PLAIN_BORDER ? 0 : 2;

		// Horizontal
		BRect invalid(bounds);
		if (fBounds.Width() < bounds.Width()) {
			// enlarging
			invalid.left = bounds.left + fBounds.right - borderSize;
			invalid.right = bounds.left + fBounds.right;

			Invalidate(invalid);
		} else if (fBounds.Width() > bounds.Width()) {
			// shrinking
			invalid.left = bounds.left + bounds.right - borderSize;

			Invalidate(invalid);
		}

		// Vertical
		invalid = bounds;
		if (fBounds.Height() < bounds.Height()) {
			// enlarging
			invalid.top = bounds.top + fBounds.bottom - borderSize;
			invalid.bottom = bounds.top + fBounds.bottom;

			Invalidate(invalid);
		} else if (fBounds.Height() > bounds.Height()) {
			// shrinking
			invalid.top = bounds.top + bounds.bottom - borderSize;

			Invalidate(invalid);
		}
	}

	fBounds.right = width;
	fBounds.bottom = height;
}


void
BBox::MessageReceived(BMessage* message)
{
	BView::MessageReceived(message);
}


void
BBox::MouseDown(BPoint point)
{
	BView::MouseDown(point);
}


void
BBox::MouseUp(BPoint point)
{
	BView::MouseUp(point);
}


void
BBox::WindowActivated(bool active)
{
	BView::WindowActivated(active);
}


void
BBox::MouseMoved(BPoint point, uint32 transit, const BMessage* message)
{
	BView::MouseMoved(point, transit, message);
}


void
BBox::FrameMoved(BPoint newLocation)
{
	BView::FrameMoved(newLocation);
}


BHandler*
BBox::ResolveSpecifier(BMessage* message, int32 index, BMessage* specifier,
	int32 what, const char* property)
{
	return BView::ResolveSpecifier(message, index, specifier, what, property);
}


void
BBox::ResizeToPreferred()
{
	float width, height;
	GetPreferredSize(&width, &height);

	// make sure the box don't get smaller than it already is
	if (width < Bounds().Width())
		width = Bounds().Width();
	if (height < Bounds().Height())
		height = Bounds().Height();

	BView::ResizeTo(width, height);
}


void
BBox::GetPreferredSize(float* _width, float* _height)
{
	_ValidateLayoutData();

	if (_width)
		*_width = fLayoutData->preferred.width;
	if (_height)
		*_height = fLayoutData->preferred.height;
}


void
BBox::MakeFocus(bool focused)
{
	BView::MakeFocus(focused);
}


status_t
BBox::GetSupportedSuites(BMessage* message)
{
	return BView::GetSupportedSuites(message);
}


status_t
BBox::Perform(perform_code code, void* _data)
{
	switch (code) {
		case PERFORM_CODE_MIN_SIZE:
			((perform_data_min_size*)_data)->return_value
				= BBox::MinSize();
			return B_OK;
		case PERFORM_CODE_MAX_SIZE:
			((perform_data_max_size*)_data)->return_value
				= BBox::MaxSize();
			return B_OK;
		case PERFORM_CODE_PREFERRED_SIZE:
			((perform_data_preferred_size*)_data)->return_value
				= BBox::PreferredSize();
			return B_OK;
		case PERFORM_CODE_LAYOUT_ALIGNMENT:
			((perform_data_layout_alignment*)_data)->return_value
				= BBox::LayoutAlignment();
			return B_OK;
		case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
			((perform_data_has_height_for_width*)_data)->return_value
				= BBox::HasHeightForWidth();
			return B_OK;
		case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
		{
			perform_data_get_height_for_width* data
				= (perform_data_get_height_for_width*)_data;
			BBox::GetHeightForWidth(data->width, &data->min, &data->max,
				&data->preferred);
			return B_OK;
		}
		case PERFORM_CODE_SET_LAYOUT:
		{
			perform_data_set_layout* data = (perform_data_set_layout*)_data;
			BBox::SetLayout(data->layout);
			return B_OK;
		}
		case PERFORM_CODE_LAYOUT_INVALIDATED:
		{
			perform_data_layout_invalidated* data
				= (perform_data_layout_invalidated*)_data;
			BBox::LayoutInvalidated(data->descendants);
			return B_OK;
		}
		case PERFORM_CODE_DO_LAYOUT:
		{
			BBox::DoLayout();
			return B_OK;
		}
	}

	return BView::Perform(code, _data);
}


BSize
BBox::MinSize()
{
	_ValidateLayoutData();

	BSize size = (GetLayout() ? GetLayout()->MinSize() : fLayoutData->min);
	if (size.width < fLayoutData->min.width)
		size.width = fLayoutData->min.width;
	return BLayoutUtils::ComposeSize(ExplicitMinSize(), size);
}


BSize
BBox::MaxSize()
{
	_ValidateLayoutData();

	BSize size = (GetLayout() ? GetLayout()->MaxSize() : fLayoutData->max);
	return BLayoutUtils::ComposeSize(ExplicitMaxSize(), size);
}


BSize
BBox::PreferredSize()
{
	_ValidateLayoutData();

	BSize size = (GetLayout() ? GetLayout()->PreferredSize()
		: fLayoutData->preferred);
	return BLayoutUtils::ComposeSize(ExplicitPreferredSize(), size);
}


BAlignment
BBox::LayoutAlignment()
{
	_ValidateLayoutData();

	BAlignment alignment = (GetLayout() ? GetLayout()->Alignment()
			: fLayoutData->alignment);
	return BLayoutUtils::ComposeAlignment(ExplicitAlignment(), alignment);
}


void
BBox::LayoutInvalidated(bool descendants)
{
	fLayoutData->valid = false;
}


void
BBox::DoLayout()
{
	// Bail out, if we shan't do layout.
	if (!(Flags() & B_SUPPORTS_LAYOUT))
		return;

	BLayout* layout = GetLayout();

	// If the user set a layout, let the base class version call its
	// hook. In case when we have BView as a label, remove it from child list
	// so it won't be layouted with the rest of views and add it again
	// after that.
	if (layout != NULL) {
		if (fLabelView)
			RemoveChild(fLabelView);

		BView::DoLayout();

		if (fLabelView != NULL) {
			DisableLayoutInvalidation();
				// don't trigger a relayout
			AddChild(fLabelView, ChildAt(0));
			EnableLayoutInvalidation();
		}
	}

	_ValidateLayoutData();

	// Even if the user set a layout, restore label view to it's
	// desired position.

	// layout the label view
	if (fLabelView != NULL) {
		fLabelView->MoveTo(fLayoutData->label_box.LeftTop());
		fLabelView->ResizeTo(fLayoutData->label_box.Size());
	}

	// If we have layout return here and do not layout the child
	if (layout != NULL)
		return;

	// layout the child
	BView* child = _Child();
	if (child != NULL) {
		BRect frame(Bounds());
		frame.left += fLayoutData->insets.left;
		frame.top += fLayoutData->insets.top;
		frame.right -= fLayoutData->insets.right;
		frame.bottom -= fLayoutData->insets.bottom;

		if ((child->Flags() & B_SUPPORTS_LAYOUT) != 0)
			BLayoutUtils::AlignInFrame(child, frame);
		else
			child->MoveTo(frame.LeftTop());
	}
}


void BBox::_ReservedBox1() {}
void BBox::_ReservedBox2() {}


BBox &
BBox::operator=(const BBox &)
{
	return *this;
}


void
BBox::_InitObject(BMessage* archive)
{
	fBounds = Bounds().OffsetToCopy(0, 0);

	fLabel = NULL;
	fLabelView = NULL;
	fLayoutData = new LayoutData;

	int32 flags = 0;

	BFont font(be_bold_font);

	if (!archive || !archive->HasString("_fname"))
		flags = B_FONT_FAMILY_AND_STYLE;

	if (!archive || !archive->HasFloat("_fflt"))
		flags |= B_FONT_SIZE;

	if (flags != 0)
		SetFont(&font, flags);

	if (archive != NULL) {
		const char* string;
		if (archive->FindString("_label", &string) == B_OK)
			SetLabel(string);

		bool fancy;
		int32 style;

		if (archive->FindBool("_style", &fancy) == B_OK)
			fStyle = fancy ? B_FANCY_BORDER : B_PLAIN_BORDER;
		else if (archive->FindInt32("_style", &style) == B_OK)
			fStyle = (border_style)style;

		bool hasLabelView;
		if (archive->FindBool("_lblview", &hasLabelView) == B_OK)
			fLabelView = ChildAt(0);
	}

	AdoptSystemColors();
}


void
BBox::_DrawPlain(BRect labelBox)
{
	BRect rect = Bounds();
	rect.top += TopBorderOffset();

	float lightTint;
	float shadowTint;
	lightTint = B_LIGHTEN_1_TINT;
	shadowTint = B_DARKEN_1_TINT;

	if (rect.Height() == 0.0 || rect.Width() == 0.0) {
		// used as separator
		rgb_color shadow = tint_color(ViewColor(), B_DARKEN_2_TINT);

		SetHighColor(shadow);
		StrokeLine(rect.LeftTop(),rect.RightBottom());
	} else {
		// used as box
		rgb_color light = tint_color(ViewColor(), lightTint);
		rgb_color shadow = tint_color(ViewColor(), shadowTint);

		BeginLineArray(4);
			AddLine(BPoint(rect.left, rect.bottom),
					BPoint(rect.left, rect.top), light);
			AddLine(BPoint(rect.left + 1.0f, rect.top),
					BPoint(rect.right, rect.top), light);
			AddLine(BPoint(rect.left + 1.0f, rect.bottom),
					BPoint(rect.right, rect.bottom), shadow);
			AddLine(BPoint(rect.right, rect.bottom - 1.0f),
					BPoint(rect.right, rect.top + 1.0f), shadow);
		EndLineArray();
	}
}


void
BBox::_DrawFancy(BRect labelBox)
{
	BRect rect = Bounds();
	rect.top += TopBorderOffset();

	rgb_color base = ViewColor();
	if (rect.Height() == 1.0) {
		// used as horizontal separator
		be_control_look->DrawGroupFrame(this, rect, rect, base,
			BControlLook::B_TOP_BORDER);
	} else if (rect.Width() == 1.0) {
		// used as vertical separator
		be_control_look->DrawGroupFrame(this, rect, rect, base,
			BControlLook::B_LEFT_BORDER);
	} else {
		// used as box
		be_control_look->DrawGroupFrame(this, rect, rect, base);
	}
}


void
BBox::_ClearLabel()
{
	if (fLabel) {
		free(fLabel);
		fLabel = NULL;
	} else if (fLabelView) {
		fLabelView->RemoveSelf();
		delete fLabelView;
		fLabelView = NULL;
	}
}


BView*
BBox::_Child() const
{
	for (int32 i = 0; BView* view = ChildAt(i); i++) {
		if (view != fLabelView)
			return view;
	}

	return NULL;
}


void
BBox::_ValidateLayoutData()
{
	if (fLayoutData->valid)
		return;

	// compute the label box, width and height
	bool label = true;
	float labelHeight = 0;	// height of the label (pixel count)
	if (fLabel) {
		// leave 6 pixels of the frame, and have a gap of 4 pixels between
		// the frame and the text on either side
		font_height fontHeight;
		GetFontHeight(&fontHeight);
		fLayoutData->label_box.Set(6.0f, 0, 14.0f + StringWidth(fLabel),
			ceilf(fontHeight.ascent));
		labelHeight = ceilf(fontHeight.ascent + fontHeight.descent) + 1;
	} else if (fLabelView) {
		// the label view is placed at (0, 10) at its preferred size
		BSize size = fLabelView->PreferredSize();
		fLayoutData->label_box.Set(10, 0, 10 + size.width, size.height);
		labelHeight = size.height + 1;
	} else
		label = false;

	// border
	switch (fStyle) {
		case B_PLAIN_BORDER:
			fLayoutData->insets.Set(1, 1, 1, 1);
			break;
		case B_FANCY_BORDER:
			fLayoutData->insets.Set(3, 3, 3, 3);
			break;
		case B_NO_BORDER:
		default:
			fLayoutData->insets.Set(0, 0, 0, 0);
			break;
	}

	// if there's a label, the top inset will be dictated by the label
	if (label && labelHeight > fLayoutData->insets.top)
		fLayoutData->insets.top = labelHeight;

	// total number of pixel the border adds
	float addWidth = fLayoutData->insets.left + fLayoutData->insets.right;
	float addHeight = fLayoutData->insets.top + fLayoutData->insets.bottom;

	// compute the minimal width induced by the label
	float minWidth;
	if (label)
		minWidth = fLayoutData->label_box.right + fLayoutData->insets.right;
	else
		minWidth = addWidth - 1;

	BAlignment alignment(B_ALIGN_HORIZONTAL_CENTER, B_ALIGN_VERTICAL_CENTER);

	// finally consider the child constraints, if we shall support layout
	BView* child = _Child();
	if (child && (child->Flags() & B_SUPPORTS_LAYOUT)) {
		BSize min = child->MinSize();
		BSize max = child->MaxSize();
		BSize preferred = child->PreferredSize();

		min.width += addWidth;
		min.height += addHeight;
		preferred.width += addWidth;
		preferred.height += addHeight;
		max.width = BLayoutUtils::AddDistances(max.width, addWidth - 1);
		max.height = BLayoutUtils::AddDistances(max.height, addHeight - 1);

		if (min.width < minWidth)
			min.width = minWidth;
		BLayoutUtils::FixSizeConstraints(min, max, preferred);

		fLayoutData->min = min;
		fLayoutData->max = max;
		fLayoutData->preferred = preferred;

		BAlignment childAlignment = child->LayoutAlignment();
		if (childAlignment.horizontal == B_ALIGN_USE_FULL_WIDTH)
			alignment.horizontal = B_ALIGN_USE_FULL_WIDTH;
		if (childAlignment.vertical == B_ALIGN_USE_FULL_HEIGHT)
			alignment.vertical = B_ALIGN_USE_FULL_HEIGHT;

		fLayoutData->alignment = alignment;
	} else {
		fLayoutData->min.Set(minWidth, addHeight - 1);
		fLayoutData->max.Set(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
		fLayoutData->preferred = fLayoutData->min;
		fLayoutData->alignment = alignment;
	}

	fLayoutData->valid = true;
	ResetLayoutInvalidation();
}


extern "C" void
B_IF_GCC_2(InvalidateLayout__4BBoxb, _ZN4BBox16InvalidateLayoutEb)(
	BBox* box, bool descendants)
{
	perform_data_layout_invalidated data;
	data.descendants = descendants;

	box->Perform(PERFORM_CODE_LAYOUT_INVALIDATED, &data);
}