using System;
using System.Collections.Generic;
using UnityEngine;
namespace MyInfoMate.Vr.Menu
{
///
/// Une pile de pages qu'on feuillette — la vue commune du Slider, de la
/// Map et de l'Event, item E8 du lot XR-4.
///
/// Pourquoi une seule vue pour trois types. Une galerie de photos, une liste de
/// points d'intérêt et un programme de la journée sont trois contenus différents dans
/// le CMS, mais dans le casque ce sont les mêmes gestes : une chose à la fois, grande,
/// devant soi, et de quoi passer à la suivante. Les trois renderers ne se distinguent
/// que par ce qu'ils mettent dans les pages — c'est le rôle de
/// .
///
/// Les pages s'ajoutent au fil du chargement, et la première s'affiche dès qu'elle
/// arrive : sur vingt photos, attendre la vingtième ferait passer le chargement pour
/// une panne.
///
public class PagedView : MonoBehaviour
{
const string CommandPrevious = "__previous";
const string CommandNext = "__next";
const string CommandBack = "__back";
const float DistanceMeters = 2.5f;
const float ImageWidthMeters = 1.6f;
const float ImageHeightMeters = 0.9f;
const float CenterHeight = 1.5f;
public class Page
{
/// Le titre de la page. Toujours présent.
public string Heading;
/// Description, horaire, adresse… Peut être vide.
public string Body;
/// Nulle sur une page sans média — le texte tient tout seul.
public Texture2D Image;
}
readonly List _pages = new List();
Material _imageMaterial;
Transform _imageQuad;
Ui.Label _text;
MenuItemPanel _previous;
MenuItemPanel _next;
int _index;
/// Le visiteur veut revenir au menu.
public event Action Closed;
///
/// N'importe quelle action du visiteur. Sert au mode borne : feuilleter est une
/// activité, même si la tête ne bouge presque pas.
///
public event Action Interacted;
public int PageCount => _pages.Count;
public static PagedView Create(Transform head)
{
var root = new GameObject("PagedView");
var view = root.AddComponent();
if (head != null)
{
var forward = head.forward;
forward.y = 0f;
if (forward.sqrMagnitude < 0.01f) forward = Vector3.forward;
root.transform.position = new Vector3(head.position.x, 0f, head.position.z);
root.transform.rotation = Quaternion.LookRotation(forward.normalized);
}
var selector = root.AddComponent();
selector.Configure(head);
selector.Selected += view.OnSelected;
view.Build();
return view;
}
void Build()
{
var quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
quad.name = "Image";
quad.transform.SetParent(transform, false);
quad.transform.localPosition = new Vector3(0f, CenterHeight, DistanceMeters);
quad.transform.localScale = new Vector3(ImageWidthMeters, ImageHeightMeters, 1f);
// L'image n'est pas une cible de visée : ce sont les trois commandes qui le
// sont, et un collider plein cadre les masquerait.
Destroy(quad.GetComponent());
_imageMaterial = new Material(Shader.Find("Universal Render Pipeline/Unlit"));
_imageMaterial.SetFloat("_Cull", 0f);
_imageMaterial.color = new Color(0.08f, 0.10f, 0.13f, 1f);
quad.GetComponent().material = _imageMaterial;
_imageQuad = quad.transform;
_text = Ui.Label.Create(transform, "Text", Ui.Label.Anchor.TopCenter, 0.048f, Color.white);
_text.transform.localPosition = new Vector3(
0f, CenterHeight - ImageHeightMeters * 0.62f, DistanceMeters);
_previous = Command(CommandPrevious, "◀", -ImageWidthMeters * 0.62f, CenterHeight);
_next = Command(CommandNext, "▶", ImageWidthMeters * 0.62f, CenterHeight);
Command(CommandBack, "Retour au menu", 0f, CenterHeight - ImageHeightMeters * 0.95f);
UpdateCommands();
}
MenuItemPanel Command(string id, string label, float x, float y)
{
var panel = MenuItemPanel.Create(id, label, transform);
panel.transform.localPosition = new Vector3(x, y, DistanceMeters - 0.02f);
panel.transform.localScale = Vector3.one * 0.55f;
return panel;
}
public void AddPage(Page page)
{
_pages.Add(page);
if (_pages.Count == 1) Display(0);
else UpdateCommands();
}
/// Quand il n'y a rien à montrer, le dire — pas laisser un cadre vide.
public void ShowEmpty(string message)
{
_text.Text = Wrap(message, 46);
UpdateCommands();
}
void Display(int index)
{
if (index < 0 || index >= _pages.Count) return;
_index = index;
var page = _pages[index];
if (page.Image != null)
{
_imageMaterial.mainTexture = page.Image;
_imageMaterial.color = Color.white;
_imageQuad.gameObject.SetActive(true);
}
else
{
// Sans image, le cadre reste — mais sombre : il porte le texte, qui
// devient alors le contenu principal.
_imageMaterial.mainTexture = null;
_imageMaterial.color = new Color(0.08f, 0.10f, 0.13f, 1f);
}
var counter = _pages.Count > 1 ? $"\n{index + 1} / {_pages.Count}" : "";
var body = string.IsNullOrEmpty(page.Body) ? "" : $"\n{page.Body}";
_text.Text = Wrap(page.Heading, 46) + Wrap(body, 46) + counter;
UpdateCommands();
}
///
/// Les flèches disparaissent aux extrémités plutôt que de ne rien faire : un
/// bouton présent qui ne réagit pas se lit comme une panne.
///
void UpdateCommands()
{
if (_previous != null) _previous.gameObject.SetActive(_index > 0);
if (_next != null) _next.gameObject.SetActive(_index < _pages.Count - 1);
}
void OnSelected(IAimTarget target)
{
if (!(target is MenuItemPanel panel)) return;
Interacted?.Invoke();
switch (panel.Id)
{
case CommandPrevious: Display(_index - 1); break;
case CommandNext: Display(_index + 1); break;
case CommandBack: Closed?.Invoke(); break;
}
}
static string Wrap(string text, int columns)
{
if (string.IsNullOrEmpty(text)) return "";
var wrapped = new System.Text.StringBuilder();
// Les retours à la ligne du contenu sont respectés : c'est la mise en forme
// voulue par le gestionnaire, on ne recolle pas ses paragraphes.
foreach (var paragraph in text.Split('\n'))
{
if (wrapped.Length > 0) wrapped.Append('\n');
var line = 0;
foreach (var word in paragraph.Split(' '))
{
if (line + word.Length > columns)
{
wrapped.Append('\n');
line = 0;
}
else if (line > 0)
{
wrapped.Append(' ');
line++;
}
wrapped.Append(word);
line += word.Length;
}
}
return wrapped.ToString();
}
}
}