1/*
2 * Copyright 2001-2022 Haiku, Inc. All Rights Reserved.
3 * Distributed under the terms of the MIT license.
4 *
5 * Authors:
6 *		Stephan A��mus, superstippi@gmx.de
7 *		DarkWyrm, bpmagic@columbus.rr.com
8 *		Axel D��rfler, axeld@pinc-software.de
9 *		Marc Flerackers, mflerackers@androme.be
10 *		John Scipione, jscipione@gmail.com
11 */
12
13
14#include <Box.h>
15
16#include <stdio.h>
17#include <stdlib.h>
18#include <string.h>
19
20#include <ControlLook.h>
21#include <Layout.h>
22#include <LayoutUtils.h>
23#include <Message.h>
24#include <Region.h>
25
26#include <binary_compatibility/Interface.h>
27
28
29struct BBox::LayoutData {
30	LayoutData()
31		: valid(false)
32	{
33	}
34
35	BRect	label_box;		// label box (label string or label view); in case
36							// of a label string not including descent
37	BRect	insets;			// insets induced by border and label
38	BSize	min;
39	BSize	max;
40	BSize	preferred;
41	BAlignment alignment;
42	bool	valid;			// validity the other fields
43};
44
45
46BBox::BBox(BRect frame, const char* name, uint32 resizingMode, uint32 flags,
47		border_style border)
48	:
49	BView(frame, name, resizingMode, flags  | B_WILL_DRAW | B_FRAME_EVENTS),
50	  fStyle(border)
51{
52	_InitObject();
53}
54
55
56BBox::BBox(const char* name, uint32 flags, border_style border, BView* child)
57	:
58	BView(name, flags | B_WILL_DRAW | B_FRAME_EVENTS),
59	fStyle(border)
60{
61	_InitObject();
62
63	if (child)
64		AddChild(child);
65}
66
67
68BBox::BBox(border_style border, BView* child)
69	:
70	BView(NULL, B_WILL_DRAW | B_FRAME_EVENTS | B_NAVIGABLE_JUMP),
71	fStyle(border)
72{
73	_InitObject();
74
75	if (child)
76		AddChild(child);
77}
78
79
80BBox::BBox(BMessage* archive)
81	:
82	BView(archive),
83	fStyle(B_FANCY_BORDER)
84{
85	_InitObject(archive);
86}
87
88
89BBox::~BBox()
90{
91	_ClearLabel();
92
93	delete fLayoutData;
94}
95
96
97BArchivable*
98BBox::Instantiate(BMessage* archive)
99{
100	if (validate_instantiation(archive, "BBox"))
101		return new BBox(archive);
102
103	return NULL;
104}
105
106
107status_t
108BBox::Archive(BMessage* archive, bool deep) const
109{
110	status_t ret = BView::Archive(archive, deep);
111
112	if (fLabel && ret == B_OK)
113		ret = archive->AddString("_label", fLabel);
114
115	if (fLabelView && ret == B_OK)
116		ret = archive->AddBool("_lblview", true);
117
118	if (fStyle != B_FANCY_BORDER && ret == B_OK)
119		ret = archive->AddInt32("_style", fStyle);
120
121	return ret;
122}
123
124
125void
126BBox::SetBorder(border_style border)
127{
128	if (border == fStyle)
129		return;
130
131	fStyle = border;
132
133	InvalidateLayout();
134
135	if (Window() != NULL && LockLooper()) {
136		Invalidate();
137		UnlockLooper();
138	}
139}
140
141
142border_style
143BBox::Border() const
144{
145	return fStyle;
146}
147
148
149//! This function is not part of the R5 API and is not yet finalized yet
150float
151BBox::TopBorderOffset()
152{
153	_ValidateLayoutData();
154
155	if (fLabel != NULL || fLabelView != NULL)
156		return fLayoutData->label_box.Height() / 2;
157
158	return 0;
159}
160
161
162//! This function is not part of the R5 API and is not yet finalized yet
163BRect
164BBox::InnerFrame()
165{
166	_ValidateLayoutData();
167
168	BRect frame(Bounds());
169	frame.left += fLayoutData->insets.left;
170	frame.top += fLayoutData->insets.top;
171	frame.right -= fLayoutData->insets.right;
172	frame.bottom -= fLayoutData->insets.bottom;
173
174	return frame;
175}
176
177
178void
179BBox::SetLabel(const char* string)
180{
181	_ClearLabel();
182
183	if (string)
184		fLabel = strdup(string);
185
186	InvalidateLayout();
187
188	if (Window())
189		Invalidate();
190}
191
192
193status_t
194BBox::SetLabel(BView* viewLabel)
195{
196	_ClearLabel();
197
198	if (viewLabel) {
199		fLabelView = viewLabel;
200		fLabelView->MoveTo(10.0f, 0.0f);
201		AddChild(fLabelView, ChildAt(0));
202	}
203
204	InvalidateLayout();
205
206	if (Window())
207		Invalidate();
208
209	return B_OK;
210}
211
212
213const char*
214BBox::Label() const
215{
216	return fLabel;
217}
218
219
220BView*
221BBox::LabelView() const
222{
223	return fLabelView;
224}
225
226
227void
228BBox::Draw(BRect updateRect)
229{
230	_ValidateLayoutData();
231
232	PushState();
233
234	BRect labelBox = BRect(0, 0, 0, 0);
235	if (fLabel != NULL) {
236		labelBox = fLayoutData->label_box;
237		BRegion update(updateRect);
238		update.Exclude(labelBox);
239
240		ConstrainClippingRegion(&update);
241	} else if (fLabelView != NULL)
242		labelBox = fLabelView->Bounds();
243
244	switch (fStyle) {
245		case B_FANCY_BORDER:
246			_DrawFancy(labelBox);
247			break;
248
249		case B_PLAIN_BORDER:
250			_DrawPlain(labelBox);
251			break;
252
253		default:
254			break;
255	}
256
257	if (fLabel != NULL) {
258		ConstrainClippingRegion(NULL);
259
260		font_height fontHeight;
261		GetFontHeight(&fontHeight);
262
263		// offset label up by 1/6 the font height
264		float lineHeight = fontHeight.ascent + fontHeight.descent;
265		float yOffset = roundf(lineHeight / 6.0f);
266
267		SetHighColor(ui_color(B_PANEL_TEXT_COLOR));
268		DrawString(fLabel, BPoint(10.0f, fontHeight.ascent - yOffset));
269	}
270
271	PopState();
272}
273
274
275void
276BBox::AttachedToWindow()
277{
278	AdoptParentColors();
279
280	// Force low color to match view color for proper label drawing.
281	float viewTint = B_NO_TINT;
282	float lowTint = B_NO_TINT;
283
284	if (LowUIColor(&lowTint) != ViewUIColor(&viewTint) || viewTint != lowTint)
285		SetLowUIColor(ViewUIColor(), viewTint);
286	else if (LowColor() != ViewColor())
287		SetLowColor(ViewColor());
288
289	if (ViewColor() == B_TRANSPARENT_COLOR)
290		AdoptSystemColors();
291
292	// The box could have been resized in the mean time
293	fBounds = Bounds().OffsetToCopy(0, 0);
294}
295
296
297void
298BBox::DetachedFromWindow()
299{
300	BView::DetachedFromWindow();
301}
302
303
304void
305BBox::AllAttached()
306{
307	BView::AllAttached();
308}
309
310
311void
312BBox::AllDetached()
313{
314	BView::AllDetached();
315}
316
317
318void
319BBox::FrameResized(float width, float height)
320{
321	BRect bounds(Bounds());
322
323	// invalidate the regions that the app_server did not
324	// (for removing the previous or drawing the new border)
325	if (fStyle != B_NO_BORDER) {
326		// TODO: this must be made part of the be_control_look stuff!
327		int32 borderSize = fStyle == B_PLAIN_BORDER ? 0 : 2;
328
329		// Horizontal
330		BRect invalid(bounds);
331		if (fBounds.Width() < bounds.Width()) {
332			// enlarging
333			invalid.left = bounds.left + fBounds.right - borderSize;
334			invalid.right = bounds.left + fBounds.right;
335
336			Invalidate(invalid);
337		} else if (fBounds.Width() > bounds.Width()) {
338			// shrinking
339			invalid.left = bounds.left + bounds.right - borderSize;
340
341			Invalidate(invalid);
342		}
343
344		// Vertical
345		invalid = bounds;
346		if (fBounds.Height() < bounds.Height()) {
347			// enlarging
348			invalid.top = bounds.top + fBounds.bottom - borderSize;
349			invalid.bottom = bounds.top + fBounds.bottom;
350
351			Invalidate(invalid);
352		} else if (fBounds.Height() > bounds.Height()) {
353			// shrinking
354			invalid.top = bounds.top + bounds.bottom - borderSize;
355
356			Invalidate(invalid);
357		}
358	}
359
360	fBounds.right = width;
361	fBounds.bottom = height;
362}
363
364
365void
366BBox::MessageReceived(BMessage* message)
367{
368	BView::MessageReceived(message);
369}
370
371
372void
373BBox::MouseDown(BPoint point)
374{
375	BView::MouseDown(point);
376}
377
378
379void
380BBox::MouseUp(BPoint point)
381{
382	BView::MouseUp(point);
383}
384
385
386void
387BBox::WindowActivated(bool active)
388{
389	BView::WindowActivated(active);
390}
391
392
393void
394BBox::MouseMoved(BPoint point, uint32 transit, const BMessage* message)
395{
396	BView::MouseMoved(point, transit, message);
397}
398
399
400void
401BBox::FrameMoved(BPoint newLocation)
402{
403	BView::FrameMoved(newLocation);
404}
405
406
407BHandler*
408BBox::ResolveSpecifier(BMessage* message, int32 index, BMessage* specifier,
409	int32 what, const char* property)
410{
411	return BView::ResolveSpecifier(message, index, specifier, what, property);
412}
413
414
415void
416BBox::ResizeToPreferred()
417{
418	float width, height;
419	GetPreferredSize(&width, &height);
420
421	// make sure the box don't get smaller than it already is
422	if (width < Bounds().Width())
423		width = Bounds().Width();
424	if (height < Bounds().Height())
425		height = Bounds().Height();
426
427	BView::ResizeTo(width, height);
428}
429
430
431void
432BBox::GetPreferredSize(float* _width, float* _height)
433{
434	_ValidateLayoutData();
435
436	if (_width)
437		*_width = fLayoutData->preferred.width;
438	if (_height)
439		*_height = fLayoutData->preferred.height;
440}
441
442
443void
444BBox::MakeFocus(bool focused)
445{
446	BView::MakeFocus(focused);
447}
448
449
450status_t
451BBox::GetSupportedSuites(BMessage* message)
452{
453	return BView::GetSupportedSuites(message);
454}
455
456
457status_t
458BBox::Perform(perform_code code, void* _data)
459{
460	switch (code) {
461		case PERFORM_CODE_MIN_SIZE:
462			((perform_data_min_size*)_data)->return_value
463				= BBox::MinSize();
464			return B_OK;
465		case PERFORM_CODE_MAX_SIZE:
466			((perform_data_max_size*)_data)->return_value
467				= BBox::MaxSize();
468			return B_OK;
469		case PERFORM_CODE_PREFERRED_SIZE:
470			((perform_data_preferred_size*)_data)->return_value
471				= BBox::PreferredSize();
472			return B_OK;
473		case PERFORM_CODE_LAYOUT_ALIGNMENT:
474			((perform_data_layout_alignment*)_data)->return_value
475				= BBox::LayoutAlignment();
476			return B_OK;
477		case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
478			((perform_data_has_height_for_width*)_data)->return_value
479				= BBox::HasHeightForWidth();
480			return B_OK;
481		case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
482		{
483			perform_data_get_height_for_width* data
484				= (perform_data_get_height_for_width*)_data;
485			BBox::GetHeightForWidth(data->width, &data->min, &data->max,
486				&data->preferred);
487			return B_OK;
488		}
489		case PERFORM_CODE_SET_LAYOUT:
490		{
491			perform_data_set_layout* data = (perform_data_set_layout*)_data;
492			BBox::SetLayout(data->layout);
493			return B_OK;
494		}
495		case PERFORM_CODE_LAYOUT_INVALIDATED:
496		{
497			perform_data_layout_invalidated* data
498				= (perform_data_layout_invalidated*)_data;
499			BBox::LayoutInvalidated(data->descendants);
500			return B_OK;
501		}
502		case PERFORM_CODE_DO_LAYOUT:
503		{
504			BBox::DoLayout();
505			return B_OK;
506		}
507	}
508
509	return BView::Perform(code, _data);
510}
511
512
513BSize
514BBox::MinSize()
515{
516	_ValidateLayoutData();
517
518	BSize size = (GetLayout() ? GetLayout()->MinSize() : fLayoutData->min);
519	if (size.width < fLayoutData->min.width)
520		size.width = fLayoutData->min.width;
521	return BLayoutUtils::ComposeSize(ExplicitMinSize(), size);
522}
523
524
525BSize
526BBox::MaxSize()
527{
528	_ValidateLayoutData();
529
530	BSize size = (GetLayout() ? GetLayout()->MaxSize() : fLayoutData->max);
531	return BLayoutUtils::ComposeSize(ExplicitMaxSize(), size);
532}
533
534
535BSize
536BBox::PreferredSize()
537{
538	_ValidateLayoutData();
539
540	BSize size = (GetLayout() ? GetLayout()->PreferredSize()
541		: fLayoutData->preferred);
542	return BLayoutUtils::ComposeSize(ExplicitPreferredSize(), size);
543}
544
545
546BAlignment
547BBox::LayoutAlignment()
548{
549	_ValidateLayoutData();
550
551	BAlignment alignment = (GetLayout() ? GetLayout()->Alignment()
552			: fLayoutData->alignment);
553	return BLayoutUtils::ComposeAlignment(ExplicitAlignment(), alignment);
554}
555
556
557void
558BBox::LayoutInvalidated(bool descendants)
559{
560	fLayoutData->valid = false;
561}
562
563
564void
565BBox::DoLayout()
566{
567	// Bail out, if we shan't do layout.
568	if (!(Flags() & B_SUPPORTS_LAYOUT))
569		return;
570
571	BLayout* layout = GetLayout();
572
573	// If the user set a layout, let the base class version call its
574	// hook. In case when we have BView as a label, remove it from child list
575	// so it won't be layouted with the rest of views and add it again
576	// after that.
577	if (layout != NULL) {
578		if (fLabelView)
579			RemoveChild(fLabelView);
580
581		BView::DoLayout();
582
583		if (fLabelView != NULL) {
584			DisableLayoutInvalidation();
585				// don't trigger a relayout
586			AddChild(fLabelView, ChildAt(0));
587			EnableLayoutInvalidation();
588		}
589	}
590
591	_ValidateLayoutData();
592
593	// Even if the user set a layout, restore label view to it's
594	// desired position.
595
596	// layout the label view
597	if (fLabelView != NULL) {
598		fLabelView->MoveTo(fLayoutData->label_box.LeftTop());
599		fLabelView->ResizeTo(fLayoutData->label_box.Size());
600	}
601
602	// If we have layout return here and do not layout the child
603	if (layout != NULL)
604		return;
605
606	// layout the child
607	BView* child = _Child();
608	if (child != NULL) {
609		BRect frame(Bounds());
610		frame.left += fLayoutData->insets.left;
611		frame.top += fLayoutData->insets.top;
612		frame.right -= fLayoutData->insets.right;
613		frame.bottom -= fLayoutData->insets.bottom;
614
615		if ((child->Flags() & B_SUPPORTS_LAYOUT) != 0)
616			BLayoutUtils::AlignInFrame(child, frame);
617		else
618			child->MoveTo(frame.LeftTop());
619	}
620}
621
622
623void BBox::_ReservedBox1() {}
624void BBox::_ReservedBox2() {}
625
626
627BBox &
628BBox::operator=(const BBox &)
629{
630	return *this;
631}
632
633
634void
635BBox::_InitObject(BMessage* archive)
636{
637	fBounds = Bounds().OffsetToCopy(0, 0);
638
639	fLabel = NULL;
640	fLabelView = NULL;
641	fLayoutData = new LayoutData;
642
643	int32 flags = 0;
644
645	BFont font(be_bold_font);
646
647	if (!archive || !archive->HasString("_fname"))
648		flags = B_FONT_FAMILY_AND_STYLE;
649
650	if (!archive || !archive->HasFloat("_fflt"))
651		flags |= B_FONT_SIZE;
652
653	if (flags != 0)
654		SetFont(&font, flags);
655
656	if (archive != NULL) {
657		const char* string;
658		if (archive->FindString("_label", &string) == B_OK)
659			SetLabel(string);
660
661		bool fancy;
662		int32 style;
663
664		if (archive->FindBool("_style", &fancy) == B_OK)
665			fStyle = fancy ? B_FANCY_BORDER : B_PLAIN_BORDER;
666		else if (archive->FindInt32("_style", &style) == B_OK)
667			fStyle = (border_style)style;
668
669		bool hasLabelView;
670		if (archive->FindBool("_lblview", &hasLabelView) == B_OK)
671			fLabelView = ChildAt(0);
672	}
673
674	AdoptSystemColors();
675}
676
677
678void
679BBox::_DrawPlain(BRect labelBox)
680{
681	BRect rect = Bounds();
682	rect.top += TopBorderOffset();
683
684	float lightTint;
685	float shadowTint;
686	lightTint = B_LIGHTEN_1_TINT;
687	shadowTint = B_DARKEN_1_TINT;
688
689	if (rect.Height() == 0.0 || rect.Width() == 0.0) {
690		// used as separator
691		rgb_color shadow = tint_color(ViewColor(), B_DARKEN_2_TINT);
692
693		SetHighColor(shadow);
694		StrokeLine(rect.LeftTop(),rect.RightBottom());
695	} else {
696		// used as box
697		rgb_color light = tint_color(ViewColor(), lightTint);
698		rgb_color shadow = tint_color(ViewColor(), shadowTint);
699
700		BeginLineArray(4);
701			AddLine(BPoint(rect.left, rect.bottom),
702					BPoint(rect.left, rect.top), light);
703			AddLine(BPoint(rect.left + 1.0f, rect.top),
704					BPoint(rect.right, rect.top), light);
705			AddLine(BPoint(rect.left + 1.0f, rect.bottom),
706					BPoint(rect.right, rect.bottom), shadow);
707			AddLine(BPoint(rect.right, rect.bottom - 1.0f),
708					BPoint(rect.right, rect.top + 1.0f), shadow);
709		EndLineArray();
710	}
711}
712
713
714void
715BBox::_DrawFancy(BRect labelBox)
716{
717	BRect rect = Bounds();
718	rect.top += TopBorderOffset();
719
720	rgb_color base = ViewColor();
721	if (rect.Height() == 1.0) {
722		// used as horizontal separator
723		be_control_look->DrawGroupFrame(this, rect, rect, base,
724			BControlLook::B_TOP_BORDER);
725	} else if (rect.Width() == 1.0) {
726		// used as vertical separator
727		be_control_look->DrawGroupFrame(this, rect, rect, base,
728			BControlLook::B_LEFT_BORDER);
729	} else {
730		// used as box
731		be_control_look->DrawGroupFrame(this, rect, rect, base);
732	}
733}
734
735
736void
737BBox::_ClearLabel()
738{
739	if (fLabel) {
740		free(fLabel);
741		fLabel = NULL;
742	} else if (fLabelView) {
743		fLabelView->RemoveSelf();
744		delete fLabelView;
745		fLabelView = NULL;
746	}
747}
748
749
750BView*
751BBox::_Child() const
752{
753	for (int32 i = 0; BView* view = ChildAt(i); i++) {
754		if (view != fLabelView)
755			return view;
756	}
757
758	return NULL;
759}
760
761
762void
763BBox::_ValidateLayoutData()
764{
765	if (fLayoutData->valid)
766		return;
767
768	// compute the label box, width and height
769	bool label = true;
770	float labelHeight = 0;	// height of the label (pixel count)
771	if (fLabel) {
772		// leave 6 pixels of the frame, and have a gap of 4 pixels between
773		// the frame and the text on either side
774		font_height fontHeight;
775		GetFontHeight(&fontHeight);
776		fLayoutData->label_box.Set(6.0f, 0, 14.0f + StringWidth(fLabel),
777			ceilf(fontHeight.ascent));
778		labelHeight = ceilf(fontHeight.ascent + fontHeight.descent) + 1;
779	} else if (fLabelView) {
780		// the label view is placed at (0, 10) at its preferred size
781		BSize size = fLabelView->PreferredSize();
782		fLayoutData->label_box.Set(10, 0, 10 + size.width, size.height);
783		labelHeight = size.height + 1;
784	} else
785		label = false;
786
787	// border
788	switch (fStyle) {
789		case B_PLAIN_BORDER:
790			fLayoutData->insets.Set(1, 1, 1, 1);
791			break;
792		case B_FANCY_BORDER:
793			fLayoutData->insets.Set(3, 3, 3, 3);
794			break;
795		case B_NO_BORDER:
796		default:
797			fLayoutData->insets.Set(0, 0, 0, 0);
798			break;
799	}
800
801	// if there's a label, the top inset will be dictated by the label
802	if (label && labelHeight > fLayoutData->insets.top)
803		fLayoutData->insets.top = labelHeight;
804
805	// total number of pixel the border adds
806	float addWidth = fLayoutData->insets.left + fLayoutData->insets.right;
807	float addHeight = fLayoutData->insets.top + fLayoutData->insets.bottom;
808
809	// compute the minimal width induced by the label
810	float minWidth;
811	if (label)
812		minWidth = fLayoutData->label_box.right + fLayoutData->insets.right;
813	else
814		minWidth = addWidth - 1;
815
816	BAlignment alignment(B_ALIGN_HORIZONTAL_CENTER, B_ALIGN_VERTICAL_CENTER);
817
818	// finally consider the child constraints, if we shall support layout
819	BView* child = _Child();
820	if (child && (child->Flags() & B_SUPPORTS_LAYOUT)) {
821		BSize min = child->MinSize();
822		BSize max = child->MaxSize();
823		BSize preferred = child->PreferredSize();
824
825		min.width += addWidth;
826		min.height += addHeight;
827		preferred.width += addWidth;
828		preferred.height += addHeight;
829		max.width = BLayoutUtils::AddDistances(max.width, addWidth - 1);
830		max.height = BLayoutUtils::AddDistances(max.height, addHeight - 1);
831
832		if (min.width < minWidth)
833			min.width = minWidth;
834		BLayoutUtils::FixSizeConstraints(min, max, preferred);
835
836		fLayoutData->min = min;
837		fLayoutData->max = max;
838		fLayoutData->preferred = preferred;
839
840		BAlignment childAlignment = child->LayoutAlignment();
841		if (childAlignment.horizontal == B_ALIGN_USE_FULL_WIDTH)
842			alignment.horizontal = B_ALIGN_USE_FULL_WIDTH;
843		if (childAlignment.vertical == B_ALIGN_USE_FULL_HEIGHT)
844			alignment.vertical = B_ALIGN_USE_FULL_HEIGHT;
845
846		fLayoutData->alignment = alignment;
847	} else {
848		fLayoutData->min.Set(minWidth, addHeight - 1);
849		fLayoutData->max.Set(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
850		fLayoutData->preferred = fLayoutData->min;
851		fLayoutData->alignment = alignment;
852	}
853
854	fLayoutData->valid = true;
855	ResetLayoutInvalidation();
856}
857
858
859extern "C" void
860B_IF_GCC_2(InvalidateLayout__4BBoxb, _ZN4BBox16InvalidateLayoutEb)(
861	BBox* box, bool descendants)
862{
863	perform_data_layout_invalidated data;
864	data.descendants = descendants;
865
866	box->Perform(PERFORM_CODE_LAYOUT_INVALIDATED, &data);
867}
868
869