/** * Un menù che si apre dal comando che lo contiene: il «+», i tre puntini di * una sezione, il banner della foto. * * Sull'iPhone è il menù di SwiftUI (`Menu` di @expo/ui), lo stesso di Note e * delle app di Apple: nasce dal comando toccato con l'animazione di vetro di * iOS 26, coi simboli di Apple accanto alle voci e la voce che elimina in * rosso. Paolo l'ha chiesto il 30/09 al posto del foglio di sistema che saliva * dal basso. Su Android è il menù a tendina di Material (`DropdownMenu`), che * si apre anch'esso sopra il comando, coi nostri segni. * * `children` è solo il disegno del comando, senza Pressable: il tocco lo * prende il menù. Con `spento` il comando si vede ma non apre niente (la foto * che sta ancora comprimendo). * * Quale dei due lo decide tipoMenu() in logica-menu.ts. */ import type { ReactElement } from 'react'; import { useRef, useState } from 'react'; import { Platform, Pressable, useWindowDimensions, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Button as BottoneSwift, Host as HostSwift, Menu as MenuSwift, RNHostView as RNHostSwift, Section as SezioneSwift, Image as ImmagineSwift, Text as TestoSwift, } from '@expo/ui/swift-ui'; import { Column as ColonnaCompose, DropdownMenu, DropdownMenuItem, Host as HostCompose, Icon, RNHostView as RNHostCompose, Text as TestoCompose, } from '@expo/ui/jetpack-compose'; import { altezzaTendina, scorrimentoPerTendina, tipoMenu } from '../logica-menu.ts'; import type { ChiaveSegno } from '../segni.ts'; import { useColori, useSchema } from '../tema.ts'; import { FILE_SEGNI } from './Segno.tsx'; export interface VoceMenu { etichetta: string; /** Il nostro segno, accanto alla voce su Android. */ segno: ChiaveSegno; /** Il simbolo di Apple accanto alla voce sull'iPhone (SF Symbols). */ simbolo: string; /** Una riga sotto l'etichetta, per le voci che vanno spiegate. */ spiega?: string; pericolo?: boolean; azione: () => void; } export interface PropsMenuNativo { voci: VoceMenu[]; /** Una riga sopra le voci: il nome della sezione, per esempio. */ titolo?: string; /** Per chi usa VoiceOver o TalkBack: cosa fa il comando. */ etichetta: string; spento?: boolean; /** * Su Android: fa scorrere la pagina di `dy` punti. Se sotto il comando non * c'è posto per la tendina, prima si scorre e poi la si apre, così nasce * sotto il comando e non sopra (scorrimentoPerTendina). */ scorri?: (dy: number) => void; children: ReactElement; } export function MenuNativo({ voci, titolo, etichetta, spento = false, scorri, children }: PropsMenuNativo) { const colori = useColori(); const schema = useSchema(); const [aperto, setAperto] = useState(false); const finestra = useWindowDimensions(); const sotto = useSafeAreaInsets().bottom; const comando = useRef(null); // Una voce parte una volta sola: la tendina di Material si chiude con // un'animazione, e un secondo tocco rapido la rilancerebbe (la fotocamera // aperta due volte). const scelto = useRef(false); const apri = () => { scelto.current = false; if (scorri === undefined || comando.current === null) return setAperto(true); comando.current.measureInWindow((_x, y, _l, h) => { // Material tiene la tendina ad almeno 48 dp dal bordo della finestra // (MenuVerticalMargin), e in prova la apriva sopra anche con qualche // decina di punti in più: il margine è largo apposta. const dy = scorrimentoPerTendina(y + h, altezzaTendina(voci), finestra.height, sotto + 120); if (dy === 0) return setAperto(true); scorri(dy); // Si apre quando lo scorrimento è finito: aperta prima, Material la // metterebbe dove il comando era, non dove è arrivato. setTimeout(() => setAperto(true), 320); }); }; if (spento) { return ( {children} ); } if (tipoMenu(Platform.OS) === 'swiftui') { // Con una spiegazione, SwiftUI mette il secondo testo sotto il primo, più // piccolo, come «Suggerisci risultati» nel menù di Note. const bottoni = voci.map((v) => v.spiega === undefined ? ( ) : ( {v.etichetta} {v.spiega} ), ); return ( {children} } > {titolo !== undefined ? {bottoni} : bottoni} ); } return ( setAperto(false)} color={colori.superficie}> (pressed ? { opacity: 0.6 } : null)} > {children} {voci.map((v) => { const colore = v.pericolo === true ? colori.errore : colori.testo; return ( { if (scelto.current) return; scelto.current = true; setAperto(false); v.azione(); }} > {v.spiega === undefined ? ( {v.etichetta} ) : ( {v.etichetta} {v.spiega} )} ); })} ); }