Skip to content

Instantly share code, notes, and snippets.

@rguiscard
Last active July 28, 2026 05:59
Show Gist options
  • Select an option

  • Save rguiscard/e6b25580fe8dbc47414dd2e0c48a228e to your computer and use it in GitHub Desktop.

Select an option

Save rguiscard/e6b25580fe8dbc47414dd2e0c48a228e to your computer and use it in GitHub Desktop.
Cards in scrollview (Haiku OS)
#include <Application.h>
#include <Window.h>
#include <View.h>
#include <ScrollView.h>
#include <TextView.h>
#include <String.h>
#include <MenuBar.h>
#include <Menu.h>
#include <MenuItem.h>
static const char* kAppSignature = "application/x-vnd.Test-CardDemo";
static const uint32 kMsgAddCard = 'adcd';
static const uint32 kMsgInsertCard = 'incd'; // New message for Insert
// ============================================================================
// CardView - A card containing wrapped text
// ============================================================================
class CardView : public BView {
public:
CardView(const char* name, const char* text, float width);
void SetWidth(float width);
virtual void Draw(BRect updateRect);
private:
BTextView* fTextView;
float fWidth;
static const float kInset;
};
const float CardView::kInset = 15.0f;
CardView::CardView(const char* name, const char* text, float width)
:
BView(name, B_WILL_DRAW | B_FRAME_EVENTS),
fTextView(NULL),
fWidth(width)
{
SetViewColor(255, 255, 255);
fTextView = new BTextView("text", B_WILL_DRAW);
fTextView->SetText(text);
fTextView->SetViewColor(255, 255, 255);
fTextView->MakeEditable(false);
fTextView->SetWordWrap(true);
fTextView->SetStylable(false);
fTextView->SetFlags(fTextView->Flags() & ~(B_NAVIGABLE | B_FRAME_EVENTS));
rgb_color textColor = ui_color(B_DOCUMENT_TEXT_COLOR);
fTextView->SetFontAndColor(NULL, 0, &textColor);
AddChild(fTextView);
}
void
CardView::SetWidth(float width)
{
fWidth = width;
float textWidth = fWidth - 2.0f * kInset;
if (textWidth < 10.0f) textWidth = 10.0f;
fTextView->ResizeTo(textWidth, 5000.0f);
float textHeight = fTextView->TextHeight(0, fTextView->TextLength());
float totalHeight = textHeight + 2.0f * kInset;
ResizeTo(fWidth, totalHeight);
fTextView->MoveTo(kInset, kInset);
fTextView->ResizeTo(textWidth, textHeight);
}
void
CardView::Draw(BRect updateRect)
{
BView::Draw(updateRect);
SetHighColor(200, 200, 200);
StrokeRect(Bounds());
}
// ============================================================================
// ContentView - Holds all cards, placed inside a BScrollView
// ============================================================================
class ContentView : public BView {
public:
ContentView(float initialWidth);
virtual void AttachedToWindow();
void UpdateWidth(float newWidth);
void AddCard(const char* text);
void InsertCard(const char* text); // New method
virtual void GetPreferredSize(float* width, float* height);
private:
void RelayoutCards();
float fContentHeight;
float fCurrentWidth;
float fInitialWidth;
};
ContentView::ContentView(float initialWidth)
:
BView("content", B_WILL_DRAW),
fContentHeight(0.0f),
fCurrentWidth(-1.0f),
fInitialWidth(initialWidth)
{
SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR));
const char* texts[] = {
"Short text.",
"This is a medium length text that should wrap to multiple lines "
"depending on the width of the card.",
"Brief.",
"Haiku is an open-source operating system that specifically targets "
"personal computing. It is inspired by the Be Operating System (BeOS), "
"which was known for its clean design and responsive interface. The goal "
"of Haiku is to create a fast, efficient, and easy-to-use desktop "
"operating system that preserves the spirit of BeOS.",
"Another short one.",
"The BView class is one of the fundamental building blocks of Haiku's "
"interface toolkit. It represents a rectangular area on screen and can "
"handle drawing, mouse events, and keyboard events. Views can be nested "
"to create complex user interfaces.",
"Test.",
"Scroll views are essential for displaying content that exceeds the "
"available screen space. They provide scrollbars that allow users to "
"navigate through the content. In Haiku, the BScrollView class wraps "
"another view to provide this functionality.",
"Cards are a popular UI pattern that groups related information together "
"in a visually distinct container. They typically have a background "
"color, some padding to separate content from edges, and sometimes "
"rounded corners or shadows for depth.",
"Hi!",
"The layout system in Haiku uses a constraint-based approach that makes "
"it easier to create responsive interfaces. The BLayout classes handle "
"the positioning and sizing of child views automatically, reducing the "
"need for manual frame calculations.",
"OK.",
"Text wrapping is an important feature for displaying long passages of "
"text in a constrained space. Without it, text would either overflow "
"the visible area or be truncated, making it difficult to read. "
"BTextView provides built-in support for word wrapping.",
"Simple.",
"When implementing card-based interfaces, it's important to consider "
"the performance implications. Each card is a separate view with its "
"own drawing context and event handling. For small numbers of cards, "
"this is fine, but for hundreds or thousands of cards, more efficient "
"approaches like custom drawing or view recycling may be necessary.",
"Yep.",
"The color scheme of an application can significantly affect its "
"usability and aesthetics. Haiku provides several predefined color "
"constants like B_DOCUMENT_BACKGROUND_COLOR and B_PANEL_BACKGROUND_COLOR "
"that help maintain consistency with the system theme.",
"Hey there!",
"In this demo, we have twenty cards with varying amounts of text to "
"demonstrate how the scroll view and card layout work together. The "
"scroll view automatically adjusts its scrollbars based on the total "
"height of the content.",
"End."
};
const int kNumCards = 20;
for (int i = 0; i < kNumCards; i++) {
BString name;
name << "Card" << (i + 1);
CardView* card = new CardView(name.String(), texts[i], fInitialWidth);
AddChild(card);
}
}
void
ContentView::AttachedToWindow()
{
UpdateWidth(fInitialWidth);
}
void
ContentView::UpdateWidth(float newWidth)
{
if (newWidth == fCurrentWidth)
return;
fCurrentWidth = newWidth;
RelayoutCards();
}
void
ContentView::RelayoutCards()
{
const float kCardSpacing = 10.0f;
const float kLeftMargin = 20.0f;
float cardWidth = fCurrentWidth - 2.0f * kLeftMargin;
if (cardWidth < 50.0f) cardWidth = 50.0f;
float yOffset = 20.0f;
// Loops through children in list order (0 to N)
for (int i = 0; i < CountChildren(); i++) {
CardView* card = static_cast<CardView*>(ChildAt(i));
if (card != NULL) {
card->SetWidth(cardWidth);
card->MoveTo(kLeftMargin, yOffset);
yOffset += card->Bounds().Height() + kCardSpacing;
}
}
fContentHeight = yOffset + 20.0f;
ResizeTo(fCurrentWidth, fContentHeight);
}
void
ContentView::AddCard(const char* text)
{
const float kCardSpacing = 10.0f;
const float kLeftMargin = 20.0f;
int cardNum = CountChildren() + 1;
BString name;
name << "Card" << cardNum;
float cardWidth = fCurrentWidth - 2.0f * kLeftMargin;
if (cardWidth < 50.0f) cardWidth = 50.0f;
CardView* card = new CardView(name.String(), text, cardWidth);
AddChild(card);
card->SetWidth(cardWidth);
float yOffset = fContentHeight - 20.0f;
card->MoveTo(kLeftMargin, yOffset);
fContentHeight = yOffset + card->Bounds().Height() + kCardSpacing + 20.0f;
ResizeTo(fCurrentWidth, fContentHeight);
}
void
ContentView::InsertCard(const char* text)
{
const float kLeftMargin = 20.0f;
int cardNum = CountChildren() + 1;
BString name;
name << "Card" << cardNum;
float cardWidth = fCurrentWidth - 2.0f * kLeftMargin;
if (cardWidth < 50.0f) cardWidth = 50.0f;
CardView* card = new CardView(name.String(), text, cardWidth);
// Insert at the very beginning of the child list (index 0)
BView* firstChild = ChildAt(0);
if (firstChild != NULL)
AddChild(card, firstChild);
else
AddChild(card);
// Recalculate layout. Since the new card is at index 0, it gets placed
// at the top, and all other cards are pushed down automatically!
RelayoutCards();
}
void
ContentView::GetPreferredSize(float* width, float* height)
{
if (width != NULL)
*width = fCurrentWidth > 0 ? fCurrentWidth : fInitialWidth;
if (height != NULL)
*height = fContentHeight;
}
// ============================================================================
// MainWindow
// ============================================================================
class MainWindow : public BWindow {
public:
MainWindow();
virtual bool QuitRequested();
virtual void FrameResized(float newWidth, float newHeight);
virtual void MessageReceived(BMessage* message);
private:
ContentView* fContentView;
BScrollView* fScrollView;
BMenuBar* fMenuBar;
static const float kMenuHeight;
};
const float MainWindow::kMenuHeight = 25.0f;
MainWindow::MainWindow()
:
BWindow(BRect(50, 50, 450, 400), "Card Demo",
B_DOCUMENT_WINDOW, 0)
{
// 1. Menu Bar
fMenuBar = new BMenuBar("menubar");
BMenu* fileMenu = new BMenu("File");
fileMenu->AddItem(new BMenuItem("Add Card", new BMessage(kMsgAddCard), 'N'));
fileMenu->AddItem(new BMenuItem("Insert Card", new BMessage(kMsgInsertCard), 'I'));
fileMenu->AddSeparatorItem();
fileMenu->AddItem(new BMenuItem("Quit", new BMessage(B_QUIT_REQUESTED), 'Q'));
fMenuBar->AddItem(fileMenu);
AddChild(fMenuBar);
// 2. Fixed dimensions for manual layout
const float kBorder = 2.0f;
const float kScrollBarWidth = 16.0f;
float initialContentWidth = Bounds().Width() - (2.0f * kBorder) - kScrollBarWidth;
fContentView = new ContentView(initialContentWidth);
// 3. Scroll View
fScrollView = new BScrollView("scroll", fContentView,
0, false, true, B_FANCY_BORDER);
AddChild(fScrollView);
// 4. Manual Positioning
fMenuBar->MoveTo(0, 0);
fMenuBar->ResizeTo(Bounds().Width(), kMenuHeight);
fScrollView->MoveTo(0, kMenuHeight);
fScrollView->ResizeTo(Bounds().Width(), Bounds().Height() - kMenuHeight);
SetSizeLimits(300, 2000, 200, 2000);
Show();
}
bool
MainWindow::QuitRequested()
{
be_app->PostMessage(B_QUIT_REQUESTED);
return true;
}
void
MainWindow::FrameResized(float newWidth, float newHeight)
{
BWindow::FrameResized(newWidth, newHeight);
const float kBorder = 2.0f;
fMenuBar->ResizeTo(newWidth, kMenuHeight);
fScrollView->MoveTo(0, kMenuHeight);
fScrollView->ResizeTo(newWidth, newHeight - kMenuHeight);
float vScrollBarWidth = 0.0f;
BScrollBar* vScroll = fScrollView->ScrollBar(B_VERTICAL);
if (vScroll != NULL && !vScroll->IsHidden())
vScrollBarWidth = vScroll->Bounds().Width();
float contentWidth = newWidth - (2.0f * kBorder) - vScrollBarWidth;
fContentView->UpdateWidth(contentWidth);
}
void
MainWindow::MessageReceived(BMessage* message)
{
switch (message->what) {
case kMsgAddCard:
{
const char* dummyText = "This is a dynamically added card. "
"It demonstrates that we can append views to the content area "
"at runtime, and the scroll view will correctly adjust to show "
"the new content at the bottom.";
fContentView->AddCard(dummyText);
BScrollBar* vScroll = fScrollView->ScrollBar(B_VERTICAL);
if (vScroll != NULL) {
float visibleHeight = fScrollView->Bounds().Height() - 4.0f;
float totalHeight = fContentView->Bounds().Height();
if (totalHeight > visibleHeight) {
vScroll->SetRange(0, totalHeight - visibleHeight);
vScroll->SetProportion(visibleHeight / totalHeight);
} else {
vScroll->SetRange(0, 0);
vScroll->SetProportion(1.0f);
}
}
fScrollView->Invalidate();
break;
}
case kMsgInsertCard:
{
const char* dummyText = "This is an inserted card at the top. "
"It pushes all existing cards downward, demonstrating how "
"to insert views dynamically into the beginning of a list.";
fContentView->InsertCard(dummyText);
// Update scroll bar proportions because total height changed
BScrollBar* vScroll = fScrollView->ScrollBar(B_VERTICAL);
if (vScroll != NULL) {
float visibleHeight = fScrollView->Bounds().Height() - 4.0f;
float totalHeight = fContentView->Bounds().Height();
if (totalHeight > visibleHeight) {
vScroll->SetRange(0, totalHeight - visibleHeight);
vScroll->SetProportion(visibleHeight / totalHeight);
} else {
vScroll->SetRange(0, 0);
vScroll->SetProportion(1.0f);
}
// Scroll all the way to the top so the user can see the new card
vScroll->SetValue(0);
}
fScrollView->Invalidate();
break;
}
default:
BWindow::MessageReceived(message);
break;
}
}
// ============================================================================
// CardApp
// ============================================================================
class CardApp : public BApplication {
public:
CardApp();
virtual void ReadyToRun();
};
CardApp::CardApp()
:
BApplication(kAppSignature)
{
}
void
CardApp::ReadyToRun()
{
new MainWindow();
}
// ============================================================================
// main
// ============================================================================
int
main()
{
CardApp app;
app.Run();
return 0;
}
// This version use layout builder for most of layout making less calculation
#include <Application.h>
#include <Window.h>
#include <View.h>
#include <ScrollView.h>
#include <TextView.h>
#include <String.h>
#include <MenuBar.h>
#include <Menu.h>
#include <MenuItem.h>
#include <LayoutBuilder.h>
#include <Layout.h>
#include <iostream>
static const char* kAppSignature = "application/x-vnd.Test-CardDemo";
static const uint32 kMsgAddCard = 'adcd';
static const uint32 kMsgInsertCard = 'incd'; // New message for Insert
// ============================================================================
// CardView - A card containing wrapped text
// ============================================================================
class CardView : public BView {
public:
CardView(const char* name, const char* text);
virtual void FrameResized(float newWidth, float newHeight);
virtual bool HasHeightForWidth();
virtual void GetHeightForWidth(float width, float* min, float* max, float* preferred);
private:
BTextView* fTextView;
static const float kInset;
};
const float CardView::kInset = 15.0f;
CardView::CardView(const char* name, const char* text)
:
BView(name, B_WILL_DRAW | B_FRAME_EVENTS),
fTextView(NULL)
{
SetViewColor(255, 255, 255);
fTextView = new BTextView("text", B_WILL_DRAW);
fTextView->SetText(text);
fTextView->SetViewColor(255, 255, 255);
fTextView->MakeEditable(false);
fTextView->SetWordWrap(true);
fTextView->SetStylable(false);
fTextView->SetFlags(fTextView->Flags() & ~(B_NAVIGABLE | B_FRAME_EVENTS));
rgb_color textColor = ui_color(B_DOCUMENT_TEXT_COLOR);
fTextView->SetFontAndColor(NULL, 0, &textColor);
AddChild(fTextView);
}
bool
CardView::HasHeightForWidth()
{
return true;
}
void
CardView::GetHeightForWidth(float width, float* min, float* max, float* preferred)
{
float textWidth = width - 2.0f * kInset;
if (textWidth < 10.0f) textWidth = 10.0f;
BRect oldRect = fTextView->TextRect();
BRect tempRect(0, 0, textWidth, 0);
fTextView->SetTextRect(tempRect);
float textHeight = fTextView->TextHeight(0, fTextView->TextLength());
fTextView->SetTextRect(oldRect);
float totalHeight = textHeight + 2.0f * kInset;
if (min) *min = totalHeight;
if (max) *max = totalHeight;
if (preferred) *preferred = totalHeight;
}
void
CardView::FrameResized(float newWidth, float newHeight)
{
BView::FrameResized(newWidth, newHeight);
float textWidth = newWidth - 2.0f * kInset;
if (textWidth < 10.0f) textWidth = 10.0f;
float textHeight = newHeight - 2.0f * kInset;
if (textHeight < 0.0f) textHeight = 0.0f;
fTextView->MoveTo(kInset, kInset);
fTextView->ResizeTo(textWidth, textHeight);
fTextView->SetTextRect(BRect(0, 0, textWidth, textHeight));
}
// ============================================================================
// ContentView - Holds all cards, placed inside a BScrollView
// ============================================================================
class ContentView : public BView {
public:
ContentView();
void UpdateWidth(float newWidth);
void AddCard(const char* text);
void InsertCard(const char* text); // New method
float TotalHeight() const;
private:
float fCurrentWidth;
BGroupLayout* fLayout;
};
ContentView::ContentView()
:
BView("content", B_WILL_DRAW | B_FRAME_EVENTS),
fCurrentWidth(-1.0f),
fLayout(NULL)
{
SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR));
fLayout = new BGroupLayout(B_VERTICAL);
SetLayout(fLayout);
fLayout->SetSpacing(10.0f);
fLayout->SetInsets(20.0f, 20.0f, 20.0f, 20.0f);
}
void
ContentView::UpdateWidth(float newWidth)
{
#if 0
if (newWidth == fCurrentWidth) {
std::cout << "ContentView::UpdateWidth " << "Same width" << std::endl;
return;
}
#endif
fCurrentWidth = newWidth;
}
float
ContentView::TotalHeight() const
{
float cardWidth = fCurrentWidth - 40.0f;
if (cardWidth < 20.0f) cardWidth = 20.0f;
float totalHeight = 0.0f;
int numChildren = CountChildren();
for (int i = 0; i < numChildren; i++) {
CardView* card = static_cast<CardView*>(ChildAt(i));
if (card != NULL) {
float min, max, preferred;
card->GetHeightForWidth(cardWidth, &min, &max, &preferred);
totalHeight += preferred;
}
}
if (numChildren > 1) {
totalHeight += (numChildren - 1) * 10.0f;
}
totalHeight += 40.0f;
return totalHeight;
}
void
ContentView::AddCard(const char* text)
{
int cardNum = CountChildren() + 1;
BString name;
name << "Card" << cardNum;
CardView* card = new CardView(name.String(), text);
AddChild(card);
}
void
ContentView::InsertCard(const char* text)
{
int cardNum = CountChildren() + 1;
BString name;
name << "Card" << cardNum;
CardView* card = new CardView(name.String(), text);
// Insert at the very beginning of the child list (index 0)
BView* firstChild = ChildAt(0);
if (firstChild != NULL)
AddChild(card, firstChild);
else
AddChild(card);
}
// ============================================================================
// CardScrollView - Intercepts resizing to update width
// ============================================================================
class CardScrollView : public BScrollView {
public:
CardScrollView(const char* name, ContentView* target);
virtual void FrameResized(float newWidth, float newHeight);
void UpdateScrollBar();
private:
ContentView* fContentView;
};
CardScrollView::CardScrollView(const char* name, ContentView* target)
:
BScrollView(name, target, 0, false, true, B_FANCY_BORDER),
fContentView(target)
{
}
void
CardScrollView::FrameResized(float newWidth, float newHeight)
{
std::cout << "CardScrollView " << newWidth << " & " << newHeight << std::endl;
std::cout << "CardScrollView " << Bounds().Width() << " & " << Bounds().Height() << std::endl;
BScrollView::FrameResized(newWidth, newHeight);
float vScrollBarWidth = 0.0f;
BScrollBar* vScroll = ScrollBar(B_VERTICAL);
if (vScroll != NULL && !vScroll->IsHidden())
vScrollBarWidth = vScroll->Bounds().Width();
std::cout << "ScrollBar " << vScroll->Bounds().Width() << " & " << vScroll->Bounds().Height() << std::endl;
const float kBorder = 2.0f;
float contentWidth = newWidth - (2.0f * kBorder) - vScrollBarWidth;
dynamic_cast<ContentView*>(Target())->UpdateWidth(contentWidth);
UpdateScrollBar();
}
void
CardScrollView::UpdateScrollBar()
{
BScrollBar* vScroll = ScrollBar(B_VERTICAL);
if (vScroll == NULL)
return;
float totalHeight = fContentView->TotalHeight();
float viewportHeight = Bounds().Height() - (2.0f * 2.0f);
if (totalHeight <= viewportHeight) {
vScroll->SetRange(0.0f, 0.0f);
} else {
vScroll->SetRange(0.0f, totalHeight - viewportHeight);
vScroll->SetProportion(viewportHeight / totalHeight);
}
}
// ============================================================================
// MainWindow
// ============================================================================
class MainWindow : public BWindow {
public:
MainWindow();
virtual bool QuitRequested();
virtual void MessageReceived(BMessage* message);
private:
ContentView* fContentView;
CardScrollView* fScrollView;
BMenuBar* fMenuBar;
static const float kMenuHeight;
};
const float MainWindow::kMenuHeight = 25.0f;
MainWindow::MainWindow()
:
BWindow(BRect(50, 50, 450, 400), "Card Demo",
B_DOCUMENT_WINDOW, 0)
{
// 1. Menu Bar
fMenuBar = new BMenuBar("menubar");
BMenu* fileMenu = new BMenu("File");
fileMenu->AddItem(new BMenuItem("Add Card", new BMessage(kMsgAddCard), 'N'));
fileMenu->AddItem(new BMenuItem("Insert Card", new BMessage(kMsgInsertCard), 'I'));
fileMenu->AddSeparatorItem();
fileMenu->AddItem(new BMenuItem("Quit", new BMessage(B_QUIT_REQUESTED), 'Q'));
fMenuBar->AddItem(fileMenu);
// 2. Fixed dimensions for manual layout
// const float kBorder = 2.0f;
// const float kScrollBarWidth = 16.0f;
// float initialContentWidth = Bounds().Width() - (2.0f * kBorder) - kScrollBarWidth;
fContentView = new ContentView();
// 3. Scroll View
fScrollView = new CardScrollView("scroll", fContentView);
// Create dummy text view
BTextView *textView = new BTextView("textView", B_WILL_DRAW);
textView->SetText("This is a summary view showing the selected article content. "
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.");
// Create horizontal split view for left/right panels
BSplitView* mainSplit = new BSplitView(B_HORIZONTAL, 0.0f);
mainSplit->AddChild(textView, 0.3f);
mainSplit->AddChild(fScrollView, 0.7f);
textView->SetExplicitMinSize(BSize(50, B_SIZE_UNSET));
// Build layout
BLayoutBuilder::Group<>(this, B_VERTICAL)
.Add(fMenuBar)
.Add(mainSplit)
.End();
const char* texts[] = {
"Short text.",
"This is a medium length text that should wrap to multiple lines "
"depending on the width of the card.",
"Brief.",
"Haiku is an open-source operating system that specifically targets "
"personal computing. It is inspired by the Be Operating System (BeOS), "
"which was known for its clean design and responsive interface. The goal "
"of Haiku is to create a fast, efficient, and easy-to-use desktop "
"operating system that preserves the spirit of BeOS.",
"Another short one.",
"The BView class is one of the fundamental building blocks of Haiku's "
"interface toolkit. It represents a rectangular area on screen and can "
"handle drawing, mouse events, and keyboard events. Views can be nested "
"to create complex user interfaces.",
"Test.",
"Scroll views are essential for displaying content that exceeds the "
"available screen space. They provide scrollbars that allow users to "
"navigate through the content. In Haiku, the BScrollView class wraps "
"another view to provide this functionality.",
"Cards are a popular UI pattern that groups related information together "
"in a visually distinct container. They typically have a background "
"color, some padding to separate content from edges, and sometimes "
"rounded corners or shadows for depth.",
"Hi!",
"The layout system in Haiku uses a constraint-based approach that makes "
"it easier to create responsive interfaces. The BLayout classes handle "
"the positioning and sizing of child views automatically, reducing the "
"need for manual frame calculations.",
"OK.",
"Text wrapping is an important feature for displaying long passages of "
"text in a constrained space. Without it, text would either overflow "
"the visible area or be truncated, making it difficult to read. "
"BTextView provides built-in support for word wrapping.",
"Simple.",
"When implementing card-based interfaces, it's important to consider "
"the performance implications. Each card is a separate view with its "
"own drawing context and event handling. For small numbers of cards, "
"this is fine, but for hundreds or thousands of cards, more efficient "
"approaches like custom drawing or view recycling may be necessary.",
"Yep.",
"The color scheme of an application can significantly affect its "
"usability and aesthetics. Haiku provides several predefined color "
"constants like B_DOCUMENT_BACKGROUND_COLOR and B_PANEL_BACKGROUND_COLOR "
"that help maintain consistency with the system theme.",
"Hey there!",
"In this demo, we have twenty cards with varying amounts of text to "
"demonstrate how the scroll view and card layout work together. The "
"scroll view automatically adjusts its scrollbars based on the total "
"height of the content.",
"End."
};
const int kNumCards = 20;
for (int i = 0; i < kNumCards; i++) {
BString name;
name << "Card" << (i + 1);
CardView* card = new CardView(name.String(), texts[i]);
fContentView->AddChild(card);
}
fScrollView->UpdateScrollBar();
Show();
}
bool
MainWindow::QuitRequested()
{
be_app->PostMessage(B_QUIT_REQUESTED);
return true;
}
void
MainWindow::MessageReceived(BMessage* message)
{
switch (message->what) {
case kMsgAddCard:
{
const char* dummyText = "This is a dynamically added card. "
"It demonstrates that we can append views to the content area "
"at runtime, and the scroll view will correctly adjust to show "
"the new content at the bottom.";
fContentView->AddCard(dummyText);
fScrollView->Invalidate();
fScrollView->UpdateScrollBar();
break;
}
case kMsgInsertCard:
{
const char* dummyText = "This is an inserted card at the top. "
"It pushes all existing cards downward, demonstrating how "
"to insert views dynamically into the beginning of a list.";
fContentView->InsertCard(dummyText);
BScrollBar* vScroll = fScrollView->ScrollBar(B_VERTICAL);
if (vScroll != NULL) {
fScrollView->UpdateScrollBar();
vScroll->SetValue(0);
}
fScrollView->Invalidate();
break;
}
default:
BWindow::MessageReceived(message);
break;
}
}
// ============================================================================
// CardApp
// ============================================================================
class CardApp : public BApplication {
public:
CardApp();
virtual void ReadyToRun();
};
CardApp::CardApp()
:
BApplication(kAppSignature)
{
}
void
CardApp::ReadyToRun()
{
new MainWindow();
}
// ============================================================================
// main
// ============================================================================
int
main()
{
CardApp app;
app.Run();
return 0;
}
// This is a version with splitview
#include <Application.h>
#include <Window.h>
#include <View.h>
#include <ScrollView.h>
#include <TextView.h>
#include <String.h>
#include <MenuBar.h>
#include <Menu.h>
#include <MenuItem.h>
#include <LayoutBuilder.h>
#include <iostream>
static const char* kAppSignature = "application/x-vnd.Test-CardDemo";
static const uint32 kMsgAddCard = 'adcd';
static const uint32 kMsgInsertCard = 'incd'; // New message for Insert
// ============================================================================
// CardView - A card containing wrapped text
// ============================================================================
class CardView : public BView {
public:
CardView(const char* name, const char* text);
void SetWidth(float width);
virtual void Draw(BRect updateRect);
private:
BTextView* fTextView;
static const float kInset;
};
const float CardView::kInset = 15.0f;
CardView::CardView(const char* name, const char* text)
:
BView(name, B_WILL_DRAW | B_FRAME_EVENTS),
fTextView(NULL)
{
SetViewColor(255, 255, 255);
fTextView = new BTextView("text", B_WILL_DRAW);
fTextView->SetText(text);
fTextView->SetViewColor(255, 255, 255);
fTextView->MakeEditable(false);
fTextView->SetWordWrap(true);
fTextView->SetStylable(false);
fTextView->SetFlags(fTextView->Flags() & ~(B_NAVIGABLE | B_FRAME_EVENTS));
rgb_color textColor = ui_color(B_DOCUMENT_TEXT_COLOR);
fTextView->SetFontAndColor(NULL, 0, &textColor);
AddChild(fTextView);
}
void
CardView::SetWidth(float width)
{
float fWidth = width;
float textWidth = fWidth - 2.0f * kInset;
if (textWidth < 10.0f) textWidth = 10.0f;
// Use TextRect to change wrap boundaries without forcing an active structural view resize loop
BRect textRect(0, 0, textWidth, 0);
fTextView->SetTextRect(textRect);
// Request total wrapped height
float textHeight = fTextView->TextHeight(0, fTextView->TextLength());
float totalHeight = textHeight + 2.0f * kInset;
// Perform a singular, unified layout pass
MoveTo(Frame().left, Frame().top);
ResizeTo(fWidth, totalHeight);
fTextView->MoveTo(kInset, kInset);
fTextView->ResizeTo(textWidth, textHeight);
fTextView->SetTextRect(BRect(0, 0, textWidth, textHeight));
}
void
CardView::Draw(BRect updateRect)
{
BView::Draw(updateRect);
SetHighColor(200, 200, 200);
StrokeRect(Bounds());
}
// ============================================================================
// ContentView - Holds all cards, placed inside a BScrollView
// ============================================================================
class ContentView : public BView {
public:
ContentView();
virtual void AttachedToWindow();
void UpdateWidth(float newWidth);
void AddCard(const char* text);
void InsertCard(const char* text); // New method
virtual void FrameResized(float newWidth, float newHeight);
private:
void RelayoutCards();
float fContentHeight;
float fCurrentWidth;
float fInitialWidth;
};
ContentView::ContentView()
:
BView("content", B_WILL_DRAW | B_FRAME_EVENTS),
fContentHeight(0.0f),
fCurrentWidth(-1.0f)
{
SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR));
fInitialWidth = Bounds().Width();
}
void
ContentView::AttachedToWindow()
{
UpdateWidth(Bounds().Width());
}
void
ContentView::UpdateWidth(float newWidth)
{
#if 0
if (newWidth == fCurrentWidth) {
std::cout << "ContentView::UpdateWidth " << "Same width" << std::endl;
return;
}
#endif
fCurrentWidth = newWidth;
RelayoutCards();
}
void
ContentView::RelayoutCards()
{
const float kCardSpacing = 10.0f;
const float kLeftMargin = 20.0f;
float cardWidth = fCurrentWidth - 2.0f * kLeftMargin;
if (cardWidth < 20.0f) cardWidth = 20.0f;
float yOffset = 20.0f;
// Loops through children in list order (0 to N)
for (int i = 0; i < CountChildren(); i++) {
CardView* card = static_cast<CardView*>(ChildAt(i));
if (card != NULL) {
card->SetWidth(cardWidth);
card->MoveTo(kLeftMargin, yOffset);
yOffset += card->Bounds().Height() + kCardSpacing;
}
}
fContentHeight = yOffset + 20.0f;
// 2. Query our visible frame height (the viewport size)
float visibleHeight = Bounds().Height();
// 3. Find the Scroll View's Vertical Scrollbar
BScrollBar* vBar = ScrollBar(B_VERTICAL);
if (vBar != NULL) {
if (fContentHeight <= visibleHeight) {
// Entire list fits on screen; disable scrollbar tracking
vBar->SetRange(0.0f, 0.0f);
} else {
// Content is taller than the viewport window.
// Maximum scroll point is total virtual height minus what's currently visible.
float maxScrollLimit = fContentHeight - visibleHeight;
vBar->SetRange(0.0f, maxScrollLimit);
// Adjust the size of the scrollbar knob proportionally
vBar->SetProportion(visibleHeight / fContentHeight);
}
}
// 4. Force a repaint of the updated layout matrix
Invalidate();
std::cout << "Relayout Finished" << std::endl << std::endl;
}
void
ContentView::AddCard(const char* text)
{
int cardNum = CountChildren() + 1;
BString name;
name << "Card" << cardNum;
CardView* card = new CardView(name.String(), text);
AddChild(card);
RelayoutCards();
}
void
ContentView::InsertCard(const char* text)
{
int cardNum = CountChildren() + 1;
BString name;
name << "Card" << cardNum;
CardView* card = new CardView(name.String(), text);
// Insert at the very beginning of the child list (index 0)
BView* firstChild = ChildAt(0);
if (firstChild != NULL)
AddChild(card, firstChild);
else
AddChild(card);
// Recalculate layout. Since the new card is at index 0, it gets placed
// at the top, and all other cards are pushed down automatically!
RelayoutCards();
}
void
ContentView::FrameResized(float newWidth, float newHeight)
{
BView::FrameResized(newWidth, newHeight);
RelayoutCards();
}
// ============================================================================
// CardScrollView - Intercepts resizing to update width
// ============================================================================
class CardScrollView : public BScrollView {
public:
CardScrollView(const char* name, ContentView* target);
virtual void FrameResized(float newWidth, float newHeight);
private:
ContentView* fContentView;
};
CardScrollView::CardScrollView(const char* name, ContentView* target)
:
BScrollView(name, target, 0, false, true, B_FANCY_BORDER),
fContentView(target)
{
}
void
CardScrollView::FrameResized(float newWidth, float newHeight)
{
std::cout << "CardScrollView " << newWidth << " & " << newHeight << std::endl;
std::cout << "CardScrollView " << Bounds().Width() << " & " << Bounds().Height() << std::endl;
BScrollView::FrameResized(newWidth, newHeight);
float vScrollBarWidth = 0.0f;
BScrollBar* vScroll = ScrollBar(B_VERTICAL);
if (vScroll != NULL && !vScroll->IsHidden())
vScrollBarWidth = vScroll->Bounds().Width();
std::cout << "ScrollBar " << vScroll->Bounds().Width() << " & " << vScroll->Bounds().Height() << std::endl;
const float kBorder = 2.0f;
float contentWidth = newWidth - (2.0f * kBorder) - vScrollBarWidth;
dynamic_cast<ContentView*>(Target())->UpdateWidth(contentWidth);
}
// ============================================================================
// MainWindow
// ============================================================================
class MainWindow : public BWindow {
public:
MainWindow();
virtual bool QuitRequested();
virtual void MessageReceived(BMessage* message);
private:
ContentView* fContentView;
BScrollView* fScrollView;
BMenuBar* fMenuBar;
static const float kMenuHeight;
};
const float MainWindow::kMenuHeight = 25.0f;
MainWindow::MainWindow()
:
BWindow(BRect(50, 50, 450, 400), "Card Demo",
B_DOCUMENT_WINDOW, 0)
{
// 1. Menu Bar
fMenuBar = new BMenuBar("menubar");
BMenu* fileMenu = new BMenu("File");
fileMenu->AddItem(new BMenuItem("Add Card", new BMessage(kMsgAddCard), 'N'));
fileMenu->AddItem(new BMenuItem("Insert Card", new BMessage(kMsgInsertCard), 'I'));
fileMenu->AddSeparatorItem();
fileMenu->AddItem(new BMenuItem("Quit", new BMessage(B_QUIT_REQUESTED), 'Q'));
fMenuBar->AddItem(fileMenu);
// 2. Fixed dimensions for manual layout
const float kBorder = 2.0f;
const float kScrollBarWidth = 16.0f;
float initialContentWidth = Bounds().Width() - (2.0f * kBorder) - kScrollBarWidth;
fContentView = new ContentView();
// 3. Scroll View
fScrollView = new CardScrollView("scroll", fContentView);
// Create dummy text view
BTextView *textView = new BTextView("textView", B_WILL_DRAW);
textView->SetText("This is a summary view showing the selected article content. "
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.");
// Create horizontal split view for left/right panels
BSplitView* mainSplit = new BSplitView(B_HORIZONTAL, 0.0f);
mainSplit->AddChild(textView, 0.3f);
mainSplit->AddChild(fScrollView, 0.7f);
textView->SetExplicitMinSize(BSize(50, B_SIZE_UNSET));
// Build layout
BLayoutBuilder::Group<>(this, B_VERTICAL)
.Add(fMenuBar)
.Add(mainSplit)
.End();
const char* texts[] = {
"Short text.",
"This is a medium length text that should wrap to multiple lines "
"depending on the width of the card.",
"Brief.",
"Haiku is an open-source operating system that specifically targets "
"personal computing. It is inspired by the Be Operating System (BeOS), "
"which was known for its clean design and responsive interface. The goal "
"of Haiku is to create a fast, efficient, and easy-to-use desktop "
"operating system that preserves the spirit of BeOS.",
"Another short one.",
"The BView class is one of the fundamental building blocks of Haiku's "
"interface toolkit. It represents a rectangular area on screen and can "
"handle drawing, mouse events, and keyboard events. Views can be nested "
"to create complex user interfaces.",
"Test.",
"Scroll views are essential for displaying content that exceeds the "
"available screen space. They provide scrollbars that allow users to "
"navigate through the content. In Haiku, the BScrollView class wraps "
"another view to provide this functionality.",
"Cards are a popular UI pattern that groups related information together "
"in a visually distinct container. They typically have a background "
"color, some padding to separate content from edges, and sometimes "
"rounded corners or shadows for depth.",
"Hi!",
"The layout system in Haiku uses a constraint-based approach that makes "
"it easier to create responsive interfaces. The BLayout classes handle "
"the positioning and sizing of child views automatically, reducing the "
"need for manual frame calculations.",
"OK.",
"Text wrapping is an important feature for displaying long passages of "
"text in a constrained space. Without it, text would either overflow "
"the visible area or be truncated, making it difficult to read. "
"BTextView provides built-in support for word wrapping.",
"Simple.",
"When implementing card-based interfaces, it's important to consider "
"the performance implications. Each card is a separate view with its "
"own drawing context and event handling. For small numbers of cards, "
"this is fine, but for hundreds or thousands of cards, more efficient "
"approaches like custom drawing or view recycling may be necessary.",
"Yep.",
"The color scheme of an application can significantly affect its "
"usability and aesthetics. Haiku provides several predefined color "
"constants like B_DOCUMENT_BACKGROUND_COLOR and B_PANEL_BACKGROUND_COLOR "
"that help maintain consistency with the system theme.",
"Hey there!",
"In this demo, we have twenty cards with varying amounts of text to "
"demonstrate how the scroll view and card layout work together. The "
"scroll view automatically adjusts its scrollbars based on the total "
"height of the content.",
"End."
};
const int kNumCards = 20;
for (int i = 0; i < kNumCards; i++) {
BString name;
name << "Card" << (i + 1);
CardView* card = new CardView(name.String(), texts[i]);
fContentView->AddChild(card);
}
Show();
}
bool
MainWindow::QuitRequested()
{
be_app->PostMessage(B_QUIT_REQUESTED);
return true;
}
void
MainWindow::MessageReceived(BMessage* message)
{
switch (message->what) {
case kMsgAddCard:
{
const char* dummyText = "This is a dynamically added card. "
"It demonstrates that we can append views to the content area "
"at runtime, and the scroll view will correctly adjust to show "
"the new content at the bottom.";
fContentView->AddCard(dummyText);
fScrollView->Invalidate();
break;
}
case kMsgInsertCard:
{
const char* dummyText = "This is an inserted card at the top. "
"It pushes all existing cards downward, demonstrating how "
"to insert views dynamically into the beginning of a list.";
fContentView->InsertCard(dummyText);
// Update scroll bar proportions because total height changed
BScrollBar* vScroll = fScrollView->ScrollBar(B_VERTICAL);
if (vScroll != NULL) {
// Scroll all the way to the top so the user can see the new card
vScroll->SetValue(0);
}
fScrollView->Invalidate();
break;
}
default:
BWindow::MessageReceived(message);
break;
}
}
// ============================================================================
// CardApp
// ============================================================================
class CardApp : public BApplication {
public:
CardApp();
virtual void ReadyToRun();
};
CardApp::CardApp()
:
BApplication(kAppSignature)
{
}
void
CardApp::ReadyToRun()
{
new MainWindow();
}
// ============================================================================
// main
// ============================================================================
int
main()
{
CardApp app;
app.Run();
return 0;
}
// This version use vertical layout builder for a BTextView without scrollview and one with scrollview.
// Both do text wrapping and height adjustment when window resizes.
#include <Application.h>
#include <LayoutBuilder.h>
#include <ScrollView.h>
#include <TextView.h>
#include <Window.h>
class AutoWrappingTitleView : public BTextView {
public:
AutoWrappingTitleView(const char* name)
: BTextView(name)
{
SetWordWrap(true);
// Force the view to receive FrameResized notifications
SetFlags(Flags() | B_FRAME_EVENTS);
}
void FrameResized(float newWidth, float newHeight) override
{
BTextView::FrameResized(newWidth, newHeight);
// 1. Force the internal text layout engine to adapt to the new horizontal width
BRect textRect(0, 0, newWidth, newHeight);
SetTextRect(textRect);
// 2. Alert the layout manager to run a fresh layout calculation pass
InvalidateLayout();
}
// Tell the system that this view changes its height depending on its width
bool HasHeightForWidth() override
{
return true;
}
// Calculate exact height requirement based on the provided width
void GetHeightForWidth(float width, float* min, float* max, float* preferred) override
{
// Temporarily adjust text rect to calculate the height for the prospective width
BRect oldRect = TextRect();
BRect tempRect(0, 0, width, oldRect.Height());
SetTextRect(tempRect);
float textHeight = TextHeight(0, CountLines() - 1);
SetTextRect(oldRect); // Restore original formatting box
// Include tiny buffer padding so text doesn't touch the borders
float computedHeight = textHeight + 12.0f;
if (min) *min = computedHeight;
if (max) *max = computedHeight;
if (preferred) *preferred = computedHeight;
}
};
class SampleWindow : public BWindow {
public:
SampleWindow(BRect frame)
: BWindow(frame, "Dynamic Title Wrap", B_TITLED_WINDOW, B_QUIT_ON_WINDOW_CLOSE | B_AUTO_UPDATE_SIZE_LIMITS)
{
// 1. Setup auto-wrapping title text area
fTitleView = new AutoWrappingTitleView("title_view");
fTitleView->SetText("This is an extra long document title string that will instantly wrap to multiple lines when the user makes the window narrow, pushing down the main content pane.");
fTitleView->MakeEditable(true);
// 2. Setup scrolling body text area
fMainTextView = new BTextView("main_text_view");
fMainTextView->SetText("This is the main scrollable workspace.\n\nNotice that as you scale the window boundaries, the title box expands/collapses properly while this viewport takes up whatever remainder room is left over.");
fMainTextView->MakeEditable(true);
fMainScroll = new BScrollView("main_scroll", fMainTextView, 0, true, true);
// 3. Construct Layout Assembly
BLayoutBuilder::Group<>(this, B_VERTICAL, 10.0f)
.SetInsets(10.0f)
.Add(fTitleView, 0) // Weight 0 keeps title exactly at its required height
.Add(fMainScroll, 1); // Weight 1 makes scroll area dynamically scale
}
private:
AutoWrappingTitleView* fTitleView;
BTextView* fMainTextView;
BScrollView* fMainScroll;
};
class SampleApp : public BApplication {
public:
SampleApp()
: BApplication("application/x-vnd.Haiku-TextViewDemo") {}
void ReadyToRun() override {
SampleWindow* window = new SampleWindow(BRect(100, 100, 500, 400));
window->Show();
}
};
int main() {
SampleApp app;
app.Run();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment