Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions .github/workflows/testRunner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,48 @@ on: [pull_request]

jobs:
test:
name: Run Tests in PlayMode and EditMode ✨
permissions:
checks: write
name: Run Tests in PlayMode and EditMode ✨
runs-on: ubuntu-latest
steps:
timeout-minutes: 45

steps:
# Checkout
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: true

# Cache
- uses: actions/cache@v3
# Cache the Library folder for faster runs
- name: Cache Library
uses: actions/cache@v3
with:
path: Library
key: Library-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
restore-keys: |
Library-

# Test
- name: Run tests
uses: game-ci/unity-test-runner@v4

# Run Unity tests (both EditMode + PlayMode by default)
- name: Run tests (Ubuntu + Xvfb)
id: tests
uses: game-ci/unity-test-runner@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
# Linux editor image for 6000.0.39f1; "base" is fine for tests
customImage: unityci/editor:ubuntu-6000.0.39f1-base-3.1.0
# Stabilize headless graphics & stream logs to STDOUT
customParameters: "-force-glcore -logFile -"
testMode: All


# Always upload test artifacts (XML, logs, etc.)
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: Unity Test Results
path: ${{ steps.tests.outputs.artifactsPath }}
221 changes: 205 additions & 16 deletions Assets/Samples/SampleMenu/SampleMenuScript/InfoWindowPresenter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

Expand All @@ -8,51 +10,238 @@ public class InfoWindowPresenter : MonoBehaviour
private VisualElement informationView;
private VisualElement welcomeView;
private TextElement informationText;
private TextElement inforHeader;

private VisualElement upCard;
private VisualElement confirmCard;
private VisualElement downCard;


[SerializeField]
private StyleSheet informationStyleSheet;

public List<InformationWindowText> ButtonData;
public ScrollView ScrollButtonView;

private int selectedIndex = 3;

private const int DefaultSelectedIndex = 3;

private Coroutine scrollCoroutine;

[SerializeField]
private Texture2D backgroundMapTexture;

private VisualElement informationImageWindow;

private const string PulseClass = "pulse-color";
private Color pulseColor = new Color(0.98f, 0.79f, 0.04f, 0.5f); // yellow w/ alpha
private Color originalColor = new Color(0.133f, 0.133f, 0.133f);


private void Start()
{
VisualElement root = GetComponent<UIDocument>().rootVisualElement;
root.styleSheets.Add(informationStyleSheet);
ScrollButtonView = root.Q<ScrollView>("ButtonScroll");
informationView = root.Q<VisualElement>("InformationView");
informationImageWindow = informationView.Q<VisualElement>("InformationContainer");
upCard = informationView.Q<VisualElement>("UpCard");
confirmCard = informationView.Q<VisualElement>("ConfirmCard");
downCard = informationView.Q<VisualElement>("DownCard");
welcomeView = root.Q<VisualElement>("WelcomeView");
informationText = root.Q<TextElement>("InformationText");
inforHeader = root.Q<TextElement>("InfoHeader");

ScrollButtonView.Clear();

// Initialize buttons
foreach (var data in ButtonData)
{
var button = new Button();
button.text = data.ButtonName;
button.AddToClassList("button");
ScrollButtonView.Add(button);
}

HideInformationView();
}

public void ShowInformationView()
{
informationView.style.display = DisplayStyle.Flex;
welcomeView.style.display = DisplayStyle.None;
SetInformationText("To read information on possible use cases please match one of the gestures on the left", " ");
// Highlight new button
var centerButton = (Button)ScrollButtonView.ElementAt(selectedIndex);
HighlightButton(centerButton);

// Center the selected button in the scroll view
ScrollToCenter(centerButton);

}

public void HideInformationView()
{
SetInformationText(0);
// Reset selection by taking the total cound and current index and math it to get back to 3
MoveSelection(DefaultSelectedIndex - selectedIndex + 1);

SetInformationText("To read information on possible use cases please match one of the gestures on the left", " ");
informationView.style.display = DisplayStyle.None;
welcomeView.style.display = DisplayStyle.Flex;
selectedIndex = DefaultSelectedIndex;
}

public void HighlightButton(Button button)
{
// Remove highlight + glow from all buttons
foreach (var child in ScrollButtonView.Children())
{
if (child is Button b)
{
b.RemoveFromClassList("button-hover");
b.RemoveFromClassList("button-glow");
}
}

// Add highlight + glow to the current one
button.AddToClassList("button-hover");
button.AddToClassList("button-glow");
}

public void SetInformationText(int index = 0)
public void MoveSelection(int direction)
{
if (informationView.style.display == DisplayStyle.Flex)
if (direction >= 0)
{
PlayPulse(downCard);
}
else
{
PlayPulse(upCard);
}

// Wrap index instead of clamping
selectedIndex += direction;
if (selectedIndex < 0) selectedIndex = ScrollButtonView.childCount - 1;
else if (selectedIndex >= ScrollButtonView.childCount) selectedIndex = 0;

// Highlight new button
var newButton = (Button)ScrollButtonView.ElementAt(selectedIndex);
HighlightButton(newButton);

// Center the selected button in the scroll view
SmoothScrollToCenter(newButton);

}

public void ConfirmSelection()
{
// Clear the background image for any other button
informationImageWindow.style.backgroundImage = null;

if (informationView.style.display == DisplayStyle.None)
{
switch (index)
ShowInformationView();
return;
}

PlayPulse(confirmCard);

if (selectedIndex >= 0 && selectedIndex < ButtonData.Count)
{
// if info text is "Return" hide the information view
if (ButtonData[selectedIndex].InfoText == "Return")
{
HideInformationView();
return;
}
else if (ButtonData[selectedIndex].InfoText == "MAP")
{
case 0:
informationText.text = "To read information on possible use cases please match one of the gestures on the left";
break;
case 1:
informationText.text = "Our Unity plugin enables intuitive hand gesture recognition, making it ideal for modern kiosk technologies. It allows users to interact with digital content in a completely touchless manner, which is especially valuable in public environments where hygiene is a concern. This can be applied to interactive information kiosks in museums, airports, or retail settings, where users can browse content, navigate menus, or trigger actions using simple gestures. Additionally, the plugin supports integration with projector-based or holographic displays, enabling users to control immersive content in mid-air without physical contact, enhancing accessibility and creating futuristic, engaging user experiences.";
break;
case 2:
informationText.text = "Our Unity plugin opens up powerful possibilities in the realm of sign language recognition and learning. By accurately detecting and interpreting hand gestures, it can be used to create real-time translation tools that convert sign language into text or speech, improving communication accessibility for Deaf and hard-of-hearing individuals. Additionally, it can support immersive educational applications, allowing users to learn sign language through interactive lessons, visual feedback, and guided practice in a virtual environment. This makes it a valuable tool for both assistive technology and inclusive education.";
break;
default:
informationText.text = "To read information on possible use cases please match one of the gestures on the left";
break;
if (backgroundMapTexture != null)
{
informationImageWindow.style.backgroundImage = new StyleBackground(backgroundMapTexture);
SetInformationText("", " ");
return;
}
}

SetInformationText(ButtonData[selectedIndex].InfoText, ButtonData[selectedIndex].ButtonName);

var selectedButton = (Button)ScrollButtonView.ElementAt(selectedIndex);
}
}


public void SetInformationText(string text, string header)
{
if (informationView.style.display == DisplayStyle.Flex)
{
informationText.text = text;
inforHeader.text = header;
}
}

public void PlayPulse(VisualElement card)
{
// Set pulse color
card.style.backgroundColor = pulseColor;

// Schedule reset back to original
card.schedule.Execute(() =>
{
card.style.backgroundColor = originalColor;
}).StartingIn(300); // 300ms
}

// function to scroll the selected button to the center of the scroll view
private void ScrollToCenter(Button button)
{
if (scrollCoroutine != null)
{
StopCoroutine(scrollCoroutine);
}
scrollCoroutine = StartCoroutine(ScrollToCenterCoroutine(button));
}

private void SmoothScrollToCenter(Button button)
{
if (scrollCoroutine != null)
{
StopCoroutine(scrollCoroutine);
}
scrollCoroutine = StartCoroutine(SmoothScrollToCenterCoroutine(button));
}

private IEnumerator ScrollToCenterCoroutine(Button button)
{
// Wait for the next frame to ensure the button is fully rendered
yield return null;
// Calculate the position of the button in the scroll view
float buttonPosition = button.resolvedStyle.top + button.resolvedStyle.height / 2;
float scrollViewHeight = ScrollButtonView.resolvedStyle.height;
// Calculate the new scroll position to center the button
float newScrollPosition = buttonPosition - scrollViewHeight / 2;
// Smoothly scroll to the new position
ScrollButtonView.scrollOffset = new Vector2(0, newScrollPosition);
}

// coroutine to smoothly scroll the scroll view to the center of the selected button
private IEnumerator SmoothScrollToCenterCoroutine(Button button)
{
float targetPosition = button.resolvedStyle.top + button.resolvedStyle.height / 2;
float scrollViewHeight = ScrollButtonView.resolvedStyle.height;
float startPosition = ScrollButtonView.scrollOffset.y;
float newScrollPosition = targetPosition - scrollViewHeight / 2;
float duration = 0.25f; // Duration of the scroll
float elapsedTime = 0f;
while (elapsedTime < duration)
{
elapsedTime += Time.deltaTime;
float t = Mathf.Clamp01(elapsedTime / duration);
ScrollButtonView.scrollOffset = new Vector2(0, Mathf.Lerp(startPosition, newScrollPosition, t));
yield return null;
}
ScrollButtonView.scrollOffset = new Vector2(0, newScrollPosition);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using UnityEngine;

[CreateAssetMenu(fileName = "InformationWindowText", menuName = "Scriptable Objects/InformationWindowText")]
public class InformationWindowText : ScriptableObject
{
// variable for name
[Tooltip("The name of the button")]
public string ButtonName;

// variable for text
[Tooltip("The text to display in the information window")]
public string InfoText;

}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Assets/Samples/SampleMenu/SampleMenuScriptableObjects.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions Assets/Samples/SampleMenu/SampleMenuScriptableObjects/Art.asset

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading