Textbox.hx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. package textbox;
  2. import textbox.CommandValues;
  3. import textbox.Text;
  4. import textbox.TextboxLine;
  5. import textbox.TextPool;
  6. import flixel.group.FlxGroup;
  7. import flixel.group.FlxSpriteGroup;
  8. import flixel.util.typeLimit.OneOfTwo;
  9. import haxe.Utf8;
  10. using StringTools;
  11. enum Status
  12. {
  13. EMPTY;
  14. WRITING;
  15. PAUSED;
  16. FULL;
  17. DONE;
  18. }
  19. typedef TextboxCharacter = OneOfTwo<String, CommandValues>;
  20. // Callback typedefs
  21. // To be called when the textbox's status changes
  22. typedef StatusChangeCallback = Status -> Void;
  23. // To be called when a character is shown (for sound callbacks, timing or other).
  24. typedef CharacterDisplayCallback = Text -> Void;
  25. /**
  26. * The holy mother of the textboxes.
  27. * Accepts text with tokens to enable and disable per-character effects.
  28. * Accept a settings structure to customize the basic behavior (such as font options or text speed.)
  29. * The token can take two shapes
  30. * - @XX0 => disable effect 0xXX
  31. * - @XX1AABBCC => set effect 0xXX with args 0xAA, 0xBB 0xCC
  32. */
  33. class Textbox extends FlxSpriteGroup {
  34. public function new(X:Float, Y:Float, settings:Settings)
  35. {
  36. super(X, Y);
  37. this.settings = settings;
  38. status = DONE;
  39. currentCharacterIndex = 0;
  40. currentLineIndex = 0;
  41. timerBeforeNewCharacter = 0;
  42. willResume = false;
  43. // Sub structure allocation.
  44. characters = [];
  45. characterPool = new TextPool();
  46. lines = [for(i in 0...settings.numLines) new TextBoxLine()];
  47. // Those ones can only be set when the lines are created, else we crash.
  48. visible = false;
  49. active = false;
  50. effects = [];
  51. for (i in 0 ... TextEffectArray.effectClasses.length)
  52. {
  53. effects.push({
  54. command:i,
  55. activated:false,
  56. arg1:0,
  57. arg2:0,
  58. arg3:0
  59. });
  60. }
  61. }
  62. public override function update(elapsed:Float)
  63. {
  64. // If asked to continue after getting a full state
  65. if(willResume)
  66. {
  67. if(status == FULL)
  68. moveTextUp();
  69. status = WRITING;
  70. willResume = false;
  71. }
  72. else if(!(status == PAUSED || status == DONE))
  73. {
  74. // Nothing to do here
  75. timerBeforeNewCharacter += elapsed;
  76. while(timerBeforeNewCharacter > timePerCharacter)
  77. {
  78. if(status == WRITING)
  79. {
  80. advanceCharacter();
  81. }
  82. timerBeforeNewCharacter -= timePerCharacter;
  83. }
  84. }
  85. super.update(elapsed);
  86. }
  87. // When called, the textbox will go poof and disables itselfs from the scene.
  88. public function dismiss()
  89. {
  90. if(!visible)
  91. {
  92. return;
  93. }
  94. // TODO : add a tween hook.
  95. visible = false;
  96. active = false;
  97. }
  98. // When called, the textbox will come on and activates itself into the scene.
  99. public function bring()
  100. {
  101. // TODO : add a tween hook.
  102. startWriting();
  103. visible = true;
  104. active = true;
  105. }
  106. // Sets a new string to the textbox and clears the shown characters.
  107. public function setText(text:String)
  108. {
  109. for(line in lines)
  110. {
  111. // Puts back every used character into the pool.
  112. for(character in line.dispose())
  113. {
  114. remove(character);
  115. characterPool.put(character);
  116. }
  117. }
  118. prepareString(text);
  119. // Ready.
  120. status = EMPTY;
  121. }
  122. // When called, this functions sets back some variables back for starting typing again.
  123. public function startWriting()
  124. {
  125. currentCharacterIndex = 0;
  126. currentLineIndex = 0;
  127. timerBeforeNewCharacter = 0;
  128. resetTextEffects();
  129. status = WRITING;
  130. }
  131. // Small function to ask for the rest of the text when FULL.
  132. public function continueWriting()
  133. {
  134. if(status == PAUSED || status == FULL)
  135. {
  136. willResume = true;
  137. }
  138. }
  139. function resetTextEffects()
  140. {
  141. for (effect in effects)
  142. {
  143. effect =
  144. {
  145. command:0,
  146. activated:false,
  147. arg1:0,
  148. arg2:0,
  149. arg3:0
  150. };
  151. }
  152. }
  153. // Parses the set string and fill the character array with the possible characters and commands
  154. function prepareString(text:String)
  155. {
  156. characters = [];
  157. var is_in_command = false;
  158. var command:CommandValues =
  159. {
  160. command: 0,
  161. activated:false,
  162. arg1: 0,
  163. arg2: 0,
  164. arg3: 0
  165. };
  166. var current_hex:String = "0x";
  167. var in_command_step:Int = 0;
  168. for(i in 0...Utf8.length(text))
  169. {
  170. var char_code = Utf8.charCodeAt(text, i);
  171. var current_character = Utf8.sub(text, i, 1);
  172. // If we're still parsing a command code
  173. if(is_in_command)
  174. {
  175. // Quick shortcut to check if the code is @@ to put @ in the text, just interrupt the parsing.
  176. if(current_character == "@" && in_command_step == 0)
  177. {
  178. is_in_command = false;
  179. characters.push(current_character);
  180. continue;
  181. }
  182. // Spacing
  183. // TODO : find a better way to determine if it's a space character than only that character
  184. if (current_character == " ")
  185. {
  186. continue;
  187. }
  188. // Continue parsing the hex code.
  189. current_hex += current_character;
  190. if((in_command_step == 2 && current_hex.length == 3) || current_hex.length == 4)
  191. {
  192. // If we parsed a pair, just put it in the correct variable.
  193. var value:Null<Int> = Std.parseInt(current_hex);
  194. // Bad parsing
  195. if(value == null)
  196. {
  197. is_in_command = false;
  198. continue;
  199. }
  200. switch(in_command_step)
  201. {
  202. case 1:
  203. command.command = value;
  204. case 2:
  205. command.activated = value != 0;
  206. case 4:
  207. command.arg1 = value;
  208. case 6:
  209. command.arg2 = value;
  210. case 8:
  211. command.arg3 = value;
  212. }
  213. current_hex = "0x";
  214. }
  215. // Go forward in the process
  216. in_command_step++;
  217. // And stop it if we had enough characters.
  218. if(in_command_step == 9 || (in_command_step == 3 && !command.activated))
  219. {
  220. is_in_command = false;
  221. characters.push(command);
  222. command =
  223. {
  224. command: 0,
  225. activated:false,
  226. arg1: 0,
  227. arg2: 0,
  228. arg3: 0
  229. };
  230. }
  231. }
  232. else
  233. {
  234. // Go into the hex code system if requested.
  235. if(char_code == '@'.charCodeAt(0))
  236. {
  237. is_in_command = true;
  238. in_command_step = 0;
  239. current_hex = "0x";
  240. command.command = 0;
  241. command.activated = false;
  242. command.arg1 = 0;
  243. command.arg2 = 0;
  244. command.arg3 = 0;
  245. }
  246. else{
  247. characters.push(current_character);
  248. }
  249. }
  250. }
  251. // Decided that the system wouldn't add a partial command code at the end of a text entry.
  252. }
  253. // This one is a helluva function but it does everything you need.
  254. function advanceCharacter()
  255. {
  256. // Just avoid an access exception.
  257. if(currentCharacterIndex >= characters.length)
  258. {
  259. status = DONE;
  260. return;
  261. }
  262. var current_character = characters[currentCharacterIndex];
  263. // If space, pre-calculate next word's length to word wrap.
  264. if(!(Std.is(current_character, String) && (!cast(current_character, String).isSpace(0) || cast(current_character, String) == '\n'))){
  265. // We have to build a string containing the next characters to calculate the size of the line.
  266. var word:String = " ";
  267. var index:Int = currentCharacterIndex+1;
  268. // So, while we're finding non-invisible characters
  269. while(index < characters.length)
  270. {
  271. var forward_character = characters[index];
  272. if(!Std.is(forward_character, String))
  273. {
  274. index++;
  275. continue;
  276. }
  277. var forward_char:String = cast(characters[index], String);
  278. if(!forward_char.isSpace(0))
  279. word += forward_char;
  280. else
  281. break;
  282. index++;
  283. }
  284. // TODO : please don't make bigass words
  285. // SO, if we're going over the limit, just go to the next line.
  286. if(lines[currentLineIndex].projectWidth(word) > settings.textFieldWidth)
  287. {
  288. currentCharacterIndex++;
  289. if(currentLineIndex < settings.numLines-1){
  290. currentLineIndex++;
  291. }
  292. else {
  293. status = FULL;
  294. }
  295. return;
  296. }
  297. }
  298. else if(Std.is(current_character, String)){
  299. // Now, character wrap should be useless but let's keep it.
  300. if(cast(current_character, String) == '\n' || lines[currentLineIndex].projectWidth(current_character) > settings.textFieldWidth)
  301. {
  302. if(currentLineIndex < settings.numLines-1){
  303. currentLineIndex++;
  304. }
  305. else {
  306. status = FULL;
  307. return;
  308. }
  309. }
  310. }
  311. if(Std.is(current_character, String))
  312. {
  313. var char:String = cast(current_character, String);
  314. // Get a new character from the pool
  315. var new_character:Text = characterPool.get();
  316. // Preparing it for the default style.
  317. new_character.autoSize = true;
  318. new_character.font = settings.font;
  319. new_character.size = settings.fontSize;
  320. new_character.text = char;
  321. new_character.color = settings.color;
  322. new_character.y = currentLineIndex * new_character.height;
  323. new_character.x = lines[currentLineIndex].text_width;
  324. for (effect in effects)
  325. {
  326. var characterEffect = new_character.effects[effect.command];
  327. characterEffect.reset(effect.arg1,effect.arg2,effect.arg3, 0);
  328. characterEffect.setActive(effect.activated);
  329. characterEffect.apply(new_character);
  330. }
  331. // This line is only for the opacity tweens to work.
  332. new_character.alpha = alpha;
  333. // Raaaaaise from the deeead.
  334. new_character.revive();
  335. // Put it in the line and go forward
  336. lines[currentLineIndex].push(new_character);
  337. add(new_character);
  338. if(characterDisplayCallback != null)
  339. {
  340. characterDisplayCallback(new_character);
  341. }
  342. currentCharacterIndex++;
  343. }
  344. else
  345. {
  346. var command:CommandValues = cast current_character;
  347. effects[command.command] = command;
  348. currentCharacterIndex++;
  349. timerBeforeNewCharacter += timePerCharacter;
  350. }
  351. }
  352. // THis function is only called when having to continue spitting out characters after going FULL
  353. function moveTextUp()
  354. {
  355. // Clearing the first line and putting its characters in the pool.
  356. var characters_to_dispose = lines[0].dispose();
  357. for(character in characters_to_dispose)
  358. {
  359. remove(character);
  360. characterPool.put(character);
  361. }
  362. // Moving the text one line upwrads.
  363. for(i in 1...settings.numLines)
  364. {
  365. var characters_to_pass:FlxTypedGroup<Text> = lines[i].dispose();
  366. for(character in characters_to_pass)
  367. {
  368. character.y -= character.height;
  369. }
  370. lines[i-1].take(characters_to_pass);
  371. }
  372. currentLineIndex = settings.numLines - 1;
  373. }
  374. // callbacks
  375. public var characterDisplayCallback: CharacterDisplayCallback;
  376. public var statusChangeCallback: StatusChangeCallback;
  377. // Variable members
  378. var status(default, set):Status;
  379. var settings:Settings;
  380. // internal
  381. // The character array. Calculated from a sent string, it contains the list of characters to show or commands to execute.
  382. var characters:Array<TextboxCharacter>;
  383. var timePerCharacter(get, never):Float;
  384. // The position among the whole character array.
  385. var currentCharacterIndex:Int;
  386. // The timer before adding a new character
  387. var timerBeforeNewCharacter:Float;
  388. // A TextPool to easily manage the ever-changing characters with a minimal amount of allocation.
  389. var characterPool:TextPool;
  390. // The line array, contains the data structures to store and manage the characters.
  391. var lines:Array<TextBoxLine>;
  392. // The text line's index.
  393. var currentLineIndex:Int;
  394. // Just a small internal boolean to notice when a FULL textbox can continue.
  395. var willResume:Bool;
  396. // Textbox things. Kinds of act like a pen.
  397. // Stores the current color of the text.
  398. var effects:Array<CommandValues>;
  399. // getter/setters
  400. public function get_timePerCharacter():Float
  401. {
  402. return 1./settings.charactersPerSecond;
  403. }
  404. // Small helper to manage the status_icon.
  405. function set_status(status:Status)
  406. {
  407. var previousStatus = this.status;
  408. this.status = status;
  409. if (status != previousStatus && statusChangeCallback != null)
  410. {
  411. statusChangeCallback(status);
  412. }
  413. return status;
  414. }
  415. public override function set_alpha(Alpha:Float):Float
  416. {
  417. for(line in lines)
  418. line.characters.forEach(function(s){s.set_alpha(Alpha);});
  419. return super.set_alpha(Alpha);
  420. }
  421. // Why do I need to do this...
  422. public override function set_visible(Visible:Bool):Bool
  423. {
  424. visible = Visible;
  425. group.set_visible(true);
  426. return visible;
  427. }
  428. public override function set_active(Active:Bool):Bool
  429. {
  430. active = Active;
  431. group.set_active(true);
  432. return active;
  433. }
  434. }