@@ -134,7 +134,7 @@ mod platform {
134134 pub struct Host {
135135 panels : Vec < Retained < NSPanel > > ,
136136 last_rects : Vec < crate :: scanning:: PaintedRect > ,
137- last_title : Option < ( String , Rect , f64 ) > ,
137+ last_title : Option < ( String , Rect , f64 , Option < Rect > ) > ,
138138 title : Option < (
139139 Retained < objc2_app_kit:: NSView > ,
140140 Retained < objc2_app_kit:: NSTextField > ,
@@ -188,26 +188,32 @@ mod platform {
188188 Ok ( ( ) )
189189 }
190190 pub fn menu_title ( & mut self , text : & str , rect : Rect , scale : f64 ) -> Result < ( ) , String > {
191+ self . text_panel ( text, rect, scale, None )
192+ }
193+ pub fn text_panel (
194+ & mut self ,
195+ text : & str ,
196+ requested : Rect ,
197+ scale : f64 ,
198+ screen : Option < Rect > ,
199+ ) -> Result < ( ) , String > {
191200 use objc2_app_kit:: {
192201 NSFont , NSFontWeightSemibold , NSTextAlignment , NSTextField , NSView ,
193202 } ;
194203 use objc2_foundation:: NSString ;
195- if self
196- . last_title
197- . as_ref ( )
198- . is_some_and ( |( old_text, old_rect, old_scale) | {
199- old_text == text && * old_rect == rect && * old_scale == scale
200- } )
201- {
204+ if self . last_title . as_ref ( ) . is_some_and (
205+ |( old_text, old_rect, old_scale, old_screen) | {
206+ old_text == text
207+ && * old_rect == requested
208+ && * old_scale == scale
209+ && * old_screen == screen
210+ } ,
211+ ) {
202212 return Ok ( ( ) ) ;
203213 }
204214 let mtm = MainThreadMarker :: new ( ) . ok_or ( "Menu title requires the main thread." ) ?;
205- self . render ( & [ crate :: scanning:: PaintedRect {
206- rect,
207- color : [ 30 , 35 , 46 ] ,
208- opacity : 0 ,
209- role : crate :: scanning:: VisualRole :: Accent ,
210- } ] ) ?;
215+ let mut rect =
216+ screen. map_or ( requested, |screen| hud_rect ( requested, screen, 0.0 , scale) ) ;
211217 let bounds = NSRect :: new ( NSPoint :: new ( 0.0 , 0.0 ) , NSSize :: new ( rect. width , rect. height ) ) ;
212218 if self . title . is_none ( ) {
213219 let view = NSView :: initWithFrame ( NSView :: alloc ( mtm) , bounds) ;
@@ -220,19 +226,6 @@ mod platform {
220226 }
221227 let ( view, label) = self . title . as_ref ( ) . unwrap ( ) ;
222228 view. setFrame ( bounds) ;
223- let layer = view
224- . layer ( )
225- . ok_or ( "Menu title background is unavailable." ) ?;
226- layer. setBackgroundColor ( Some (
227- & NSColor :: colorWithSRGBRed_green_blue_alpha (
228- 30.0 / 255.0 ,
229- 35.0 / 255.0 ,
230- 46.0 / 255.0 ,
231- 1.0 ,
232- )
233- . CGColor ( ) ,
234- ) ) ;
235- layer. setCornerRadius ( 10.0 * scale) ;
236229 let value = NSString :: from_str ( text) ;
237230 if label. stringValue ( ) != value {
238231 label. setStringValue ( & value) ;
@@ -247,45 +240,50 @@ mod platform {
247240 . ok_or ( "Menu title text is unavailable." ) ?
248241 . cellSizeForBounds ( NSRect :: new (
249242 NSPoint :: new ( 0.0 , 0.0 ) ,
250- NSSize :: new ( width, rect . height ) ,
243+ NSSize :: new ( width, f64 :: MAX ) ,
251244 ) ) ;
252- let height = measured. height . min ( rect. height ) ;
245+ if let Some ( screen) = screen {
246+ rect = hud_rect ( requested, screen, measured. height , scale) ;
247+ }
248+ let height = measured. height . min ( ( rect. height - 24.0 * scale) . max ( 1.0 ) ) ;
249+ view. setFrame ( NSRect :: new (
250+ NSPoint :: new ( 0.0 , 0.0 ) ,
251+ NSSize :: new ( rect. width , rect. height ) ,
252+ ) ) ;
253+ let layer = view
254+ . layer ( )
255+ . ok_or ( "Menu title background is unavailable." ) ?;
256+ layer. setBackgroundColor ( Some (
257+ & NSColor :: colorWithSRGBRed_green_blue_alpha (
258+ 30.0 / 255.0 ,
259+ 35.0 / 255.0 ,
260+ 46.0 / 255.0 ,
261+ 1.0 ,
262+ )
263+ . CGColor ( ) ,
264+ ) ) ;
265+ layer. setCornerRadius ( 10.0 * scale) ;
266+
253267 label. setFrame ( NSRect :: new (
254268 NSPoint :: new ( 12.0 * scale, ( rect. height - height) / 2.0 ) ,
255269 NSSize :: new ( width, height) ,
256270 ) ) ;
257- if self . panels [ 0 ] . contentView ( ) . as_deref ( ) != Some ( view. as_ref ( ) ) {
258- self . panels [ 0 ] . setContentView ( Some ( view) ) ;
259- }
260- self . last_title = Some ( ( text. to_owned ( ) , rect, scale) ) ;
261- Ok ( ( ) )
262- }
263- pub fn prompt ( & mut self , text : & str , rect : Rect , scale : f64 ) -> Result < ( ) , String > {
264- use objc2_app_kit:: { NSFont , NSTextField } ;
265- use objc2_foundation:: NSString ;
266- self . last_title = None ;
267- let mtm = MainThreadMarker :: new ( ) . ok_or ( "Prompt requires the main thread." ) ?;
271+ let view = view. clone ( ) ;
268272 self . render ( & [ crate :: scanning:: PaintedRect {
269273 rect,
270274 color : [ 30 , 35 , 46 ] ,
271- opacity : 255 ,
275+ opacity : 0 ,
272276 role : crate :: scanning:: VisualRole :: Accent ,
273277 } ] ) ?;
274- let label = NSTextField :: wrappingLabelWithString ( & NSString :: from_str ( text) , mtm) ;
275- label. setFont ( Some ( & NSFont :: systemFontOfSize ( 20.0 * scale) ) ) ;
276- label. setTextColor ( Some ( & NSColor :: whiteColor ( ) ) ) ;
277- label. setFrame ( NSRect :: new (
278- NSPoint :: new ( 12.0 * scale, 12.0 * scale) ,
279- NSSize :: new (
280- ( rect. width - 24.0 * scale) . max ( 1.0 ) ,
281- ( rect. height - 24.0 * scale) . max ( 1.0 ) ,
282- ) ,
283- ) ) ;
284- self . panels [ 0 ] . setBackgroundColor ( Some ( & NSColor :: blackColor ( ) ) ) ;
285- self . panels [ 0 ] . setContentView ( Some ( & label) ) ;
286- self . panels [ 0 ] . orderFrontRegardless ( ) ;
278+ if self . panels [ 0 ] . contentView ( ) . as_deref ( ) != Some ( view. as_ref ( ) ) {
279+ self . panels [ 0 ] . setContentView ( Some ( & view) ) ;
280+ }
281+ self . last_title = Some ( ( text. to_owned ( ) , requested, scale, screen) ) ;
287282 Ok ( ( ) )
288283 }
284+ pub fn prompt ( & mut self , text : & str , rect : Rect , scale : f64 ) -> Result < ( ) , String > {
285+ self . text_panel ( text, rect, scale, None )
286+ }
289287 pub fn tile ( & mut self , tile : & crate :: scanning:: FrameTile ) -> Result < ( ) , String > {
290288 use objc2_app_kit:: { NSFont , NSImageView , NSTextAlignment , NSTextField , NSView } ;
291289 use objc2_foundation:: NSString ;
@@ -444,6 +442,24 @@ pub fn close_foreground_window() -> Result<(), String> {
444442}
445443
446444impl Host {
445+ pub fn hud_prompt (
446+ & mut self ,
447+ text : & str ,
448+ rect : Rect ,
449+ legacy_scale : f64 ,
450+ presentation : crate :: scanning:: HudPresentation ,
451+ ) -> Result < ( ) , String > {
452+ #[ cfg( target_os = "macos" ) ]
453+ {
454+ let _ = legacy_scale;
455+ self . text_panel ( text, rect, presentation. scale , Some ( presentation. screen ) )
456+ }
457+ #[ cfg( not( target_os = "macos" ) ) ]
458+ {
459+ let _ = presentation;
460+ self . prompt ( text, rect, legacy_scale)
461+ }
462+ }
447463 pub fn label (
448464 & mut self ,
449465 label : & crate :: scanning:: FrameLabel ,
@@ -457,6 +473,9 @@ impl Host {
457473 if let Some ( MenuTitle { rect, scale } ) = menu_title {
458474 let _ = ( rect, scale) ;
459475 }
476+ if let Some ( hud) = & label. hud {
477+ return self . hud_prompt ( & label. text , label. rect , label. scale , hud. clone ( ) ) ;
478+ }
460479 self . prompt ( & label. text , label. rect , label. scale )
461480 }
462481}
@@ -490,11 +509,73 @@ pub fn menu_title_geometry(
490509 } )
491510}
492511
512+ #[ cfg( any( target_os = "macos" , test) ) ]
513+ fn hud_rect ( requested : Rect , screen : Rect , text_height : f64 , scale : f64 ) -> Rect {
514+ let width = requested. width . min ( screen. width ) . max ( 1.0 ) ;
515+ let height = requested
516+ . height
517+ . max ( text_height + 24.0 * scale)
518+ . min ( screen. height )
519+ . max ( 1.0 ) ;
520+ Rect {
521+ x : requested. x . clamp ( screen. x , screen. x + screen. width - width) ,
522+ y : requested
523+ . y
524+ . clamp ( screen. y , screen. y + screen. height - height) ,
525+ width,
526+ height,
527+ }
528+ }
529+
493530#[ cfg( test) ]
494531mod title_tests {
495532 use super :: * ;
496533 use crate :: scan_menu:: { Kind , Menu } ;
497534
535+ #[ test]
536+ fn wrapped_hud_grows_and_stays_inside_scaled_negative_displays ( ) {
537+ for scale in [ 1.0 , 2.0 ] {
538+ let screen = Rect {
539+ x : -640.0 * scale,
540+ y : -200.0 * scale,
541+ width : 640.0 * scale,
542+ height : 480.0 * scale,
543+ } ;
544+ let requested = Rect {
545+ x : screen. x + 20.0 * scale,
546+ y : screen. y + 420.0 * scale,
547+ width : 360.0 * scale,
548+ height : 64.0 * scale,
549+ } ;
550+ let rect = hud_rect ( requested, screen, 120.0 * scale, scale) ;
551+ assert_eq ! ( rect. width, requested. width) ;
552+ assert_eq ! ( rect. x, requested. x) ;
553+ assert_eq ! ( rect. height, 144.0 * scale) ;
554+ assert_eq ! ( rect. y + rect. height, screen. y + screen. height) ;
555+ assert_eq ! (
556+ hud_rect( requested, screen, 20.0 * scale, scale) . height,
557+ requested. height
558+ ) ;
559+ }
560+ }
561+
562+ #[ test]
563+ fn tiny_display_bounds_cap_long_hud_and_preserve_visible_geometry ( ) {
564+ let screen = Rect {
565+ x : 100.0 ,
566+ y : -50.0 ,
567+ width : 160.0 ,
568+ height : 100.0 ,
569+ } ;
570+ let requested = Rect {
571+ x : 110.0 ,
572+ y : -30.0 ,
573+ width : 720.0 ,
574+ height : 64.0 ,
575+ } ;
576+ assert_eq ! ( hud_rect( requested, screen, 400.0 , 1.0 ) , screen) ;
577+ }
578+
498579 #[ test]
499580 fn titles_follow_tile_edges_without_changing_header_spacing ( ) {
500581 for screen in [
0 commit comments