package net.minecraft.client.gui.components.tabs; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Optional; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.Renderable; import net.minecraft.client.gui.components.TabButton; import net.minecraft.client.gui.components.events.AbstractContainerEventHandler; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.layouts.LinearLayout; import net.minecraft.client.gui.narration.NarratableEntry; import net.minecraft.client.gui.narration.NarratedElementType; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.client.gui.navigation.FocusNavigationEvent.TabNavigation; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.renderer.RenderType; import net.minecraft.network.chat.Component; import net.minecraft.util.Mth; import org.jetbrains.annotations.Nullable; @Environment(EnvType.CLIENT) public class TabNavigationBar extends AbstractContainerEventHandler implements Renderable, NarratableEntry { private static final int NO_TAB = -1; private static final int MAX_WIDTH = 400; private static final int HEIGHT = 24; private static final int MARGIN = 14; private static final Component USAGE_NARRATION = Component.translatable("narration.tab_navigation.usage"); private final LinearLayout layout = LinearLayout.horizontal(); private int width; private final TabManager tabManager; private final ImmutableList tabs; private final ImmutableList tabButtons; TabNavigationBar(int width, TabManager tabManager, Iterable tabs) { this.width = width; this.tabManager = tabManager; this.tabs = ImmutableList.copyOf(tabs); this.layout.defaultCellSetting().alignHorizontallyCenter(); ImmutableList.Builder builder = ImmutableList.builder(); for (Tab tab : tabs) { builder.add(this.layout.addChild(new TabButton(tabManager, tab, 0, 24))); } this.tabButtons = builder.build(); } public static TabNavigationBar.Builder builder(TabManager tabManager, int width) { return new TabNavigationBar.Builder(tabManager, width); } public void setWidth(int width) { this.width = width; } @Override public boolean isMouseOver(double mouseX, double mouseY) { return mouseX >= this.layout.getX() && mouseY >= this.layout.getY() && mouseX < this.layout.getX() + this.layout.getWidth() && mouseY < this.layout.getY() + this.layout.getHeight(); } @Override public void setFocused(boolean focused) { super.setFocused(focused); if (this.getFocused() != null) { this.getFocused().setFocused(focused); } } @Override public void setFocused(@Nullable GuiEventListener focused) { super.setFocused(focused); if (focused instanceof TabButton tabButton) { this.tabManager.setCurrentTab(tabButton.tab(), true); } } @Nullable @Override public ComponentPath nextFocusPath(FocusNavigationEvent event) { if (!this.isFocused()) { TabButton tabButton = this.currentTabButton(); if (tabButton != null) { return ComponentPath.path(this, ComponentPath.leaf(tabButton)); } } return event instanceof TabNavigation ? null : super.nextFocusPath(event); } @Override public List children() { return this.tabButtons; } @Override public NarratableEntry.NarrationPriority narrationPriority() { return (NarratableEntry.NarrationPriority)this.tabButtons .stream() .map(AbstractWidget::narrationPriority) .max(Comparator.naturalOrder()) .orElse(NarratableEntry.NarrationPriority.NONE); } @Override public void updateNarration(NarrationElementOutput narrationElementOutput) { Optional optional = this.tabButtons.stream().filter(AbstractWidget::isHovered).findFirst().or(() -> Optional.ofNullable(this.currentTabButton())); optional.ifPresent(tabButton -> { this.narrateListElementPosition(narrationElementOutput.nest(), tabButton); tabButton.updateNarration(narrationElementOutput); }); if (this.isFocused()) { narrationElementOutput.add(NarratedElementType.USAGE, USAGE_NARRATION); } } /** * Narrates the position of a list element (tab button). * * @param narrationElementOutput the narration output to update. * @param tabButton the tab button whose position is being narrated. */ protected void narrateListElementPosition(NarrationElementOutput narrationElementOutput, TabButton tabButton) { if (this.tabs.size() > 1) { int i = this.tabButtons.indexOf(tabButton); if (i != -1) { narrationElementOutput.add(NarratedElementType.POSITION, Component.translatable("narrator.position.tab", i + 1, this.tabs.size())); } } } @Override public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { guiGraphics.blit( RenderType::guiTextured, Screen.HEADER_SEPARATOR, 0, this.layout.getY() + this.layout.getHeight() - 2, 0.0F, 0.0F, ((TabButton)this.tabButtons.get(0)).getX(), 2, 32, 2 ); int i = ((TabButton)this.tabButtons.get(this.tabButtons.size() - 1)).getRight(); guiGraphics.blit(RenderType::guiTextured, Screen.HEADER_SEPARATOR, i, this.layout.getY() + this.layout.getHeight() - 2, 0.0F, 0.0F, this.width, 2, 32, 2); for (TabButton tabButton : this.tabButtons) { tabButton.render(guiGraphics, mouseX, mouseY, partialTick); } } @Override public ScreenRectangle getRectangle() { return this.layout.getRectangle(); } /** * Arranges the elements within the tabbed layout. */ public void arrangeElements() { int i = Math.min(400, this.width) - 28; int j = Mth.roundToward(i / this.tabs.size(), 2); for (TabButton tabButton : this.tabButtons) { tabButton.setWidth(j); } this.layout.arrangeElements(); this.layout.setX(Mth.roundToward((this.width - i) / 2, 2)); this.layout.setY(0); } /** * Selects the tab at the specified index. * * @param index the index of the tab to select. * @param playClickSound whether to play a click sound when selecting the tab. */ public void selectTab(int index, boolean playClickSound) { if (this.isFocused()) { this.setFocused((GuiEventListener)this.tabButtons.get(index)); } else { this.tabManager.setCurrentTab((Tab)this.tabs.get(index), playClickSound); } } /** * Handles key pressed events. *

* @return {@code true} if the key press was handled, {@code false} otherwise. * * @param keycode the keycode of the pressed key. */ public boolean keyPressed(int keycode) { if (Screen.hasControlDown()) { int i = this.getNextTabIndex(keycode); if (i != -1) { this.selectTab(Mth.clamp(i, 0, this.tabs.size() - 1), true); return true; } } return false; } /** * Returns the index of the next tab based on the given key code. *

* @return the index of the next tab, or -1 if no valid tab index is found. * * @param keycode the keycode of the pressed key. */ private int getNextTabIndex(int keycode) { if (keycode >= 49 && keycode <= 57) { return keycode - 49; } else { if (keycode == 258) { int i = this.currentTabIndex(); if (i != -1) { int j = Screen.hasShiftDown() ? i - 1 : i + 1; return Math.floorMod(j, this.tabs.size()); } } return -1; } } /** * Returns the index of the current tab. *

* @return the index of the current tab, or -1 if no current tab is set. */ private int currentTabIndex() { Tab tab = this.tabManager.getCurrentTab(); int i = this.tabs.indexOf(tab); return i != -1 ? i : -1; } /** * Returns the current tab button. *

* @return the current tab button, or null if no current tab is set. */ @Nullable private TabButton currentTabButton() { int i = this.currentTabIndex(); return i != -1 ? (TabButton)this.tabButtons.get(i) : null; } /** * Builder class for creating a TabNavigationBar instance. */ @Environment(EnvType.CLIENT) public static class Builder { private final int width; private final TabManager tabManager; private final List tabs = new ArrayList(); Builder(TabManager tabManager, int width) { this.tabManager = tabManager; this.width = width; } /** * Adds multiple tabs to the TabNavigationBar. *

* @return the {@link Builder} instance. * * @param tabs the tabs to add. */ public TabNavigationBar.Builder addTabs(Tab... tabs) { Collections.addAll(this.tabs, tabs); return this; } /** * Builds and returns a new TabNavigationBar instance. *

* @return a new TabNavigationBar instance. */ public TabNavigationBar build() { return new TabNavigationBar(this.width, this.tabManager, this.tabs); } } }