Cacao: More docs

This commit is contained in:
Shadowfacts 2021-02-28 12:19:09 -05:00
parent 500ad94442
commit 2c19b8456b
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
16 changed files with 322 additions and 105 deletions

View File

@ -0,0 +1,14 @@
package net.shadowfacts.phycon.mixin.client;
import net.minecraft.client.gui.widget.TextFieldWidget;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
/**
* @author shadowfacts
*/
@Mixin(TextFieldWidget.class)
public interface TextFieldWidgetAccessor {
@Accessor("maxLength")
int cacao_getMaxLength();
}

View File

@ -24,14 +24,15 @@ open class CacaoScreen(title: Text = LiteralText("CacaoScreen")): Screen(title),
private val _windows = LinkedList<Window>()
/**
* The list of windows that belong to this screen.
*
* The window at the end of this list is the active window is the only window that will receive input events.
*
* This list should never be modified directly, only by using the [addWindow]/[removeWindow] methods.
*/
override val windows: List<Window> = _windows
/**
* Adds the given window to this screen's window list.
* By default, the new window is added at the tail of the window list, making it the active window.
* Only the active window will receive input events.
* Adds the given window to this screen's window list at the given position.
*
* @param window The Window to add to this screen.
* @param index The index to insert the window into the window list at.
@ -47,6 +48,9 @@ open class CacaoScreen(title: Text = LiteralText("CacaoScreen")): Screen(title),
return window
}
/**
* Adds the given window to the end of this screen's window list, making it the active window.
*/
override fun <T : Window> addWindow(window: T): T {
return addWindow(window, _windows.size)
}

View File

@ -4,17 +4,42 @@ import net.shadowfacts.cacao.util.KeyModifiers
import net.shadowfacts.cacao.window.Window
/**
* A responder is an object which participates in the responder chain, a mechanism for propagating indirect events (i.e.
* events for which there is no on-screen position to determine the targeted view, such as key presses) through the
* view hierarchy.
*
* @author shadowfacts
*/
interface Responder {
/**
* The window that this responder is part of.
*/
val window: Window?
/**
* Whether this responder is the first responder of its window.
*
* `false` if [window] is null.
*
* @see Window.firstResponder
*/
val isFirstResponder: Boolean
get() = window?.firstResponder === this
/**
* The next responder in the chain after this. The next responder will receive an event if this responder did not
* accept it.
*
* The next responder may be `null` if this responder is at the end of the chain.
*/
val nextResponder: Responder?
/**
* Makes this responder become the window's first responder.
* @throws RuntimeException if [window] is null
* @see Window.firstResponder
*/
fun becomeFirstResponder() {
if (window == null) {
throw RuntimeException("Cannot become first responder while not in Window")
@ -22,8 +47,17 @@ interface Responder {
window!!.firstResponder = this
}
/**
* Called immediately after this responder has become the window's first responder.
* @see Window.firstResponder
*/
fun didBecomeFirstResponder() {}
/**
* Removes this object as the window's first responder.
* @throws RuntimeException if [window] is null
* @see Window.firstResponder
*/
fun resignFirstResponder() {
if (window == null) {
throw RuntimeException("Cannot resign first responder while not in Window")
@ -31,14 +65,36 @@ interface Responder {
window!!.firstResponder = null
}
/**
* Called immediately before this object is removed as the window's first responder.
* @see Window.firstResponder
*/
fun didResignFirstResponder() {}
/**
* Called when a character has been typed.
*
* @param char The character that was typed.
* @param modifiers The key modifiers that were held down when the character was typed.
* @return Whether this responder accepted the event. If `true`, it will not be passed to the next responder.
*/
fun charTyped(char: Char, modifiers: KeyModifiers): Boolean {
return false
}
/**
* Called when a keyboard key is pressed.
*
* If the pressed key is a typed character, [charTyped] will also be called. The order in which the methods are
* invoked is undefined and should not be relied upon.
*
* @param keyCode The integer code of the key that was pressed.
* @param modifiers The key modifiers that were held down when the character was typed.
* @return Whether this responder accepted the event. If `true`, it will not be passed to the next responder.
* @see org.lwjgl.glfw.GLFW for key code constants
*/
fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean {
return false
}
}
}

View File

@ -3,7 +3,10 @@ package net.shadowfacts.cacao.util
import org.lwjgl.glfw.GLFW
/**
* A set of modifier keys that are being pressed at given instant.
*
* @author shadowfacts
* @param value The integer representation of the pressed modifiers as received from GLFW.
*/
class KeyModifiers(val value: Int) {
@ -29,4 +32,4 @@ class KeyModifiers(val value: Int) {
return (value and mod) == mod
}
}
}

View File

@ -29,6 +29,10 @@ open class View(): Responder {
*/
override var window: Window? = null
/**
* The next responder after this one.
* For views, the next responder is the view's superview.
*/
override val nextResponder: Responder?
// todo: should the view controller be a Responder?
get() = superview
@ -39,7 +43,7 @@ open class View(): Responder {
}
}
/**
* The constraint solver used by the [net.shadowfacts.cacao.Window] this view belongs to.
* The constraint solver used by the [Window] this view belongs to.
* Not initialized until [wasAdded] called, using it before that will throw a runtime exception.
*/
var solver: Solver by solverDelegate
@ -189,7 +193,9 @@ open class View(): Responder {
}
/**
* Removes the given view from this view's children and removes all constraints associated with it.
* Removes the given view from this view's children and removes all constraints that connect the subview or any of
* its children (recursively) to a view outside of the subview's hierarchy. Constraints internal to the subview's
* hierarchy (e.g., one between the subview and its child) will be left in place.
*
* @param view The view to removed as a child of this view.
* @throws RuntimeException If the given [view] is not a subview of this view.
@ -199,7 +205,6 @@ open class View(): Responder {
throw RuntimeException("Cannot remove subview whose superview is not this view")
}
_subviews.remove(view)
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
@ -350,7 +355,13 @@ open class View(): Responder {
open fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {}
/**
* Called when this view is clicked. May delegate to [subviews].
* Called when this view is clicked.
*
* The base implementation of this method forwards the click event to the first subview (sorted by [zIndex]) that
* contains the clicked point. Additionally, any subviews of this view that do not contain the clicked point receive
* the [mouseClickedOutside] event. If multiple views contain the point, any after one that returns `true` from this
* method will not receive the event or the click-outside event.
*
* If overridden, the super-class method does not have to be called. Intentionally not calling it may be used
* to prevent [subviews] from receiving click events.
*
@ -373,6 +384,14 @@ open class View(): Responder {
return result
}
/**
* Called when the mouse was clicked outside this view.
*
* The base implementation of this method simply forwards the event to all of this view's subviews.
*
* @param point The clicked point _in the coordinate space of this view_.
* @param mouseButton The mouse button used to click.
*/
open fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
for (view in subviews) {
val pointInView = convert(point, to = view)

View File

@ -10,8 +10,8 @@ import net.shadowfacts.cacao.view.View
import net.shadowfacts.kiwidsl.dsl
/**
* A abstract button class. Cannot be constructed directly, used for creating button implementations with their own logic.
* Use [Button] for a generic no-frills button.
* An abstract button class. Cannot be constructed directly, used for creating button implementations with their own
* logic. Use [Button] for a generic no-frills button.
*
* @author shadowfacts
* @param Impl The type of the concrete implementation of the button.

View File

@ -8,9 +8,14 @@ import net.shadowfacts.cacao.view.View
* @author shadowfacts
* @param content The [View] that provides the content of this button.
* @param padding The padding between the [content] and the edges of the button.
* @param handler The handler function to invoke when this button is pressed.
*/
class Button(content: View, padding: Double = 4.0): AbstractButton<Button>(content, padding) {
constructor(content: View, padding: Double = 4.0, handler: (Button) -> Unit): this(content, padding) {
class Button(
content: View,
padding: Double = 4.0,
handler: ((Button) -> Unit)? = null
): AbstractButton<Button>(content, padding) {
init {
this.handler = handler
}
}

View File

@ -14,7 +14,7 @@ import net.shadowfacts.cacao.view.Label
*
* @author shadowfacts
* @param initialValue The initial enum value for this button.
* @param localizer A function that takes an enum value and converts into a string for the button's label.
* @param localizer A function that takes an enum value and converts into a [Text] for the button's label.
*/
class EnumButton<E: Enum<E>>(
initialValue: E,

View File

@ -10,26 +10,58 @@ import net.shadowfacts.cacao.util.KeyModifiers
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.view.View
import net.shadowfacts.phycon.mixin.client.TextFieldWidgetAccessor
/**
* An abstract text field class. Cannot be constructed directly, use for creating other text fields with more specific
* behavior. Use [TextField] for a plain text field.
*
* @author shadowfacts
* @param Impl The type of the concrete implementation of the text field. Used to allow the [handler] to receive the
* the exact type of text field.
* @param initialText The initial value of the text field.
*/
abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
initialText: String
): View() {
/**
* A function that is invoked when the text in this text field changes.
*/
var handler: ((Impl) -> Unit)? = null
/**
* Whether the text field is disabled.
* Disabled text fields cannot be interacted with.
*/
var disabled = false
/**
* Whether this text field is focused (i.e. [isFirstResponder]) and receives key events.
*/
val focused: Boolean
get() = isFirstResponder
/**
* The current text of this text field.
*/
var text: String
get() = minecraftWidget.text
set(value) {
minecraftWidget.text = value
}
/**
* The maximum length of text that this text field can hold.
*
* Defaults to the Minecraft text field's maximum length (currently 32, subject to change).
*/
var maxLength: Int
get() = (minecraftWidget as TextFieldWidgetAccessor).cacao_getMaxLength()
set(value) {
minecraftWidget.setMaxLength(value)
}
private lateinit var originInWindow: Point
private var minecraftWidget = ProxyWidget()
@ -38,6 +70,9 @@ abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
minecraftWidget.setTextPredicate { this.validate(it) }
}
/**
* A function used by subclasses to determine whether a proposed value is acceptable for this field.
*/
abstract fun validate(proposedText: String): Boolean
override fun didLayout() {
@ -118,7 +153,7 @@ abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
}
// todo: label for the TextFieldWidget?
class ProxyWidget: TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, LiteralText("")) {
private class ProxyWidget: TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, LiteralText("")) {
// AbstractButtonWidget.height is protected
fun setHeight(height: Int) {
this.height = height

View File

@ -1,10 +1,17 @@
package net.shadowfacts.cacao.view.textfield
/**
* A simple, no-frills text field. This text field accepts any value.
*
* @author shadowfacts
* @param initialText The initial value of this text field.
* @param handler A function that is invoked when the value of the text field changes.
*/
class TextField(initialText: String): AbstractTextField<TextField>(initialText) {
constructor(initialText: String, handler: (TextField) -> Unit): this(initialText) {
class TextField(
initialText: String,
handler: ((TextField) -> Unit)? = null
): AbstractTextField<TextField>(initialText) {
init {
this.handler = handler
}

View File

@ -7,7 +7,6 @@ import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.texture.Texture
@ -17,22 +16,69 @@ import net.shadowfacts.cacao.view.TextureView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.view.button.AbstractButton
import net.shadowfacts.kiwidsl.dsl
import java.lang.RuntimeException
/**
* A tab view controller is divided into two sections: a tab bar at the top, and a content view at the bottom.
*
* The tab bar contains a tab button for each of the tabs in the VC and the content view contains the view of the
* active tab's view controller.
*
* The active tab's view controller is also added as a child of the tab view controller.
*
* @author shadowfacts
* @param T The type of the tab objects this view controller uses.
* @param tabs The list of tabs in this controller.
* @param initialTab The tab that is initially selected when the controller is first created.
* @param onTabChange A function invoked immediately after the active tab has changed (and the content view has been
* updated).
*/
class TabViewController<T: TabViewController.Tab>(
val tabs: List<T>,
initalTab: T = tabs.first()
initialTab: T = tabs.first(),
val onTabChange: ((T) -> Unit)? = null
): ViewController() {
/**
* The Tab interface defines the requirements for tab objects that can be used with this view controller.
*
* This is an interface to allow for tab objects to carry additional data. A simple implementation is provided.
* @see SimpleTab
*/
interface Tab {
/**
* The view displayed on the button for this tab.
*/
val tabView: View
/**
* The tooltip displayed when the button for this tab is hovered. `null` if no tooltip should be shown.
*/
val tooltip: Text?
/**
* The view controller used as content when this tab is active. When switching tabs, the returned content VC
* may be reused or created from scratch each time.
*/
val controller: ViewController
}
var currentTab: T = initalTab
/**
* A simple [Tab] implementation that provides the minimum necessary information.
* @param tabView The view to display on the tab's button.
* @param tooltip The tooltip to display when the tab's button is hovered (or `null`, if none).
* @param controller The content view controller for this tab.
*/
class SimpleTab(
override val tabView: View,
override val tooltip: Text? = null,
override val controller: ViewController,
): Tab
/**
* The currently selected tab.
*/
var currentTab: T = initialTab
private set
private lateinit var tabButtons: List<TabButton<T>>
@ -57,7 +103,7 @@ class TabViewController<T: TabViewController.Tab>(
tabButtons = tabs.mapIndexed { index, tab ->
val btn = TabButton(tab)
btn.handler = this::selectTab
btn.handler = { selectTab(it.tab) }
if (tab == currentTab) {
btn.setSelected(true)
}
@ -91,16 +137,30 @@ class TabViewController<T: TabViewController.Tab>(
}
}
private fun selectTab(button: TabButton<T>) {
/**
* Sets the provided tab as the currently active tab for this controller. Updates the state of tab bar buttons and
* swaps the content view controller.
*
* After the tab and the content are changed, [onTabChange] is invoked.
*
* @throws RuntimeException If the provided tab was not passed in as part of the [tabs] list.
*/
fun selectTab(tab: T) {
if (!tabs.contains(tab)) {
throw RuntimeException("Cannot activate tab not in TabViewController.tabs")
}
val oldTab = currentTab
currentTab = button.tab
currentTab = tab
// todo: unselect old button
tabButtons.forEach { it.setSelected(false) }
tabButtons.forEach {
it.setSelected(it.tab === tab)
}
oldTab.controller.removeFromParent()
button.setSelected(true)
embedChild(currentTab.controller, tabVCContainer)
onTabChange?.invoke(currentTab)
// todo: setNeedsLayout
window!!.layout()
}

View File

@ -76,6 +76,9 @@ abstract class ViewController {
view = View()
}
/**
* If the view for this controller has already been loaded.
*/
val isViewLoaded: Boolean
get() = ::view.isInitialized

View File

@ -77,6 +77,18 @@ open class Window(
*/
val centerYAnchor = Variable("centerY")
/**
* The first responder of the a window is the first object that receives indirect events (e.g., keypresses).
*
* When an indirect event is received by the window, it is given to the first responder. If the first responder does
* not accept it (i.e. returns `false` from the appropriate method), the event will be passed to that responder's
* [Responder.nextResponder], and so on.
*
* The following is the order of events when setting this property:
* 1. The old first responder (if any) has [Responder.didResignFirstResponder] invoked.
* 2. The value of the field is updated.
* 3. The new value (if any) has [Responder.didBecomeFirstResponder] invoked.
*/
var firstResponder: Responder? = null
set(value) {
field?.didResignFirstResponder()
@ -183,6 +195,13 @@ open class Window(
}
}
/**
* Draw a tooltip containing the given lines at the mouse pointer location.
*
* Implementation note: the tooltip is not drawn immediately, it is done after the window is done drawing all of its
* views. This is done to prevent other views from being drawn in front of the tooltip. Additionally, more than one
* tooltip cannot be drawn in a frame as they would appear at the same position.
*/
fun drawTooltip(text: List<Text>) {
if (currentDeferredTooltip != null) {
throw RuntimeException("Deferred tooltip already registered for current frame")
@ -199,6 +218,7 @@ open class Window(
* @return Whether the mouse click was handled by a view.
*/
fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
// todo: isn't this always true?
if (point in viewController.view.frame) {
val mouseInView = Point(point.x - viewController.view.frame.left, point.y - viewController.view.frame.top)
return viewController.view.mouseClicked(mouseInView, mouseButton)

View File

@ -24,78 +24,78 @@ import net.shadowfacts.kiwidsl.dsl
class TestCacaoScreen: CacaoScreen() {
init {
val viewController = object: ViewController() {
override fun loadView() {
view = View()
}
override fun viewDidLoad() {
super.viewDidLoad()
val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.CENTER, spacing = 4.0)).apply {
backgroundColor = Color.WHITE
}
val birch = stack.addArrangedSubview(TextureView(Texture(Identifier("textures/block/birch_log_top.png"), 0, 0, 16, 16))).apply {
intrinsicContentSize = Size(50.0, 50.0)
}
val ninePatch = stack.addArrangedSubview(NinePatchView(NinePatchTexture.PANEL_BG)).apply {
intrinsicContentSize = Size(75.0, 100.0)
}
val red = stack.addArrangedSubview(View()).apply {
intrinsicContentSize = Size(50.0, 50.0)
backgroundColor = Color.RED
}
val label = Label(LiteralText("Test"), wrappingMode = Label.WrappingMode.NO_WRAP).apply {
// textColor = Color.BLACK
}
// stack.addArrangedSubview(label)
val button = red.addSubview(Button(label))
val field = TextField("Test") {
println("new value: ${it.text}")
}
stack.addArrangedSubview(field)
view.solver.dsl {
stack.topAnchor equalTo 0
stack.centerXAnchor equalTo window!!.centerXAnchor
stack.widthAnchor equalTo 100
button.centerXAnchor equalTo red.centerXAnchor
button.centerYAnchor equalTo red.centerYAnchor
// label.heightAnchor equalTo 9
button.heightAnchor equalTo 20
field.widthAnchor equalTo stack.widthAnchor
field.heightAnchor equalTo 20
}
}
}
addWindow(Window(viewController))
// val viewController = object: ViewController() {
// override fun loadView() {
// view = View()
// }
//
// override fun viewDidLoad() {
// super.viewDidLoad()
//
// val tabs = arrayOf(
// Tab(Label("A"), AViewController()),
// Tab(Label("B"), BViewController()),
// )
// val tabVC = TabViewController(tabs)
// embedChild(tabVC, pinEdges = false)
// val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.CENTER, spacing = 4.0)).apply {
// backgroundColor = Color.WHITE
// }
// val birch = stack.addArrangedSubview(TextureView(Texture(Identifier("textures/block/birch_log_top.png"), 0, 0, 16, 16))).apply {
// intrinsicContentSize = Size(50.0, 50.0)
// }
// val ninePatch = stack.addArrangedSubview(NinePatchView(NinePatchTexture.PANEL_BG)).apply {
// intrinsicContentSize = Size(75.0, 100.0)
// }
// val red = stack.addArrangedSubview(View()).apply {
// intrinsicContentSize = Size(50.0, 50.0)
// backgroundColor = Color.RED
// }
//
// val label = Label(LiteralText("Test"), wrappingMode = Label.WrappingMode.NO_WRAP).apply {
//// textColor = Color.BLACK
// }
//// stack.addArrangedSubview(label)
// val button = red.addSubview(Button(label))
//
// val field = TextField("Test") {
// println("new value: ${it.text}")
// }
// stack.addArrangedSubview(field)
//
// view.solver.dsl {
// tabVC.view.centerXAnchor equalTo view.centerXAnchor
// tabVC.view.centerYAnchor equalTo view.centerYAnchor
// tabVC.view.widthAnchor equalTo 200
// tabVC.view.heightAnchor equalTo 150
// stack.topAnchor equalTo 0
// stack.centerXAnchor equalTo window!!.centerXAnchor
// stack.widthAnchor equalTo 100
//
//
// button.centerXAnchor equalTo red.centerXAnchor
// button.centerYAnchor equalTo red.centerYAnchor
//// label.heightAnchor equalTo 9
// button.heightAnchor equalTo 20
//
// field.widthAnchor equalTo stack.widthAnchor
// field.heightAnchor equalTo 20
// }
//
// }
// }
// addWindow(Window(viewController))
val viewController = object: ViewController() {
override fun viewDidLoad() {
super.viewDidLoad()
val tabs = listOf(
Tab(Label("A"), AViewController(), LiteralText("Tab A")),
Tab(Label("B"), BViewController(), LiteralText("Tab B")),
)
val tabVC = TabViewController(tabs)
embedChild(tabVC, pinEdges = false)
view.solver.dsl {
tabVC.view.centerXAnchor equalTo view.centerXAnchor
tabVC.view.centerYAnchor equalTo view.centerYAnchor
tabVC.view.widthAnchor equalTo 200
tabVC.view.heightAnchor equalTo 150
}
}
}
addWindow(Window(viewController))
}
data class Tab(

View File

@ -1,21 +1,17 @@
package net.shadowfacts.phycon.screen.console
import net.minecraft.text.Text
import net.minecraft.text.TranslatableText
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.TextureView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.viewcontroller.TabViewController
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.cacao.window.Window
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlockEntity
import net.shadowfacts.phycon.network.component.ActivationController
@ -28,18 +24,18 @@ class DeviceConsoleScreen(
val device: DeviceBlockEntity,
): CacaoScreen(TranslatableText("item.phycon.console")) {
private val tabController: TabViewController<Tab>
private val tabController: TabViewController<TabViewController.SimpleTab>
init {
val tabs = mutableListOf(
Tab(
TabViewController.SimpleTab(
Label("IP").apply { textColor = Color.TEXT },
TranslatableText("gui.phycon.console.details"),
DeviceDetailsViewController(device)
)
)
if (device is ActivationController.ActivatableDevice) {
tabs.add(Tab(
tabs.add(TabViewController.SimpleTab(
TextureView(Texture(Identifier("textures/item/ender_pearl.png"), 0, 0, 16, 16)).apply {
intrinsicContentSize = Size(16.0, 16.0)
},
@ -48,7 +44,7 @@ class DeviceConsoleScreen(
))
}
if (device is RedstoneControllerBlockEntity) {
tabs.add(Tab(
tabs.add(TabViewController.SimpleTab(
TextureView(Texture(Identifier("textures/block/redstone_torch.png"), 0, 0, 16, 16)).apply {
intrinsicContentSize = Size(16.0, 16.0)
},
@ -85,10 +81,4 @@ class DeviceConsoleScreen(
return super.keyPressed(keyCode, scanCode, modifiers)
}
data class Tab(
override val tabView: View,
override val tooltip: Text?,
override val controller: ViewController,
): TabViewController.Tab
}

View File

@ -3,9 +3,10 @@
"package": "net.shadowfacts.phycon.mixin.client",
"compatibilityLevel": "JAVA_8",
"mixins": [
"MixinHandledScreen"
"MixinHandledScreen",
"TextFieldWidgetAccessor"
],
"injectors": {
"defaultRequire": 1
}
}
}