You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A smart layout system for panels that display variable numbers of items (network interfaces, storage devices, sensors). The system automatically determines the optimal widget layout (1-4 widgets) based on item count and scales fonts appropriately for each widget size.
Design Principles
Prefer fewer, larger widgets - Better readability from a distance
Consistent content structure - Same information per widget, just scaled
No horizontal scrolling - Fixed widget positions with gaps
Font scaling - Proportional font sizes for each widget tier
Resolution-Agnostic Design
The layout system is resolution-agnostic - all calculations are based on the actual width/height passed to the panel, not hardcoded pixel values. This ensures the system works correctly on any LCD device.
Scaling Philosophy
Widget dimensions: Calculated as fractions of total screen size
Font sizes: Scaled proportionally based on widget height
Gaps/padding: Calculated as percentage of screen dimensions
Base reference: Height is the primary scaling factor (readable text is height-dependent)
// Gap is 1% of the smaller dimension, clamped to reasonable boundsintgap=Math.Clamp((int)(Math.Min(width,height)*0.01f),4,20);
Font Scaling Strategy
Fonts scale based on widget height relative to a reference height. This ensures text remains readable regardless of screen resolution.
Base Font Sizes (Reference: 480px height)
Font Type
Base Size
Purpose
Value
72pt
Large numbers (percentages)
Title
36pt
Section headers
Label
24pt
Field labels
Small
18pt
Details, secondary info
Scaling Formula
// Scale factor based on widget height relative to referencefloatheightScale=widgetHeight/480f;// Widget size multiplier (Full=1.0, Half=0.85, Quarter=0.7)floatsizeMultiplier=GetSizeMultiplier(widgetSize);// Final scalefloatscale=heightScale*sizeMultiplier;// Clamp to reasonable bounds (prevent tiny or huge fonts)scale=Math.Clamp(scale,0.3f,2.0f);
Size Multipliers
Widget Size
Multiplier
Rationale
Full
1.00
Maximum readability
Half
0.85
Slightly smaller for two items
Quarter
0.70
Compact but readable
Example Calculations
Screen
Widget
Height
Scale
Value Font
1280×480
Full
480
1.0 × 1.0 = 1.0
72pt
1280×480
Half
480
1.0 × 0.85 = 0.85
61pt
1280×480
Quarter
235
0.49 × 0.70 = 0.34
24pt
800×480
Full
480
1.0 × 1.0 = 1.0
72pt
800×480
Quarter
235
0.49 × 0.70 = 0.34
24pt
320×240
Full
240
0.5 × 1.0 = 0.5
36pt
320×240
Quarter
115
0.24 × 0.70 = 0.17
12pt
Implementation Components
Phase 1: Core Layout Infrastructure
1.1 WidgetSize Enum
// src/LCDPossible/Panels/Layout/WidgetSize.cspublicenumWidgetSize{Full,// 1280×480 - single item, maximum detailHalf,// 635×480 - two items side by sideQuarter// 635×235 - four items in grid}
// src/LCDPossible/Panels/Layout/WidgetFontSet.cspublicsealedclassWidgetFontSet{publicFontValue{get;}// Large numbers (percentages)publicFontTitle{get;}// Section headerspublicFontLabel{get;}// Field labelspublicFontSmall{get;}// Details, secondary infopublicfloatScale{get;}// For reference// Base font sizes at reference height (480px)privateconstfloatBaseValueSize=72f;privateconstfloatBaseTitleSize=36f;privateconstfloatBaseLabelSize=24f;privateconstfloatBaseSmallSize=18f;privateconstfloatReferenceHeight=480f;/// <summary>/// Creates a font set scaled for the given widget dimensions./// Fonts scale proportionally based on widget height./// </summary>publicstaticWidgetFontSetCreate(FontFamilyfamily,WidgetBoundsbounds){// Calculate scale based on widget height relative to referencefloatheightScale=bounds.Height/ReferenceHeight;// Apply size multiplier based on widget sizefloatsizeMultiplier=bounds.Sizeswitch{WidgetSize.Full=>1.0f,WidgetSize.Half=>0.85f,WidgetSize.Quarter=>0.70f,
_ =>1.0f};// Final scale with reasonable boundsfloatscale=Math.Clamp(heightScale*sizeMultiplier,0.3f,2.0f);// Minimum font sizes for readabilityfloatvalueSize=Math.Max(BaseValueSize*scale,14f);floattitleSize=Math.Max(BaseTitleSize*scale,10f);floatlabelSize=Math.Max(BaseLabelSize*scale,8f);floatsmallSize=Math.Max(BaseSmallSize*scale,6f);returnnewWidgetFontSet{Value=family.CreateFont(valueSize,FontStyle.Bold),Title=family.CreateFont(titleSize,FontStyle.Bold),Label=family.CreateFont(labelSize,FontStyle.Regular),Small=family.CreateFont(smallSize,FontStyle.Regular),Scale=scale};}}
1.4 WidgetLayout Class
// src/LCDPossible/Panels/Layout/WidgetLayout.cspublicsealedclassWidgetLayout{publicintTotalWidth{get;}publicintTotalHeight{get;}publicIReadOnlyList<WidgetBounds>Widgets{get;}publicintDisplayedCount{get;}// Actual items shownpublicintOverflowCount{get;}// Items not shown (0 if all fit)/// <summary>/// Calculates widget layout based on screen dimensions and item count./// All calculations are proportional - no hardcoded pixel values./// </summary>publicstaticWidgetLayoutCalculate(intwidth,intheight,intitemCount){// Gap is 1% of smaller dimension, clamped to reasonable boundsintgap=Math.Clamp((int)(Math.Min(width,height)*0.01f),4,20);varwidgets=newList<WidgetBounds>();intdisplayedCount=Math.Min(itemCount,4);intoverflowCount=Math.Max(0,itemCount-3);// Show 3 + overflow if 4+switch(itemCount){case0:break;case1:// Full panelwidgets.Add(newWidgetBounds{X=0,Y=0,Width=width,Height=height,Size=WidgetSize.Full});break;case2:// Side by sideinthalfWidth=(width-gap)/2;widgets.Add(newWidgetBounds{X=0,Y=0,Width=halfWidth,Height=height,Size=WidgetSize.Half});widgets.Add(newWidgetBounds{X=halfWidth+gap,Y=0,Width=width-halfWidth-gap,Height=height,Size=WidgetSize.Half});break;case3:// Left large + right stackintleftWidth=(width-gap)/2;intrightWidth=width-leftWidth-gap;inttopHeight=(height-gap)/2;intbottomHeight=height-topHeight-gap;widgets.Add(newWidgetBounds{X=0,Y=0,Width=leftWidth,Height=height,Size=WidgetSize.Half});widgets.Add(newWidgetBounds{X=leftWidth+gap,Y=0,Width=rightWidth,Height=topHeight,Size=WidgetSize.Quarter});widgets.Add(newWidgetBounds{X=leftWidth+gap,Y=topHeight+gap,Width=rightWidth,Height=bottomHeight,Size=WidgetSize.Quarter});break;default:// 4 or more// 2x2 grid (3 items + overflow indicator for 5+)intcolWidth=(width-gap)/2;introwHeight=(height-gap)/2;widgets.Add(newWidgetBounds{X=0,Y=0,Width=colWidth,Height=rowHeight,Size=WidgetSize.Quarter});widgets.Add(newWidgetBounds{X=colWidth+gap,Y=0,Width=width-colWidth-gap,Height=rowHeight,Size=WidgetSize.Quarter});widgets.Add(newWidgetBounds{X=0,Y=rowHeight+gap,Width=colWidth,Height=height-rowHeight-gap,Size=WidgetSize.Quarter});widgets.Add(newWidgetBounds{X=colWidth+gap,Y=rowHeight+gap,Width=width-colWidth-gap,Height=height-rowHeight-gap,Size=WidgetSize.Quarter});displayedCount=itemCount>4?3:4;// 4th slot is overflow if 5+overflowCount=itemCount>4?itemCount-3:0;break;}returnnewWidgetLayout(width,height,widgets,displayedCount,overflowCount);}}
1.5 WidgetRenderContext Class
// src/LCDPossible/Panels/Layout/WidgetRenderContext.cspublicsealedclassWidgetRenderContext{publicWidgetBoundsBounds{get;}publicWidgetFontSetFonts{get;}publicResolvedColorSchemeColors{get;}publicintIndex{get;}// 0-based widget indexpublicboolIsOverflowWidget{get;}publicintOverflowCount{get;}// Items not shown// Helper methods for positioning within widgetpublicPointFCenter=>new(Bounds.X+Bounds.Width/2f,Bounds.Y+Bounds.Height/2f);publicintContentX=>Bounds.X+Padding;publicintContentY=>Bounds.Y+Padding;publicintContentWidth=>Bounds.Width-Padding*2;publicintContentHeight=>Bounds.Height-Padding*2;privateconstintPadding=15;}
Phase 2: Base Panel Class
2.1 SmartLayoutPanel Abstract Class
// src/LCDPossible/Panels/SmartLayoutPanel.cspublicabstractclassSmartLayoutPanel<TItem>:BaseLivePanel{// Abstract: Get items to displayprotectedabstractTask<IReadOnlyList<TItem>>GetItemsAsync(CancellationTokenct);// Abstract: Render single item into widget areaprotectedabstractvoidRenderWidget(IImageProcessingContextctx,WidgetRenderContextwidget,TItemitem);// Virtual: Render when no items available (default shows "No items" message)protectedvirtualvoidRenderEmptyState(IImageProcessingContextctx,intwidth,intheight){if(!FontsLoaded||TitleFont==null)return;varmessage=GetEmptyStateMessage();DrawCenteredText(ctx,message,width/2f,height/2f-20,TitleFont,Colors.TextMuted);}// Virtual: Get message for empty state (override to customize)protectedvirtualstringGetEmptyStateMessage()=>"No items available";// Virtual: Render overflow indicator (default implementation provided)protectedvirtualvoidRenderOverflowWidget(IImageProcessingContextctx,WidgetRenderContextwidget){// Draw centered "+N more" messagevarmessage=$"+{widget.OverflowCount} more";DrawCenteredText(ctx,message,widget.Center.X,widget.Center.Y,widget.Fonts.Title,Colors.TextSecondary);}// Template method - handles layout calculation and delegationpublicsealedoverrideasyncTask<Image<Rgba32>>RenderFrameAsync(intwidth,intheight,CancellationTokenct){varitems=awaitGetItemsAsync(ct);varimage=CreateBaseImage(width,height);image.Mutate(ctx =>{// Handle empty stateif(items.Count==0){RenderEmptyState(ctx,width,height);DrawTimestamp(ctx,width,height);return;}// Calculate layout for itemsvarlayout=WidgetLayout.Calculate(width,height,items.Count);// Render item widgetsfor(inti=0;i<layout.DisplayedCount;i++){varwidgetCtx=CreateWidgetContext(layout.Widgets[i],i,layout);RenderWidget(ctx,widgetCtx,items[i]);}// Render overflow widget if neededif(layout.OverflowCount>0){varoverflowCtx=CreateOverflowContext(layout);RenderOverflowWidget(ctx,overflowCtx);}DrawTimestamp(ctx,width,height);});returnimage;}}
Phase 3: Concrete Panel Implementations
3.1 NetworkInfoPanel
// src/LCDPossible/Panels/NetworkPanels.cspublicsealedclassNetworkInfoPanel:SmartLayoutPanel<NetworkInterfaceInfo>{publicoverridestringPanelId=>"network-info";publicoverridestringDisplayName=>"Network Interfaces";protectedoverrideasyncTask<IReadOnlyList<NetworkInterfaceInfo>>GetItemsAsync(CancellationTokenct){// Get active network interfaces from systemreturnawait_provider.GetNetworkInterfacesAsync(ct);}protectedoverridevoidRenderWidget(IImageProcessingContextctx,WidgetRenderContextwidget,NetworkInterfaceInfoiface){// Interface name (title)DrawText(ctx,iface.Name,widget.ContentX,widget.ContentY,widget.Fonts.Title,widget.Colors.Accent);// IP Address (large value)DrawCenteredText(ctx,iface.IpAddress,widget.Center.X,widget.Bounds.Y+80,widget.Fonts.Value,widget.Colors.TextPrimary);// Speed/StatusDrawText(ctx,$"{iface.Speed} Mbps",widget.ContentX,widget.ContentY+120,widget.Fonts.Label,widget.Colors.TextSecondary);// Traffic (RX/TX)DrawText(ctx,$"↓ {iface.RxRate} ↑ {iface.TxRate}",widget.ContentX,widget.ContentY+150,widget.Fonts.Small,widget.Colors.TextMuted);}}
// Add to ISystemInfoProvider or create separate interfacespublicinterfaceINetworkInfoProvider{Task<IReadOnlyList<NetworkInterfaceInfo>>GetNetworkInterfacesAsync(CancellationTokenct);}publicinterfaceIStorageInfoProvider{Task<IReadOnlyList<StorageDriveInfo>>GetStorageDrivesAsync(CancellationTokenct);}publicinterfaceISensorInfoProvider{Task<IReadOnlyList<SensorInfo>>GetSensorsAsync(CancellationTokenct);}
// Add to panel type registry{"network-info",newPanelMetadata("Network Interfaces","System",true,false)},{"storage-info",newPanelMetadata("Storage Drives","System",true,false)},{"sensors-info",newPanelMetadata("Temperature Sensors","System",true,false)},