container.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. import { Nullable } from "babylonjs/types";
  2. import { Logger } from "babylonjs/Misc/logger";
  3. import { Control } from "./control";
  4. import { Measure } from "../measure";
  5. import { AdvancedDynamicTexture } from "../advancedDynamicTexture";
  6. import { _TypeStore } from 'babylonjs/Misc/typeStore';
  7. /**
  8. * Root class for 2D containers
  9. * @see https://doc.babylonjs.com/how_to/gui#containers
  10. */
  11. export class Container extends Control {
  12. /** @hidden */
  13. public _children = new Array<Control>();
  14. /** @hidden */
  15. protected _measureForChildren = Measure.Empty();
  16. /** @hidden */
  17. protected _background = "";
  18. /** @hidden */
  19. protected _adaptWidthToChildren = false;
  20. /** @hidden */
  21. protected _adaptHeightToChildren = false;
  22. /**
  23. * Gets or sets a boolean indicating that layout cycle errors should be displayed on the console
  24. */
  25. public logLayoutCycleErrors = false;
  26. /**
  27. * Gets or sets the number of layout cycles (a change involved by a control while evaluating the layout) allowed
  28. */
  29. public maxLayoutCycle = 3;
  30. /** Gets or sets a boolean indicating if the container should try to adapt to its children height */
  31. public get adaptHeightToChildren(): boolean {
  32. return this._adaptHeightToChildren;
  33. }
  34. public set adaptHeightToChildren(value: boolean) {
  35. if (this._adaptHeightToChildren === value) {
  36. return;
  37. }
  38. this._adaptHeightToChildren = value;
  39. if (value) {
  40. this.height = "100%";
  41. }
  42. this._markAsDirty();
  43. }
  44. /** Gets or sets a boolean indicating if the container should try to adapt to its children width */
  45. public get adaptWidthToChildren(): boolean {
  46. return this._adaptWidthToChildren;
  47. }
  48. public set adaptWidthToChildren(value: boolean) {
  49. if (this._adaptWidthToChildren === value) {
  50. return;
  51. }
  52. this._adaptWidthToChildren = value;
  53. if (value) {
  54. this.width = "100%";
  55. }
  56. this._markAsDirty();
  57. }
  58. /** Gets or sets background color */
  59. public get background(): string {
  60. return this._background;
  61. }
  62. public set background(value: string) {
  63. if (this._background === value) {
  64. return;
  65. }
  66. this._background = value;
  67. this._markAsDirty();
  68. }
  69. /** Gets the list of children */
  70. public get children(): Control[] {
  71. return this._children;
  72. }
  73. /**
  74. * Creates a new Container
  75. * @param name defines the name of the container
  76. */
  77. constructor(public name?: string) {
  78. super(name);
  79. }
  80. protected _getTypeName(): string {
  81. return "Container";
  82. }
  83. public _flagDescendantsAsMatrixDirty(): void {
  84. for (var child of this.children) {
  85. child._markMatrixAsDirty();
  86. }
  87. }
  88. /**
  89. * Gets a child using its name
  90. * @param name defines the child name to look for
  91. * @returns the child control if found
  92. */
  93. public getChildByName(name: string): Nullable<Control> {
  94. for (var child of this.children) {
  95. if (child.name === name) {
  96. return child;
  97. }
  98. }
  99. return null;
  100. }
  101. /**
  102. * Gets a child using its type and its name
  103. * @param name defines the child name to look for
  104. * @param type defines the child type to look for
  105. * @returns the child control if found
  106. */
  107. public getChildByType(name: string, type: string): Nullable<Control> {
  108. for (var child of this.children) {
  109. if (child.typeName === type) {
  110. return child;
  111. }
  112. }
  113. return null;
  114. }
  115. /**
  116. * Search for a specific control in children
  117. * @param control defines the control to look for
  118. * @returns true if the control is in child list
  119. */
  120. public containsControl(control: Control): boolean {
  121. return this.children.indexOf(control) !== -1;
  122. }
  123. /**
  124. * Adds a new control to the current container
  125. * @param control defines the control to add
  126. * @returns the current container
  127. */
  128. public addControl(control: Nullable<Control>): Container {
  129. if (!control) {
  130. return this;
  131. }
  132. var index = this._children.indexOf(control);
  133. if (index !== -1) {
  134. return this;
  135. }
  136. control._link(this._host);
  137. control._markAllAsDirty();
  138. this._reOrderControl(control);
  139. this._markAsDirty();
  140. return this;
  141. }
  142. /**
  143. * Removes all controls from the current container
  144. * @returns the current container
  145. */
  146. public clearControls(): Container {
  147. let children = this.children.slice();
  148. for (var child of children) {
  149. this.removeControl(child);
  150. }
  151. return this;
  152. }
  153. /**
  154. * Removes a control from the current container
  155. * @param control defines the control to remove
  156. * @returns the current container
  157. */
  158. public removeControl(control: Control): Container {
  159. var index = this._children.indexOf(control);
  160. if (index !== -1) {
  161. this._children.splice(index, 1);
  162. control.parent = null;
  163. }
  164. control.linkWithMesh(null);
  165. if (this._host) {
  166. this._host._cleanControlAfterRemoval(control);
  167. }
  168. this._markAsDirty();
  169. return this;
  170. }
  171. /** @hidden */
  172. public _reOrderControl(control: Control): void {
  173. this.removeControl(control);
  174. let wasAdded = false;
  175. for (var index = 0; index < this._children.length; index++) {
  176. if (this._children[index].zIndex > control.zIndex) {
  177. this._children.splice(index, 0, control);
  178. wasAdded = true;
  179. break;
  180. }
  181. }
  182. if (!wasAdded) {
  183. this._children.push(control);
  184. }
  185. control.parent = this;
  186. this._markAsDirty();
  187. }
  188. /** @hidden */
  189. public _offsetLeft(offset: number) {
  190. super._offsetLeft(offset);
  191. for (var child of this._children) {
  192. child._offsetLeft(offset);
  193. }
  194. }
  195. /** @hidden */
  196. public _offsetTop(offset: number) {
  197. super._offsetTop(offset);
  198. for (var child of this._children) {
  199. child._offsetTop(offset);
  200. }
  201. }
  202. /** @hidden */
  203. public _markAllAsDirty(): void {
  204. super._markAllAsDirty();
  205. for (var index = 0; index < this._children.length; index++) {
  206. this._children[index]._markAllAsDirty();
  207. }
  208. }
  209. /** @hidden */
  210. protected _localDraw(context: CanvasRenderingContext2D): void {
  211. if (this._background) {
  212. context.save();
  213. if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) {
  214. context.shadowColor = this.shadowColor;
  215. context.shadowBlur = this.shadowBlur;
  216. context.shadowOffsetX = this.shadowOffsetX;
  217. context.shadowOffsetY = this.shadowOffsetY;
  218. }
  219. context.fillStyle = this._background;
  220. context.fillRect(this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
  221. context.restore();
  222. }
  223. }
  224. /** @hidden */
  225. public _link(host: AdvancedDynamicTexture): void {
  226. super._link(host);
  227. for (var child of this._children) {
  228. child._link(host);
  229. }
  230. }
  231. /** @hidden */
  232. protected _beforeLayout() {
  233. // Do nothing
  234. }
  235. /** @hidden */
  236. protected _processMeasures(parentMeasure: Measure, context: CanvasRenderingContext2D): void {
  237. if (this._isDirty || !this._cachedParentMeasure.isEqualsTo(parentMeasure)) {
  238. super._processMeasures(parentMeasure, context);
  239. this._evaluateClippingState(parentMeasure);
  240. }
  241. }
  242. /** @hidden */
  243. public _layout(parentMeasure: Measure, context: CanvasRenderingContext2D): boolean {
  244. if (!this.isDirty && (!this.isVisible || this.notRenderable)) {
  245. return false;
  246. }
  247. this.host._numLayoutCalls++;
  248. if (this._isDirty) {
  249. this._currentMeasure.transformToRef(this._transformMatrix, this._prevCurrentMeasureTransformedIntoGlobalSpace);
  250. }
  251. let rebuildCount = 0;
  252. context.save();
  253. this._applyStates(context);
  254. this._beforeLayout();
  255. do {
  256. let computedWidth = -1;
  257. let computedHeight = -1;
  258. this._rebuildLayout = false;
  259. this._processMeasures(parentMeasure, context);
  260. if (!this._isClipped) {
  261. for (var child of this._children) {
  262. child._tempParentMeasure.copyFrom(this._measureForChildren);
  263. if (child._layout(this._measureForChildren, context)) {
  264. if (this.adaptWidthToChildren && child._width.isPixel) {
  265. computedWidth = Math.max(computedWidth, child._currentMeasure.width + child.paddingLeftInPixels + child.paddingRightInPixels);
  266. }
  267. if (this.adaptHeightToChildren && child._height.isPixel) {
  268. computedHeight = Math.max(computedHeight, child._currentMeasure.height + child.paddingTopInPixels + child.paddingBottomInPixels);
  269. }
  270. }
  271. }
  272. if (this.adaptWidthToChildren && computedWidth >= 0) {
  273. computedWidth += this.paddingLeftInPixels + this.paddingRightInPixels;
  274. if (this.width !== computedWidth + "px") {
  275. this.width = computedWidth + "px";
  276. this._rebuildLayout = true;
  277. }
  278. }
  279. if (this.adaptHeightToChildren && computedHeight >= 0) {
  280. computedHeight += this.paddingTopInPixels + this.paddingBottomInPixels;
  281. if (this.height !== computedHeight + "px") {
  282. this.height = computedHeight + "px";
  283. this._rebuildLayout = true;
  284. }
  285. }
  286. this._postMeasure();
  287. }
  288. rebuildCount++;
  289. }
  290. while (this._rebuildLayout && rebuildCount < this.maxLayoutCycle);
  291. if (rebuildCount >= 3 && this.logLayoutCycleErrors) {
  292. Logger.Error(`Layout cycle detected in GUI (Container name=${this.name}, uniqueId=${this.uniqueId})`);
  293. }
  294. context.restore();
  295. if (this._isDirty) {
  296. this.invalidateRect();
  297. this._isDirty = false;
  298. }
  299. return true;
  300. }
  301. protected _postMeasure() {
  302. // Do nothing by default
  303. }
  304. /** @hidden */
  305. public _draw(context: CanvasRenderingContext2D, invalidatedRectangle?: Measure): void {
  306. this._localDraw(context);
  307. if (this.clipChildren) {
  308. this._clipForChildren(context);
  309. }
  310. for (var child of this._children) {
  311. // Only redraw parts of the screen that are invalidated
  312. if (invalidatedRectangle) {
  313. if (!child._intersectsRect(invalidatedRectangle)) {
  314. continue;
  315. }
  316. }
  317. child._render(context, invalidatedRectangle);
  318. }
  319. }
  320. public getDescendantsToRef(results: Control[], directDescendantsOnly: boolean = false, predicate?: (control: Control) => boolean): void {
  321. if (!this.children) {
  322. return;
  323. }
  324. for (var index = 0; index < this.children.length; index++) {
  325. var item = this.children[index];
  326. if (!predicate || predicate(item)) {
  327. results.push(item);
  328. }
  329. if (!directDescendantsOnly) {
  330. item.getDescendantsToRef(results, false, predicate);
  331. }
  332. }
  333. }
  334. /** @hidden */
  335. public _processPicking(x: number, y: number, type: number, pointerId: number, buttonIndex: number, deltaX?: number, deltaY?: number): boolean {
  336. if (!this._isEnabled || !this.isVisible || this.notRenderable) {
  337. return false;
  338. }
  339. if (!super.contains(x, y)) {
  340. return false;
  341. }
  342. // Checking backwards to pick closest first
  343. for (var index = this._children.length - 1; index >= 0; index--) {
  344. var child = this._children[index];
  345. if (child._processPicking(x, y, type, pointerId, buttonIndex, deltaX, deltaY)) {
  346. if (child.hoverCursor) {
  347. this._host._changeCursor(child.hoverCursor);
  348. }
  349. return true;
  350. }
  351. }
  352. if (!this.isHitTestVisible) {
  353. return false;
  354. }
  355. return this._processObservables(type, x, y, pointerId, buttonIndex, deltaX, deltaY);
  356. }
  357. /** @hidden */
  358. protected _additionalProcessing(parentMeasure: Measure, context: CanvasRenderingContext2D): void {
  359. super._additionalProcessing(parentMeasure, context);
  360. this._measureForChildren.copyFrom(this._currentMeasure);
  361. }
  362. /** Releases associated resources */
  363. public dispose() {
  364. super.dispose();
  365. for (var index = this.children.length - 1; index >= 0; index--) {
  366. this.children[index].dispose();
  367. }
  368. }
  369. }
  370. _TypeStore.RegisteredTypes["BABYLON.GUI.Container"] = Container;