22import React , { useEffect , useRef } from 'react' ;
33
44const LIGHT_COLORS = {
5- new : '#46bdf0' ,
6- active : '#42d392' ,
7- passive : '#9aa6b2' ,
8- selected : '#ffb347' ,
9- default : '#6b93ff' ,
10- text : '#0c1622' ,
11- glow : 'rgba(74, 157, 255, 0.25)' ,
12- bg0 : '#f7fbff' ,
13- bg1 : '#edf3ff' ,
14- grid : 'rgba(100, 125, 160, 0.12)' ,
15- link : 'rgba(70, 110, 200, 0.35)' ,
16- tooltipBg : 'rgba(15, 24, 44, 0.9)' ,
17- tooltipStroke : 'rgba(255,255,255,0.15)' ,
18- tooltipText : '#f5f7ff' ,
19- subsumed : '#ff6b6b' ,
20- highlight : '#ffe75a' ,
5+ new : '#ff9fb2' ,
6+ active : '#b62929' ,
7+ passive : '#8a8a8a' ,
8+ selected : '#f97316' ,
9+ default : '#1a1a1a' ,
10+ text : '#0b0b0b' ,
11+ glow : 'rgba(140, 20, 20, 0.22)' ,
12+ bg0 : '#fbfbfb' ,
13+ bg1 : '#f2f2f2' ,
14+ grid : 'rgba(20, 20, 20, 0.08)' ,
15+ link : 'rgba(60, 60, 60, 0.35)' ,
16+ tooltipBg : 'rgba(18, 12, 12, 0.9)' ,
17+ tooltipStroke : 'rgba(255,255,255,0.12)' ,
18+ tooltipText : '#f8f4f4' ,
19+ tooltipOp : '#ff6b6b' ,
20+ tooltipVar : '#ffd166' ,
21+ tooltipNum : '#f59e0b' ,
22+ tooltipBool : '#ffffff' ,
23+ tooltipSym : '#f8f4f4' ,
24+ tooltipPunct : '#c0b0b0' ,
25+ subsumed : '#ff4d4d' ,
26+ highlight : '#fff59d' ,
2127} ;
2228
2329const DARK_COLORS = {
24- new : '#5bc5ff ' ,
25- active : '#41e6a4 ' ,
26- passive : '#7e8a96 ' ,
27- selected : '#ffb347 ' ,
28- default : '#7aa2f7 ' ,
29- text : '#eef2ff ' ,
30- glow : 'rgba(94, 197, 255 , 0.25 )' ,
31- bg0 : '#0b1020 ' ,
32- bg1 : '#121a2f ' ,
33- grid : 'rgba(120, 150, 200 , 0.12 )' ,
34- link : 'rgba(110, 170, 255 , 0.35 )' ,
35- tooltipBg : 'rgba(20, 27, 44 , 0.92 )' ,
30+ new : '#ff3b5c ' ,
31+ active : '#9a1b1b ' ,
32+ passive : '#5f5f5f ' ,
33+ selected : '#f59e0b ' ,
34+ default : '#e0e0e0 ' ,
35+ text : '#f4f1f1 ' ,
36+ glow : 'rgba(255, 40, 40 , 0.22 )' ,
37+ bg0 : '#0a0a0a ' ,
38+ bg1 : '#101010 ' ,
39+ grid : 'rgba(255, 255, 255 , 0.08 )' ,
40+ link : 'rgba(200, 200, 200 , 0.28 )' ,
41+ tooltipBg : 'rgba(18, 10, 10 , 0.95 )' ,
3642 tooltipStroke : 'rgba(255,255,255,0.2)' ,
37- tooltipText : '#e6edf7' ,
38- subsumed : '#ff6b6b' ,
39- highlight : '#ffe75a' ,
43+ tooltipText : '#f3eaea' ,
44+ tooltipOp : '#ff8080' ,
45+ tooltipVar : '#ffd78a' ,
46+ tooltipNum : '#fbbf24' ,
47+ tooltipBool : '#ffffff' ,
48+ tooltipSym : '#f3eaea' ,
49+ tooltipPunct : '#b9aaaa' ,
50+ subsumed : '#ff4d4d' ,
51+ highlight : '#fff59d' ,
4052} ;
4153
4254function getPalette ( theme ) {
@@ -72,6 +84,7 @@ export default function ProofSearchCanvas({
7284 const timeRef = useRef ( 0 ) ;
7385 const themeRef = useRef ( 'light' ) ;
7486 const tapRef = useRef ( { time : 0 , id : null , x : 0 , y : 0 } ) ;
87+ const hoverFadeRef = useRef ( { id : null , alpha : 0 , lastTs : 0 } ) ;
7588
7689 useEffect ( ( ) => {
7790 nodesRef . current . clear ( ) ;
@@ -318,8 +331,28 @@ export default function ProofSearchCanvas({
318331
319332 // Directed edges from parents to children
320333 const edgeList = edgesRef . current ;
321- const hoverId = hoverRef . current ?. node ?. id ;
322- const highlight = hoverId ? computeHighlightSets ( hoverId , edgeList ) : null ;
334+ const hoverId = hoverRef . current ?. node ?. id || null ;
335+ const fade = hoverFadeRef . current ;
336+ if ( hoverId && hoverId !== fade . id ) {
337+ fade . id = hoverId ;
338+ fade . alpha = 0 ;
339+ fade . lastTs = ts ;
340+ }
341+ if ( fade . lastTs == null ) fade . lastTs = ts ;
342+ const dt = Math . min ( 1 , ( ts - fade . lastTs ) / 400 ) ;
343+ const target = hoverId ? 1 : 0 ;
344+ if ( target > ( fade . alpha || 0 ) ) {
345+ fade . alpha = Math . min ( 1 , ( fade . alpha || 0 ) + dt ) ;
346+ } else if ( target < ( fade . alpha || 0 ) ) {
347+ fade . alpha = Math . max ( 0 , ( fade . alpha || 0 ) - dt ) ;
348+ }
349+ fade . lastTs = ts ;
350+ if ( ! hoverId && fade . alpha === 0 ) {
351+ fade . id = null ;
352+ }
353+ const highlightId = fade . id ;
354+ const highlightAlpha = fade . alpha || 0 ;
355+ const highlight = highlightId ? computeHighlightSets ( highlightId , edgeList ) : null ;
323356 if ( edgeList && edgeList . length ) {
324357 ctx . strokeStyle = palette . link ;
325358 ctx . lineWidth = 1.4 ;
@@ -332,13 +365,13 @@ export default function ProofSearchCanvas({
332365 if ( ! from || ! to ) continue ;
333366 drawArrow ( ctx , from . x , from . y , to . x , to . y ) ;
334367 }
335- if ( highlight ?. edgeKeys ?. size ) {
368+ if ( highlight ?. edgeKeys ?. size && highlightAlpha > 0 ) {
336369 ctx . save ( ) ;
337- ctx . strokeStyle = palette . highlight ;
370+ ctx . strokeStyle = '#c1121f' ;
338371 ctx . lineWidth = 3 ;
339- ctx . globalAlpha = 0.5 ;
372+ ctx . globalAlpha = 0.55 * highlightAlpha ;
340373 ctx . shadowColor = palette . highlight ;
341- ctx . shadowBlur = 16 ;
374+ ctx . shadowBlur = 14 * highlightAlpha ;
342375 ctx . shadowOffsetX = 0 ;
343376 ctx . shadowOffsetY = 0 ;
344377 for ( let i = 0 ; i < edgeList . length ; i += step ) {
@@ -351,7 +384,7 @@ export default function ProofSearchCanvas({
351384 }
352385 ctx . strokeStyle = palette . link ;
353386 ctx . lineWidth = 1.6 ;
354- ctx . globalAlpha = 0.95 ;
387+ ctx . globalAlpha = 0.95 * highlightAlpha ;
355388 ctx . shadowBlur = 0 ;
356389 for ( let i = 0 ; i < edgeList . length ; i += step ) {
357390 const edge = edgeList [ i ] ;
@@ -386,12 +419,12 @@ export default function ProofSearchCanvas({
386419 }
387420 }
388421
389- if ( highlight ?. nodes ?. has ( String ( node . id ) ) ) {
422+ if ( highlight ?. nodes ?. has ( String ( node . id ) ) && highlightAlpha > 0 ) {
390423 const inner = radius * pulse + 2 ;
391424 const outer = radius * pulse + 20 ;
392425 const grad = ctx . createRadialGradient ( node . x , node . y , inner , node . x , node . y , outer ) ;
393- grad . addColorStop ( 0 , rgbaFrom ( palette . highlight , 0.75 ) ) ;
394- grad . addColorStop ( 0.5 , rgbaFrom ( palette . highlight , 0.35 ) ) ;
426+ grad . addColorStop ( 0 , rgbaFrom ( palette . highlight , 0.75 * highlightAlpha ) ) ;
427+ grad . addColorStop ( 0.5 , rgbaFrom ( palette . highlight , 0.35 * highlightAlpha ) ) ;
395428 grad . addColorStop ( 1 , rgbaFrom ( palette . highlight , 0 ) ) ;
396429 ctx . globalAlpha = 1 ;
397430 ctx . fillStyle = grad ;
@@ -474,11 +507,20 @@ export default function ProofSearchCanvas({
474507 const screenX = node . x * zoom + panX ;
475508 const screenY = node . y * zoom + panY ;
476509 ctx . setTransform ( dpr , 0 , 0 , dpr , 0 , 0 ) ;
477- const lines = wrapText ( ctx , node . text || '' , 220 ) ;
478510 const pad = 10 ;
479- ctx . font = '12px system-ui, sans-serif' ;
511+ const maxWidth = Math . min ( 340 , w - 20 ) ;
512+ ctx . font = '12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' ;
480513 const lineHeight = 16 ;
481- const boxWidth = 240 ;
514+ const tokens = tokenizeClause ( node . text || '' ) ;
515+ const lines = wrapTokens ( ctx , tokens , maxWidth - pad * 2 ) ;
516+ const lineWidths = lines . map ( ( line ) =>
517+ line . reduce ( ( sum , tok ) => sum + ctx . measureText ( tok . text ) . width , 0 )
518+ ) ;
519+ const contentWidth = Math . min (
520+ maxWidth ,
521+ Math . max ( 140 , ...lineWidths . map ( ( w ) => w + pad * 2 ) )
522+ ) ;
523+ const boxWidth = Math . min ( maxWidth , contentWidth ) ;
482524 const boxHeight = pad * 2 + lineHeight * Math . max ( 1 , lines . length ) ;
483525 const boxX = clamp ( screenX + 20 , 10 , w - boxWidth - 10 ) ;
484526 const boxY = clamp ( screenY + 20 , 10 , h - boxHeight - 10 ) ;
@@ -491,11 +533,17 @@ export default function ProofSearchCanvas({
491533 ctx . fill ( ) ;
492534 ctx . stroke ( ) ;
493535
494- ctx . fillStyle = palette . tooltipText ;
495536 ctx . textAlign = 'left' ;
496537 ctx . textBaseline = 'top' ;
497538 lines . forEach ( ( line , idx ) => {
498- ctx . fillText ( line , boxX + pad , boxY + pad + idx * lineHeight ) ;
539+ let cursorX = boxX + pad ;
540+ const y = boxY + pad + idx * lineHeight ;
541+ line . forEach ( ( tok ) => {
542+ if ( ! tok . text ) return ;
543+ ctx . fillStyle = tokenColor ( tok . type , palette ) ;
544+ ctx . fillText ( tok . text , cursorX , y ) ;
545+ cursorX += ctx . measureText ( tok . text ) . width ;
546+ } ) ;
499547 } ) ;
500548 }
501549
@@ -844,22 +892,83 @@ export default function ProofSearchCanvas({
844892 ) ;
845893}
846894
847- function wrapText ( ctx , text , maxWidth ) {
848- const words = String ( text || '' ) . split ( / \s + / ) . filter ( Boolean ) ;
849- if ( ! words . length ) return [ '(empty clause)' ] ;
895+ function tokenizeClause ( text ) {
896+ const raw = String ( text || '' ) . trim ( ) ;
897+ if ( ! raw ) {
898+ return [ { text : '(empty clause)' , type : 'empty' } ] ;
899+ }
900+ const regex = / ( \s + | \$ f a l s e | \$ t r u e | ! = | < = | > = | < = > | = > | [ ( ) | , & = : . ~ ] | [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * | \d + | [ ^ \s ] ) / g;
901+ const tokens = [ ] ;
902+ let match ;
903+ while ( ( match = regex . exec ( raw ) ) !== null ) {
904+ const value = match [ 0 ] ;
905+ tokens . push ( { text : value , type : classifyToken ( value ) } ) ;
906+ }
907+ return tokens ;
908+ }
909+
910+ function classifyToken ( token ) {
911+ if ( / ^ \s + $ / . test ( token ) ) return 'ws' ;
912+ if ( / ^ \$ f a l s e $ | ^ \$ t r u e $ / i. test ( token ) ) return 'bool' ;
913+ if ( / ^ [ A - Z ] / . test ( token ) ) return 'var' ;
914+ if ( / ^ \d + $ / . test ( token ) ) return 'num' ;
915+ if ( / ^ ( ~ | \| | & | = > | < = > | = | ! = | < = | > = ) $ / . test ( token ) ) return 'op' ;
916+ if ( / ^ [ a - z ] / . test ( token ) ) return 'sym' ;
917+ if ( / ^ [ ( ) | , & = : . ~ ] $ / . test ( token ) ) return 'punct' ;
918+ return 'sym' ;
919+ }
920+
921+ function tokenColor ( type , palette ) {
922+ switch ( type ) {
923+ case 'op' :
924+ return palette . tooltipOp ;
925+ case 'var' :
926+ return palette . tooltipVar ;
927+ case 'num' :
928+ return palette . tooltipNum ;
929+ case 'bool' :
930+ return palette . tooltipBool ;
931+ case 'punct' :
932+ return palette . tooltipPunct ;
933+ case 'ws' :
934+ return palette . tooltipText ;
935+ default :
936+ return palette . tooltipSym || palette . tooltipText ;
937+ }
938+ }
939+
940+ function wrapTokens ( ctx , tokens , maxWidth ) {
850941 const lines = [ ] ;
851- let line = words [ 0 ] ;
852- for ( let i = 1 ; i < words . length ; i += 1 ) {
853- const test = line + ' ' + words [ i ] ;
854- if ( ctx . measureText ( test ) . width > maxWidth ) {
855- lines . push ( line ) ;
856- line = words [ i ] ;
857- } else {
858- line = test ;
942+ let line = [ ] ;
943+ let width = 0 ;
944+ const pushLine = ( ) => {
945+ if ( line . length ) lines . push ( line ) ;
946+ line = [ ] ;
947+ width = 0 ;
948+ } ;
949+ tokens . forEach ( ( tok ) => {
950+ if ( tok . type === 'ws' && line . length === 0 ) return ;
951+ const tokenWidth = ctx . measureText ( tok . text ) . width ;
952+ if ( tokenWidth > maxWidth && tok . text . length > 1 ) {
953+ // Break long tokens into characters
954+ tok . text . split ( '' ) . forEach ( ( ch ) => {
955+ const chWidth = ctx . measureText ( ch ) . width ;
956+ if ( line . length && width + chWidth > maxWidth ) {
957+ pushLine ( ) ;
958+ }
959+ line . push ( { text : ch , type : tok . type } ) ;
960+ width += chWidth ;
961+ } ) ;
962+ return ;
859963 }
860- }
861- lines . push ( line ) ;
862- return lines . slice ( 0 , 6 ) ;
964+ if ( line . length && width + tokenWidth > maxWidth ) {
965+ pushLine ( ) ;
966+ }
967+ line . push ( tok ) ;
968+ width += tokenWidth ;
969+ } ) ;
970+ if ( line . length ) lines . push ( line ) ;
971+ return lines . slice ( 0 , 8 ) ;
863972}
864973
865974function drawArrow ( ctx , x1 , y1 , x2 , y2 ) {
0 commit comments