11use crate :: {
2- core:: { Event , NotePosition , SequenceDiagram } ,
2+ core:: { BoxColor , Event , NotePosition , SequenceDiagram } ,
33 render:: render_sequence,
44 theme:: Theme ,
55 ui:: {
@@ -23,6 +23,7 @@ pub const CONFIRM: WidgetId = WidgetId("Confirm");
2323pub const TEXT_INPUT : WidgetId = WidgetId ( "TextInput" ) ;
2424pub const SELECT_PARTICIPANT : WidgetId = WidgetId ( "SelectParticipant" ) ;
2525pub const SELECT_POSITION : WidgetId = WidgetId ( "SelectPosition" ) ;
26+ pub const SELECT_BOX_COLOR : WidgetId = WidgetId ( "SelectBoxColor" ) ;
2627
2728#[ derive( Default ) ]
2829pub struct AppState {
@@ -42,6 +43,7 @@ pub fn setup_world(world: &mut World, diagram: SequenceDiagram) {
4243 select_position_keybindings ( world) ;
4344 text_input_keybindings ( world) ;
4445 confirm_keybindings ( world) ;
46+ select_box_color_keybindings ( world) ;
4547}
4648
4749fn normal_keybindings ( world : & mut World ) {
@@ -158,6 +160,51 @@ fn normal_keybindings(world: &mut World) {
158160 }
159161 } ) ;
160162
163+ kb. bind ( NORMAL , 'B' , "Remove box" , |world| {
164+ let selection = world. get :: < EditorState > ( ) . selection ;
165+ if let Selection :: Participant ( idx) = selection {
166+ let box_idx = world
167+ . get :: < SequenceDiagram > ( )
168+ . boxes
169+ . iter ( )
170+ . position ( |b| b. start <= idx && idx <= b. end ) ;
171+ if let Some ( box_idx) = box_idx {
172+ world. get_mut :: < SequenceDiagram > ( ) . remove_box_at ( box_idx) ;
173+ } else {
174+ world. get_mut :: < EditorState > ( ) . set_status ( "No box here" ) ;
175+ }
176+ }
177+ } ) ;
178+
179+ kb. bind ( NORMAL , 'd' , "Delete selected" , |world| {
180+ let selection = world. get :: < EditorState > ( ) . selection ;
181+ match selection {
182+ Selection :: Participant ( idx) => {
183+ world. get_mut :: < SequenceDiagram > ( ) . remove_participant ( idx) ;
184+ let new_count = world. get :: < SequenceDiagram > ( ) . participant_count ( ) ;
185+ let editor = world. get_mut :: < EditorState > ( ) ;
186+ if new_count == 0 {
187+ editor. clear_selection ( ) ;
188+ } else {
189+ editor. selection = Selection :: Participant ( idx. min ( new_count - 1 ) ) ;
190+ }
191+ }
192+ Selection :: Event ( idx) => {
193+ world. get_mut :: < SequenceDiagram > ( ) . remove_event ( idx) ;
194+ let new_count = world. get :: < SequenceDiagram > ( ) . event_count ( ) ;
195+ let editor = world. get_mut :: < EditorState > ( ) ;
196+ if new_count == 0 {
197+ editor. clear_selection ( ) ;
198+ } else {
199+ editor. selection = Selection :: Event ( idx. min ( new_count - 1 ) ) ;
200+ }
201+ }
202+ Selection :: None => {
203+ world. get_mut :: < SequenceDiagram > ( ) . events . pop ( ) ;
204+ }
205+ }
206+ } ) ;
207+
161208 kb. bind_many (
162209 NORMAL ,
163210 keys ! [ 'l' , KeyCode :: Right ] ,
@@ -388,6 +435,21 @@ fn normal_keybindings(world: &mut World) {
388435 }
389436 } ) ;
390437
438+ kb. bind ( NORMAL , 'b' , "Add box" , |world| {
439+ let participant_count = world. get :: < SequenceDiagram > ( ) . participant_count ( ) ;
440+ if participant_count >= 1 {
441+ let selection = world. get :: < EditorState > ( ) . selection ;
442+ let editor = world. get_mut :: < EditorState > ( ) ;
443+ editor. mode = EditorMode :: SelectBoxStart ;
444+ editor. selected_index = match selection {
445+ Selection :: Participant ( idx) => idx,
446+ _ => 0 ,
447+ } ;
448+ editor. box_start = None ;
449+ editor. box_end = None ;
450+ }
451+ } ) ;
452+
391453 kb. bind ( NORMAL , 'C' , "Clear diagram" , |world| {
392454 let diagram = world. get :: < SequenceDiagram > ( ) ;
393455 if !diagram. participants . is_empty ( ) || !diagram. events . is_empty ( ) {
@@ -557,6 +619,7 @@ fn confirm_keybindings(world: &mut World) {
557619 let diagram = world. get_mut :: < SequenceDiagram > ( ) ;
558620 diagram. participants . clear ( ) ;
559621 diagram. events . clear ( ) ;
622+ diagram. boxes . clear ( ) ;
560623 world. get_mut :: < EditorState > ( ) . reset ( ) ;
561624 } ) ;
562625
@@ -565,6 +628,46 @@ fn confirm_keybindings(world: &mut World) {
565628 } ) ;
566629}
567630
631+ fn select_box_color_keybindings ( world : & mut World ) {
632+ let kb = world. get_mut :: < Keybindings > ( ) ;
633+
634+ kb. bind (
635+ SELECT_BOX_COLOR ,
636+ KeyBinding :: key ( KeyCode :: Enter ) ,
637+ "Confirm" ,
638+ handle_input_confirm,
639+ ) ;
640+
641+ kb. bind (
642+ SELECT_BOX_COLOR ,
643+ KeyBinding :: key ( KeyCode :: Esc ) ,
644+ "Cancel" ,
645+ |world| {
646+ world. get_mut :: < EditorState > ( ) . reset ( ) ;
647+ } ,
648+ ) ;
649+
650+ kb. bind_many (
651+ SELECT_BOX_COLOR ,
652+ keys ! [ 'h' , 'k' , KeyCode :: Left , KeyCode :: Up ] ,
653+ "Previous" ,
654+ |world| {
655+ let editor = world. get_mut :: < EditorState > ( ) ;
656+ editor. box_color = editor. box_color . prev ( ) ;
657+ } ,
658+ ) ;
659+
660+ kb. bind_many (
661+ SELECT_BOX_COLOR ,
662+ keys ! [ 'j' , 'l' , KeyCode :: Right , KeyCode :: Down ] ,
663+ "Next" ,
664+ |world| {
665+ let editor = world. get_mut :: < EditorState > ( ) ;
666+ editor. box_color = editor. box_color . next ( ) ;
667+ } ,
668+ ) ;
669+ }
670+
568671fn handle_input_confirm ( world : & mut World ) {
569672 let mode = world. get :: < EditorState > ( ) . mode . clone ( ) ;
570673 match mode {
@@ -734,6 +837,47 @@ fn handle_input_confirm(world: &mut World) {
734837 EditorMode :: EditNoteText => {
735838 save_note_changes ( world) ;
736839 }
840+ EditorMode :: SelectBoxStart => {
841+ let selected = world. get :: < EditorState > ( ) . selected_index ;
842+ let editor = world. get_mut :: < EditorState > ( ) ;
843+ editor. box_start = Some ( selected) ;
844+ editor. mode = EditorMode :: SelectBoxEnd ;
845+ editor. selected_index = selected;
846+ }
847+ EditorMode :: SelectBoxEnd => {
848+ let selected = world. get :: < EditorState > ( ) . selected_index ;
849+ let editor = world. get_mut :: < EditorState > ( ) ;
850+ editor. box_end = Some ( selected) ;
851+ editor. box_color = BoxColor :: default ( ) ;
852+ editor. mode = EditorMode :: SelectBoxColor ;
853+ }
854+ EditorMode :: SelectBoxColor => {
855+ let editor = world. get_mut :: < EditorState > ( ) ;
856+ editor. mode = EditorMode :: InputBoxLabel ;
857+ editor. input_buffer . clear ( ) ;
858+ }
859+ EditorMode :: InputBoxLabel => {
860+ let editor_state = world. get :: < EditorState > ( ) . clone ( ) ;
861+ let label = editor_state. input_buffer . trim ( ) . to_string ( ) ;
862+ if let ( Some ( mut start) , Some ( mut end) ) = ( editor_state. box_start , editor_state. box_end )
863+ {
864+ if start > end {
865+ std:: mem:: swap ( & mut start, & mut end) ;
866+ }
867+ let ok = world. get_mut :: < SequenceDiagram > ( ) . add_box (
868+ label,
869+ editor_state. box_color ,
870+ start,
871+ end,
872+ ) ;
873+ if !ok {
874+ world
875+ . get_mut :: < EditorState > ( )
876+ . set_status ( "Boxes cannot overlap" ) ;
877+ }
878+ }
879+ world. get_mut :: < EditorState > ( ) . reset ( ) ;
880+ }
737881 _ => { }
738882 }
739883}
@@ -805,6 +949,7 @@ pub fn active_widgets(world: &World) -> Vec<WidgetId> {
805949 EditorMode :: Normal | EditorMode :: Help => vec ! [ NORMAL ] ,
806950 EditorMode :: ConfirmClear => vec ! [ CONFIRM ] ,
807951 EditorMode :: SelectNotePosition | EditorMode :: EditNotePosition => vec ! [ SELECT_POSITION ] ,
952+ EditorMode :: SelectBoxColor => vec ! [ SELECT_BOX_COLOR ] ,
808953 m if m. is_selecting_participant ( ) => vec ! [ SELECT_PARTICIPANT ] ,
809954 m if m. is_text_input ( ) => vec ! [ TEXT_INPUT ] ,
810955 _ => vec ! [ ] ,
@@ -838,7 +983,8 @@ pub fn render(frame: &mut Frame, world: &mut World) {
838983 | EditorMode :: EditMessage
839984 | EditorMode :: RenameParticipant
840985 | EditorMode :: InputNoteText
841- | EditorMode :: EditNoteText => {
986+ | EditorMode :: EditNoteText
987+ | EditorMode :: InputBoxLabel => {
842988 render_input_popup ( frame, world) ;
843989 }
844990 EditorMode :: SelectFrom
@@ -863,6 +1009,12 @@ pub fn render(frame: &mut Frame, world: &mut World) {
8631009 EditorMode :: ConfirmClear => {
8641010 render_confirm_dialog ( frame, world) ;
8651011 }
1012+ EditorMode :: SelectBoxStart | EditorMode :: SelectBoxEnd => {
1013+ render_box_participant_selector ( frame, area, world) ;
1014+ }
1015+ EditorMode :: SelectBoxColor => {
1016+ render_box_color_selector ( frame, area, world) ;
1017+ }
8661018 EditorMode :: Normal => { }
8671019 }
8681020}
@@ -1125,6 +1277,151 @@ fn render_note_position_selector(frame: &mut Frame, area: Rect, world: &World) {
11251277 }
11261278}
11271279
1280+ fn render_box_participant_selector ( frame : & mut Frame , area : Rect , world : & World ) {
1281+ let editor = world. get :: < EditorState > ( ) ;
1282+ let diagram = world. get :: < SequenceDiagram > ( ) ;
1283+ let theme = world. get :: < Theme > ( ) ;
1284+
1285+ let participants = & diagram. participants ;
1286+ let cursor = editor. selected_index ;
1287+ let is_selecting_end = matches ! ( editor. mode, EditorMode :: SelectBoxEnd ) ;
1288+ let box_start = editor. box_start ;
1289+
1290+ let popup_width = 40 . min ( area. width . saturating_sub ( 4 ) ) ;
1291+ let popup_height = ( participants. len ( ) as u16 + 4 ) . min ( area. height . saturating_sub ( 4 ) ) ;
1292+
1293+ let popup_area = centered_rect ( popup_width, popup_height, area) ;
1294+
1295+ frame. render_widget ( ratatui:: widgets:: Clear , popup_area) ;
1296+
1297+ let title = if is_selecting_end {
1298+ " Box: End Participant "
1299+ } else {
1300+ " Box: Start Participant "
1301+ } ;
1302+ let block = Block :: default ( )
1303+ . title ( title)
1304+ . borders ( Borders :: ALL )
1305+ . border_style ( theme. border ) ;
1306+
1307+ let inner = block. inner ( popup_area) ;
1308+ frame. render_widget ( block, popup_area) ;
1309+
1310+ for ( i, name) in participants. iter ( ) . enumerate ( ) {
1311+ if i as u16 >= inner. height {
1312+ break ;
1313+ }
1314+
1315+ let y = inner. y + i as u16 ;
1316+ let is_cursor = cursor == i;
1317+ let is_start_marker = is_selecting_end && box_start == Some ( i) ;
1318+
1319+ let prefix = if is_cursor { "\u{25b6} " } else { " " } ;
1320+ let style = if is_cursor {
1321+ theme. selected
1322+ } else if is_start_marker {
1323+ theme. accent
1324+ } else {
1325+ theme. text
1326+ } ;
1327+
1328+ frame. render_widget (
1329+ Paragraph :: new ( format ! ( "{prefix}{name}" ) ) . style ( style) ,
1330+ Rect {
1331+ x : inner. x ,
1332+ y,
1333+ width : inner. width ,
1334+ height : 1 ,
1335+ } ,
1336+ ) ;
1337+ }
1338+ }
1339+
1340+ fn render_box_color_selector ( frame : & mut Frame , area : Rect , world : & World ) {
1341+ use ratatui:: style:: Style ;
1342+ let editor = world. get :: < EditorState > ( ) ;
1343+ let theme = world. get :: < Theme > ( ) ;
1344+
1345+ let current_color = editor. box_color ;
1346+ let colors = BoxColor :: all ( ) ;
1347+
1348+ let popup_width = 30 . min ( area. width . saturating_sub ( 4 ) ) ;
1349+ let popup_height = ( colors. len ( ) as u16 + 4 ) . min ( area. height . saturating_sub ( 4 ) ) ;
1350+
1351+ let popup_area = centered_rect ( popup_width, popup_height, area) ;
1352+
1353+ frame. render_widget ( ratatui:: widgets:: Clear , popup_area) ;
1354+
1355+ let block = Block :: default ( )
1356+ . title ( " Box Color " )
1357+ . borders ( Borders :: ALL )
1358+ . border_style ( theme. border ) ;
1359+
1360+ let inner = block. inner ( popup_area) ;
1361+ frame. render_widget ( block, popup_area) ;
1362+
1363+ for ( i, color) in colors. iter ( ) . enumerate ( ) {
1364+ let y = inner. y + i as u16 ;
1365+ if y >= inner. y + inner. height {
1366+ break ;
1367+ }
1368+
1369+ let is_selected = * color == current_color;
1370+ let swatch_color = box_swatch_color ( * color) ;
1371+ let prefix = if is_selected { "\u{25b6} " } else { " " } ;
1372+ let name_style = if is_selected {
1373+ theme. selected
1374+ } else {
1375+ theme. text
1376+ } ;
1377+
1378+ let line = Line :: from ( vec ! [
1379+ Span :: raw( prefix) ,
1380+ Span :: styled( "\u{25a0} " , Style :: default ( ) . fg( swatch_color) ) ,
1381+ Span :: styled( color. as_mermaid_str( ) , name_style) ,
1382+ ] ) ;
1383+
1384+ frame. render_widget (
1385+ Paragraph :: new ( line) ,
1386+ Rect {
1387+ x : inner. x ,
1388+ y,
1389+ width : inner. width ,
1390+ height : 1 ,
1391+ } ,
1392+ ) ;
1393+ }
1394+
1395+ if inner. height > colors. len ( ) as u16 {
1396+ let hint_y = inner. y + inner. height - 1 ;
1397+ frame. render_widget (
1398+ Paragraph :: new ( "Enter: confirm | Esc: cancel" )
1399+ . style ( theme. muted )
1400+ . alignment ( Alignment :: Right ) ,
1401+ Rect {
1402+ x : inner. x ,
1403+ y : hint_y,
1404+ width : inner. width ,
1405+ height : 1 ,
1406+ } ,
1407+ ) ;
1408+ }
1409+ }
1410+
1411+ fn box_swatch_color ( color : BoxColor ) -> ratatui:: style:: Color {
1412+ use ratatui:: style:: Color ;
1413+ match color {
1414+ BoxColor :: Blue => Color :: Rgb ( 100 , 150 , 220 ) ,
1415+ BoxColor :: Green => Color :: Rgb ( 80 , 180 , 100 ) ,
1416+ BoxColor :: Red => Color :: Rgb ( 220 , 80 , 80 ) ,
1417+ BoxColor :: Yellow => Color :: Rgb ( 200 , 180 , 50 ) ,
1418+ BoxColor :: Orange => Color :: Rgb ( 220 , 130 , 50 ) ,
1419+ BoxColor :: Purple => Color :: Rgb ( 170 , 80 , 200 ) ,
1420+ BoxColor :: Aqua => Color :: Rgb ( 60 , 190 , 200 ) ,
1421+ BoxColor :: Gray => Color :: Rgb ( 150 , 150 , 150 ) ,
1422+ }
1423+ }
1424+
11281425fn centered_rect ( width : u16 , height : u16 , area : Rect ) -> Rect {
11291426 let [ area] = Layout :: vertical ( [ Constraint :: Length ( height) ] )
11301427 . flex ( Flex :: Center )
0 commit comments