@@ -46,6 +46,16 @@ public final class WaypointsScreen extends Screen
4646 // Dimension filter (null = show all)
4747 private net .wurstclient .waypoints .WaypointDimension filterDim = null ;
4848
49+ private static enum SortMode
50+ {
51+ DATE ,
52+ NEAREST ,
53+ FURTHEST ,
54+ NAME
55+ }
56+
57+ private SortMode sortMode = SortMode .DATE ;
58+
4959 // One row = 4 buttons. We keep references so we can reposition & toggle
5060 // visibility.
5161 private static final class RowWidgets
@@ -62,6 +72,7 @@ private static final class RowWidgets
6272 // Persisted scroll per-world+dimension so returning keeps the same
6373 // position even if a new screen instance was created.
6474 private static final Map <String , Integer > savedScrolls = new HashMap <>();
75+ private static final Map <String , SortMode > savedSortModes = new HashMap <>();
6576
6677 private void saveScrollState ()
6778 {
@@ -70,6 +81,7 @@ private void saveScrollState()
7081 String key = resolveWorldId () + ":"
7182 + (filterDim == null ? "ALL" : filterDim .name ());
7283 savedScrolls .put (key , scroll );
84+ savedSortModes .put (key , sortMode );
7385 }catch (Exception ignored )
7486 {}
7587 }
@@ -172,6 +184,39 @@ protected void init()
172184 if (filterDim == null || w .getDimension () == filterDim )
173185 cachedList .add (w );
174186 }
187+
188+ // Apply sorting
189+ switch (sortMode )
190+ {
191+ case DATE -> cachedList .sort (
192+ (a , b ) -> Long .compare (b .getCreatedAt (), a .getCreatedAt ()));
193+ case NAME -> cachedList
194+ .sort ((a , b ) -> a .getName ().compareToIgnoreCase (b .getName ()));
195+ case NEAREST ->
196+ {
197+ if (minecraft .player != null )
198+ {
199+ BlockPos playerPos =
200+ BlockPos .containing (minecraft .player .getX (),
201+ minecraft .player .getY (), minecraft .player .getZ ());
202+ cachedList .sort (
203+ (a , b ) -> Double .compare (a .getPos ().distSqr (playerPos ),
204+ b .getPos ().distSqr (playerPos )));
205+ }
206+ }
207+ case FURTHEST ->
208+ {
209+ if (minecraft .player != null )
210+ {
211+ BlockPos playerPos =
212+ BlockPos .containing (minecraft .player .getX (),
213+ minecraft .player .getY (), minecraft .player .getZ ());
214+ cachedList .sort (
215+ (a , b ) -> Double .compare (b .getPos ().distSqr (playerPos ),
216+ a .getPos ().distSqr (playerPos )));
217+ }
218+ }
219+ }
175220 listStartY = y ;
176221
177222 // Determine viewport for the list (space until the Back button line)
@@ -186,6 +231,9 @@ protected void init()
186231 Integer s = savedScrolls .get (key );
187232 if (s != null )
188233 scroll = s ;
234+ SortMode sm = savedSortModes .get (key );
235+ if (sm != null )
236+ sortMode = sm ;
189237 }catch (Exception ignored )
190238 {}
191239
@@ -263,9 +311,26 @@ protected void init()
263311 scrollToBottom ();
264312 }).bounds (arrowX + 20 , viewportBottom , 20 , 20 ).build ());
265313
314+ // Back button (smaller) and Sort button on same line
315+ int backWidth = 200 ;
316+ int sortWidth = 90 ;
317+ addRenderableWidget (
318+ Button .builder (Component .literal ("Sort: " + sortMode .name ()), b -> {
319+ // cycle sort modes
320+ sortMode = switch (sortMode )
321+ {
322+ case DATE -> SortMode .NEAREST ;
323+ case NEAREST -> SortMode .FURTHEST ;
324+ case FURTHEST -> SortMode .NAME ;
325+ default -> SortMode .DATE ;
326+ };
327+ saveScrollState ();
328+ minecraft .setScreen (this );
329+ }).bounds (x , this .height - 28 , sortWidth , 20 ).build ());
266330 addRenderableWidget (Button
267331 .builder (Component .literal ("Back" ), b -> minecraft .setScreen (prev ))
268- .bounds (x , this .height - 28 , 300 , 20 ).build ());
332+ .bounds (x + sortWidth + 10 , this .height - 28 , backWidth , 20 )
333+ .build ());
269334 }
270335
271336 private void scrollBy (int dy )
@@ -412,12 +477,58 @@ public void render(GuiGraphics context, int mouseX, int mouseY, float delta)
412477 rw .copyBtn .visible = visible ;
413478 }
414479
480+ // Hide default button labels for name buttons so we can draw a single
481+ // centered/scrolling label later and avoid duplicate text (built-in
482+ // rendering + our custom rendering causing two copies).
483+ java .util .Map <RowWidgets , Component > _savedLabels =
484+ new java .util .HashMap <>();
485+ for (RowWidgets _rw : rows )
486+ {
487+ if (_rw == null || _rw .nameBtn == null || !_rw .nameBtn .visible )
488+ continue ;
489+ _savedLabels .put (_rw , _rw .nameBtn .getMessage ());
490+ _rw .nameBtn .setMessage (Component .literal ("" ));
491+ }
492+
415493 super .render (context , mouseX , mouseY , delta );
416494
495+ // Restore messages so our custom drawing can read them
496+ for (java .util .Map .Entry <RowWidgets , Component > e : _savedLabels
497+ .entrySet ())
498+ {
499+ RowWidgets _rw = e .getKey ();
500+ if (_rw != null && _rw .nameBtn != null )
501+ _rw .nameBtn .setMessage (e .getValue ());
502+ }
503+
417504 // Title
418505 context .drawCenteredString (minecraft .font , "Waypoints" , this .width / 2 ,
419506 12 , 0xFFFFFFFF );
420507
508+ // Draw beacon button backgrounds (overlay) and collect line outlines
509+ java .util .List <RowWidgets > lineOutlineRows =
510+ new java .util .ArrayList <>();
511+ for (RowWidgets rw : rows )
512+ {
513+ if (rw == null || rw .nameBtn == null || !rw .nameBtn .visible )
514+ continue ;
515+ if (rw .w .hasBeacon ())
516+ {
517+ int nameLeft = x ; // name button left
518+ int nameTop = rw .nameBtn .getY ();
519+ int nameRight = nameLeft + 140 ; // name button width
520+ int nameBottom = nameTop + 20 ;
521+ int baseColor = rw .w .getColor ();
522+ // overlay with ~40% opacity so button shading remains visible
523+ int overlay = (baseColor & 0x00FFFFFF ) | 0x66000000 ; //
524+ context .fill (nameLeft , nameTop , nameRight , nameBottom , overlay );
525+ // label will be drawn later (consistent scrolling/clip)
526+ {}
527+ }
528+ if (rw .w .isLines ())
529+ lineOutlineRows .add (rw );
530+ }
531+
421532 // Draw small color boxes for each saved waypoint next to the name
422533 // Use the same filtered list as the rows to render color boxes and clip
423534 // to viewport
@@ -458,10 +569,90 @@ public void render(GuiGraphics context, int mouseX, int mouseY, float delta)
458569 0xFF333333 );
459570 // fill
460571 context .fill (boxLeft , boxY , boxLeft + 16 , boxY + 16 , color );
572+
461573 }
462574
463575 context .disableScissor ();
464576
577+ // Draw line outlines collected earlier (use waypoint color and clamp to
578+ // screen to avoid cut-off)
579+ for (RowWidgets rw : lineOutlineRows )
580+ {
581+ if (rw == null || rw .nameBtn == null || !rw .nameBtn .visible )
582+ continue ;
583+ int nameLeft = x ;
584+ int nameTop = rw .nameBtn .getY ();
585+ int nameRight = nameLeft + 140 ;
586+ int nameBottom = nameTop + 20 ;
587+ int stroke = rw .w .getColor ();
588+ int top = Math .max (0 , nameTop - 1 );
589+ int bottom = Math .min (this .height , nameBottom + 1 );
590+ if (top >= bottom )
591+ continue ;
592+ // top
593+ context .fill (nameLeft - 1 , top , nameRight + 1 ,
594+ Math .min (bottom , nameTop ), stroke );
595+ // bottom
596+ context .fill (nameLeft - 1 , Math .max (top , nameBottom ), nameRight + 1 ,
597+ bottom , stroke );
598+ // left
599+ context .fill (nameLeft - 1 , top , nameLeft , bottom , stroke );
600+ // right
601+ context .fill (nameRight , top , nameRight + 1 , bottom , stroke );
602+ }
603+
604+ // Draw labels for all visible name buttons with clipping and centered
605+ // scrolling
606+ for (RowWidgets rw : rows )
607+ {
608+ if (rw == null || rw .nameBtn == null || !rw .nameBtn .visible )
609+ continue ;
610+ int nameLeft = x ;
611+ int nameTop = rw .nameBtn .getY ();
612+ int nameRight = nameLeft + 140 ;
613+ int nameBottom = nameTop + 20 ;
614+ String label = rw .nameBtn .getMessage ().getString ();
615+ int textWidth = minecraft .font .width (label );
616+ int centerX = (nameLeft + nameRight ) / 2 ;
617+ int maxTextWidth = nameRight - nameLeft - 8 ; // padding both sides
618+ if (maxTextWidth < 0 )
619+ maxTextWidth = 0 ;
620+ context .enableScissor (nameLeft , nameTop , nameRight , nameBottom );
621+ if (textWidth <= maxTextWidth || maxTextWidth == 0 )
622+ {
623+ // draw centered
624+ context .drawCenteredString (minecraft .font , label , centerX ,
625+ nameTop + 6 , 0xFFFFFFFF );
626+ }else
627+ {
628+ int gap = 20 ;
629+ int period = textWidth + gap ;
630+ long t = System .currentTimeMillis ();
631+ int speed = 90 ; // text scroll, ms pixel
632+ int offset = (int )((t / (long )speed ) % period );
633+ int x1 = centerX - textWidth / 2 - offset ;
634+ context .drawString (minecraft .font , label , x1 , nameTop + 6 ,
635+ 0xFFFFFFFF );
636+ context .drawString (minecraft .font , label , x1 + textWidth + gap ,
637+ nameTop + 6 , 0xFFFFFFFF );
638+ }
639+ context .disableScissor ();
640+ }
641+
642+ // Darken the "Hide" buttons for hidden waypoints with a 25% black
643+ // overlay so they appear darker.
644+ for (RowWidgets _rw : rows )
645+ {
646+ if (_rw == null || _rw .visBtn == null || !_rw .visBtn .visible )
647+ continue ;
648+ if (!_rw .w .isVisible ())
649+ {
650+ int vx = x + 145 ;
651+ int vy = _rw .visBtn .getY ();
652+ context .fill (vx , vy , vx + 55 , vy + 20 , 0x66000000 );
653+ }
654+ }
655+
465656 // Draw scrollbar track & thumb (to the right of the list area)
466657 int contentHeight = rows .size () * ROW_HEIGHT ;
467658 int trackWidth = 8 ;
0 commit comments